From 164a1d4f0ebbaa592fc7e785c7a83d2f4b50bf57 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 17:52:14 +0000 Subject: [PATCH] feat(api): document cache hits in OpenAPI Co-Authored-By: Virgil --- openapi.go | 26 ++++++++++++++++++++++---- openapi_test.go | 8 ++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/openapi.go b/openapi.go index 41ec95f..64766dc 100644 --- a/openapi.go +++ b/openapi.go @@ -129,7 +129,7 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any { "bearerAuth": []any{}, }, }, - "responses": operationResponses(rd.Response), + "responses": operationResponses(method, rd.Response), } // Add request body for methods that accept one. @@ -170,7 +170,12 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any { // 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(dataSchema map[string]any) map[string]any { +func operationResponses(method string, dataSchema map[string]any) map[string]any { + successHeaders := mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders()) + if method == "get" { + successHeaders = mergeHeaders(successHeaders, cacheSuccessHeaders()) + } + return map[string]any{ "200": map[string]any{ "description": "Successful response", @@ -179,7 +184,7 @@ func operationResponses(dataSchema map[string]any) map[string]any { "schema": envelopeSchema(dataSchema), }, }, - "headers": mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders()), + "headers": successHeaders, }, "400": map[string]any{ "description": "Bad request", @@ -249,7 +254,7 @@ func healthResponses() map[string]any { "schema": envelopeSchema(map[string]any{"type": "string"}), }, }, - "headers": mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders()), + "headers": mergeHeaders(standardResponseHeaders(), rateLimitSuccessHeaders(), cacheSuccessHeaders()), }, "429": map[string]any{ "description": "Too many requests", @@ -402,6 +407,19 @@ func rateLimitSuccessHeaders() map[string]any { } } +// cacheSuccessHeaders documents the response header emitted on successful +// cache hits. +func cacheSuccessHeaders() map[string]any { + return map[string]any{ + "X-Cache": map[string]any{ + "description": "Indicates the response was served from the in-memory cache", + "schema": map[string]any{ + "type": "string", + }, + }, + } +} + // standardResponseHeaders documents headers emitted by the response envelope // middleware on all responses when request IDs are enabled. func standardResponseHeaders() map[string]any { diff --git a/openapi_test.go b/openapi_test.go index 8c7e242..2087abb 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -102,6 +102,11 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) { if _, ok := health504Headers["X-RateLimit-Reset"]; !ok { t.Fatal("expected X-RateLimit-Reset header on /health 504 response") } + health200 := health["responses"].(map[string]any)["200"].(map[string]any) + health200Headers := health200["headers"].(map[string]any) + if _, ok := health200Headers["X-Cache"]; !ok { + t.Fatal("expected X-Cache header on /health 200 response") + } // Verify system tag exists. tags := spec["tags"].([]any) @@ -369,6 +374,9 @@ func TestSpecBuilder_Good_EnvelopeWrapping(t *testing.T) { if _, ok := headers["X-RateLimit-Reset"]; !ok { t.Fatal("expected X-RateLimit-Reset header on 200 response") } + if _, ok := headers["X-Cache"]; !ok { + t.Fatal("expected X-Cache header on 200 response") + } content := resp200["content"].(map[string]any) appJSON := content["application/json"].(map[string]any) schema := appJSON["schema"].(map[string]any)