From f53617c507a2766e3152ab5c9d013ba858d553c9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 02:56:13 +0000 Subject: [PATCH] feat(openapi): document sunsetted operations as gone Co-Authored-By: Virgil --- openapi.go | 16 ++++++++++++++-- openapi_test.go | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/openapi.go b/openapi.go index 7ceeb48..742ca9e 100644 --- a/openapi.go +++ b/openapi.go @@ -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", diff --git a/openapi_test.go b/openapi_test.go index 099e9a9..dae31db 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -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"} {