feat(openapi): document sunsetted operations as gone

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 02:56:13 +00:00
parent 13f901b88f
commit f53617c507
2 changed files with 31 additions and 2 deletions

View file

@ -229,7 +229,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.ResponseHeaders, rd.Security, deprecationHeaders),
"responses": operationResponses(method, rd.StatusCode, rd.Response, rd.ResponseExample, rd.ResponseHeaders, rd.Security, deprecated, rd.SunsetDate, rd.Replacement, deprecationHeaders),
}
if deprecated {
operation["deprecated"] = true
@ -358,7 +358,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, example any, responseHeaders map[string]string, security []map[string][]string, deprecationHeaders map[string]any) map[string]any {
func operationResponses(method string, statusCode int, dataSchema map[string]any, example any, responseHeaders map[string]string, security []map[string][]string, deprecated bool, sunsetDate, replacement string, deprecationHeaders map[string]any) map[string]any {
documentedHeaders := documentedResponseHeaders(responseHeaders)
successHeaders := mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders(), deprecationHeaders, documentedHeaders)
if method == "get" {
@ -428,6 +428,18 @@ func operationResponses(method string, statusCode int, dataSchema map[string]any
},
}
if deprecated && (strings.TrimSpace(sunsetDate) != "" || strings.TrimSpace(replacement) != "") {
responses["410"] = map[string]any{
"description": "Gone",
"content": map[string]any{
"application/json": map[string]any{
"schema": envelopeSchema(nil),
},
},
"headers": errorHeaders,
}
}
if !isPublic {
responses["401"] = map[string]any{
"description": "Unauthorised",

View file

@ -1873,6 +1873,23 @@ func TestSpecBuilder_Good_DeprecatedOperation(t *testing.T) {
}
}
gone, ok := responses["410"].(map[string]any)
if !ok {
t.Fatal("expected 410 Gone response for sunsetted operation")
}
if got := gone["description"]; got != "Gone" {
t.Fatalf("expected 410 response description Gone, got %v", got)
}
goneHeaders, ok := gone["headers"].(map[string]any)
if !ok {
t.Fatalf("expected 410 response headers map, got %T", gone["headers"])
}
for _, name := range []string{"Deprecation", "Sunset", "Link", "X-API-Warn"} {
if _, ok := goneHeaders[name]; !ok {
t.Fatalf("expected deprecation header %q in 410 response headers", name)
}
}
components := spec["components"].(map[string]any)
headerComponents := components["headers"].(map[string]any)
for _, name := range []string{"deprecation", "sunset", "link", "xapiwarn"} {