feat(openapi): document response headers
This commit is contained in:
parent
00c20ea6e8
commit
ccfbe57faf
3 changed files with 87 additions and 2 deletions
1
group.go
1
group.go
|
|
@ -64,6 +64,7 @@ type RouteDescription struct {
|
|||
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.
|
||||
ResponseHeaders map[string]string
|
||||
}
|
||||
|
||||
// ParameterDescription describes an OpenAPI parameter for a route.
|
||||
|
|
|
|||
33
openapi.go
33
openapi.go
|
|
@ -192,7 +192,7 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
|
|||
"summary": rd.Summary,
|
||||
"description": rd.Description,
|
||||
"operationId": operationID(method, fullPath, operationIDs),
|
||||
"responses": operationResponses(method, rd.StatusCode, rd.Response, rd.ResponseExample, rd.Security),
|
||||
"responses": operationResponses(method, rd.StatusCode, rd.Response, rd.ResponseExample, rd.ResponseHeaders, rd.Security),
|
||||
}
|
||||
if rd.Deprecated {
|
||||
operation["deprecated"] = true
|
||||
|
|
@ -321,11 +321,14 @@ 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, example any, security []map[string][]string) map[string]any {
|
||||
func operationResponses(method string, statusCode int, dataSchema map[string]any, example any, responseHeaders map[string]string, security []map[string][]string) map[string]any {
|
||||
successHeaders := mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders())
|
||||
if method == "get" {
|
||||
successHeaders = mergeHeaders(successHeaders, cacheSuccessHeaders())
|
||||
}
|
||||
if headers := documentedResponseHeaders(responseHeaders); len(headers) > 0 {
|
||||
successHeaders = mergeHeaders(successHeaders, headers)
|
||||
}
|
||||
|
||||
isPublic := security != nil && len(security) == 0
|
||||
|
||||
|
|
@ -847,6 +850,32 @@ func cacheSuccessHeaders() map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
// documentedResponseHeaders converts route-specific response header metadata
|
||||
// into OpenAPI header objects.
|
||||
func documentedResponseHeaders(headers map[string]string) map[string]any {
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make(map[string]any, len(headers))
|
||||
for name, description := range headers {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
out[name] = map[string]any{
|
||||
"description": description,
|
||||
"schema": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// standardResponseHeaders documents headers emitted by the response envelope
|
||||
// middleware on all responses when request IDs are enabled.
|
||||
func standardResponseHeaders() map[string]any {
|
||||
|
|
|
|||
|
|
@ -1059,6 +1059,61 @@ func TestSpecBuilder_Good_RequestExampleWithoutSchema(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_ResponseHeaders(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
group := &specStubGroup{
|
||||
name: "downloads",
|
||||
basePath: "/api",
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/exports/{id}",
|
||||
Summary: "Download export",
|
||||
ResponseHeaders: map[string]string{
|
||||
"Content-Disposition": "Download filename suggested by the server",
|
||||
"X-Export-ID": "Identifier for the generated export",
|
||||
},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := sb.Build([]api.RouteGroup{group})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
responses := spec["paths"].(map[string]any)["/api/exports/{id}"].(map[string]any)["get"].(map[string]any)["responses"].(map[string]any)
|
||||
resp200 := responses["200"].(map[string]any)
|
||||
headers, ok := resp200["headers"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected headers map, got %T", resp200["headers"])
|
||||
}
|
||||
|
||||
header, ok := headers["Content-Disposition"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("expected Content-Disposition response header to be documented")
|
||||
}
|
||||
if header["description"] != "Download filename suggested by the server" {
|
||||
t.Fatalf("expected header description to be preserved, got %v", header["description"])
|
||||
}
|
||||
schema := header["schema"].(map[string]any)
|
||||
if schema["type"] != "string" {
|
||||
t.Fatalf("expected response header schema type string, got %v", schema["type"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_PathParameters(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue