feat(api): expose reusable OpenAPI response components

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 07:10:55 +00:00
parent fb7702df67
commit 69dd16cba6
2 changed files with 106 additions and 0 deletions

View file

@ -182,6 +182,7 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
},
"securitySchemes": securitySchemeComponents(sb.SecuritySchemes),
"headers": deprecationHeaderComponents(),
"responses": responseComponents(),
}
return json.MarshalIndent(spec, "", " ")
@ -631,6 +632,78 @@ func deprecationHeaderComponents() map[string]any {
}
}
// responseComponents returns reusable OpenAPI response objects for the
// common error cases exposed by the framework. The path operations still
// inline their concrete headers so existing callers keep the same output,
// but these components make the response catalogue available for reuse.
func responseComponents() map[string]any {
return map[string]any{
"BadRequest": map[string]any{
"description": "Bad request",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": standardResponseHeaders(),
},
"Unauthorized": map[string]any{
"description": "Unauthorised",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": standardResponseHeaders(),
},
"Forbidden": map[string]any{
"description": "Forbidden",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": standardResponseHeaders(),
},
"RateLimitExceeded": map[string]any{
"description": "Too many requests",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": mergeHeaders(standardResponseHeaders(), rateLimitHeaders()),
},
"GatewayTimeout": map[string]any{
"description": "Gateway timeout",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": standardResponseHeaders(),
},
"InternalServerError": map[string]any{
"description": "Internal server error",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": standardResponseHeaders(),
},
"Gone": map[string]any{
"description": "Gone",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": mergeHeaders(standardResponseHeaders(), deprecationResponseHeaders(true, "", "")),
},
}
}
// securitySchemeComponents builds the OpenAPI security scheme registry.
// bearerAuth stays available by default, while callers can add or override
// additional scheme definitions for custom security requirements.

View file

@ -257,6 +257,39 @@ func TestSpecBuilder_Good_CustomSecuritySchemesAreMerged(t *testing.T) {
}
}
func TestSpecBuilder_Good_CommonResponseComponentsArePublished(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",
Version: "1.0.0",
}
data, err := sb.Build(nil)
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)
}
components := spec["components"].(map[string]any)
responses := components["responses"].(map[string]any)
for _, name := range []string{
"BadRequest",
"Unauthorized",
"Forbidden",
"RateLimitExceeded",
"GatewayTimeout",
"InternalServerError",
"Gone",
} {
if _, ok := responses[name]; !ok {
t.Fatalf("expected %s response component in spec", name)
}
}
}
func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",