From 071de51bb53df0e86c11ff31ef4f51d9a3d3de1d Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 20:40:53 +0000 Subject: [PATCH] feat(openapi): mark deprecated operations in spec Expose route-level deprecation in generated OpenAPI operations and cover it with a regression test. Co-Authored-By: Virgil --- docs/architecture.md | 1 + group.go | 2 ++ openapi.go | 3 +++ openapi_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index f15df25..5bde594 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -128,6 +128,7 @@ type RouteDescription struct { Summary string Description string Tags []string + Deprecated bool StatusCode int Parameters []ParameterDescription RequestBody map[string]any diff --git a/group.go b/group.go index 1e589ab..631297b 100644 --- a/group.go +++ b/group.go @@ -39,6 +39,8 @@ type RouteDescription struct { Summary string // Short summary Description string // Long description Tags []string // OpenAPI tags for grouping + // Deprecated marks the operation as deprecated in OpenAPI. + Deprecated bool // StatusCode is the documented 2xx success status code. // Zero defaults to 200. StatusCode int diff --git a/openapi.go b/openapi.go index 2356aee..43dfd1f 100644 --- a/openapi.go +++ b/openapi.go @@ -166,6 +166,9 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any { "operationId": operationID(method, fullPath, operationIDs), "responses": operationResponses(method, rd.StatusCode, rd.Response), } + if rd.Deprecated { + operation["deprecated"] = true + } if rd.Security != nil { operation["security"] = rd.Security } else { diff --git a/openapi_test.go b/openapi_test.go index 6ce6dc1..ed5758d 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -1129,6 +1129,48 @@ func TestSpecBuilder_Good_DefaultTagsFromGroupName(t *testing.T) { } } +func TestSpecBuilder_Good_DeprecatedOperation(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Version: "1.0.0", + } + + group := &specStubGroup{ + name: "legacy", + basePath: "/api/legacy", + descs: []api.RouteDescription{ + { + Method: "GET", + Path: "/status", + Summary: "Check legacy status", + Deprecated: true, + 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) + } + + op := spec["paths"].(map[string]any)["/api/legacy/status"].(map[string]any)["get"].(map[string]any) + deprecated, ok := op["deprecated"].(bool) + if !ok { + t.Fatalf("expected deprecated boolean, got %T", op["deprecated"]) + } + if !deprecated { + t.Fatal("expected deprecated operation to be marked true") + } +} + func TestSpecBuilder_Good_BlankTagsAreIgnored(t *testing.T) { sb := &api.SpecBuilder{ Title: "Test",