feat(openapi): add route examples to spec
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
bb7d88f3ce
commit
7e4d8eb179
3 changed files with 44 additions and 12 deletions
10
group.go
10
group.go
|
|
@ -46,10 +46,12 @@ type RouteDescription struct {
|
|||
StatusCode int
|
||||
// Security overrides the default bearerAuth requirement when non-nil.
|
||||
// Use an empty, non-nil slice to mark the route as public.
|
||||
Security []map[string][]string
|
||||
Parameters []ParameterDescription
|
||||
RequestBody map[string]any // JSON Schema for request body (nil for GET)
|
||||
Response map[string]any // JSON Schema for success response data
|
||||
Security []map[string][]string
|
||||
Parameters []ParameterDescription
|
||||
RequestBody map[string]any // JSON Schema for request body (nil for GET)
|
||||
RequestExample any // Optional example payload for the request body.
|
||||
Response map[string]any // JSON Schema for success response data
|
||||
ResponseExample any // Optional example payload for the success response.
|
||||
}
|
||||
|
||||
// ParameterDescription describes an OpenAPI parameter for a route.
|
||||
|
|
|
|||
28
openapi.go
28
openapi.go
|
|
@ -181,7 +181,7 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
"summary": rd.Summary,
|
||||
"description": rd.Description,
|
||||
"operationId": operationID(method, fullPath, operationIDs),
|
||||
"responses": operationResponses(method, rd.StatusCode, rd.Response),
|
||||
"responses": operationResponses(method, rd.StatusCode, rd.Response, rd.ResponseExample),
|
||||
}
|
||||
if rd.Deprecated {
|
||||
operation["deprecated"] = true
|
||||
|
|
@ -209,12 +209,17 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
// Add request body for methods that accept one.
|
||||
// The contract only excludes GET; other verbs may legitimately carry bodies.
|
||||
if rd.RequestBody != nil && method != "get" {
|
||||
requestMediaType := map[string]any{
|
||||
"schema": rd.RequestBody,
|
||||
}
|
||||
if rd.RequestExample != nil {
|
||||
requestMediaType["example"] = rd.RequestExample
|
||||
}
|
||||
|
||||
operation["requestBody"] = map[string]any{
|
||||
"required": true,
|
||||
"content": map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": rd.RequestBody,
|
||||
},
|
||||
"application/json": requestMediaType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -299,7 +304,7 @@ func normaliseOpenAPIPath(path string) string {
|
|||
// operationResponses builds the standard response set for a documented API
|
||||
// operation. The framework always exposes the common envelope responses, plus
|
||||
// middleware-driven 429 and 504 errors.
|
||||
func operationResponses(method string, statusCode int, dataSchema map[string]any) map[string]any {
|
||||
func operationResponses(method string, statusCode int, dataSchema map[string]any, example any) map[string]any {
|
||||
successHeaders := mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders())
|
||||
if method == "get" {
|
||||
successHeaders = mergeHeaders(successHeaders, cacheSuccessHeaders())
|
||||
|
|
@ -311,10 +316,17 @@ func operationResponses(method string, statusCode int, dataSchema map[string]any
|
|||
"headers": successHeaders,
|
||||
}
|
||||
if !isNoContentStatus(code) {
|
||||
content := map[string]any{
|
||||
"schema": envelopeSchema(dataSchema),
|
||||
}
|
||||
if example != nil {
|
||||
// Example payloads are optional, but when a route provides one we
|
||||
// expose it alongside the schema so generated docs stay useful.
|
||||
content["example"] = example
|
||||
}
|
||||
|
||||
successResponse["content"] = map[string]any{
|
||||
"application/json": map[string]any{
|
||||
"schema": envelopeSchema(dataSchema),
|
||||
},
|
||||
"application/json": content,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -304,12 +304,18 @@ func TestSpecBuilder_Good_WithDescribableGroup(t *testing.T) {
|
|||
"name": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
RequestExample: map[string]any{
|
||||
"name": "Widget",
|
||||
},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{"type": "integer"},
|
||||
},
|
||||
},
|
||||
ResponseExample: map[string]any{
|
||||
"id": 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -360,6 +366,18 @@ func TestSpecBuilder_Good_WithDescribableGroup(t *testing.T) {
|
|||
if postOp.(map[string]any)["requestBody"] == nil {
|
||||
t.Fatal("expected requestBody on POST /api/items/create")
|
||||
}
|
||||
requestBody := postOp.(map[string]any)["requestBody"].(map[string]any)
|
||||
appJSON := requestBody["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
if appJSON["example"].(map[string]any)["name"] != "Widget" {
|
||||
t.Fatalf("expected request example to be preserved, got %v", appJSON["example"])
|
||||
}
|
||||
|
||||
responses := postOp.(map[string]any)["responses"].(map[string]any)
|
||||
created := responses["200"].(map[string]any)
|
||||
createdJSON := created["content"].(map[string]any)["application/json"].(map[string]any)
|
||||
if createdJSON["example"].(map[string]any)["id"] != float64(42) {
|
||||
t.Fatalf("expected response example to be preserved, got %v", createdJSON["example"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_SecuredResponses(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue