From ec268c8100f6acb256e14af6270df11aa2762a6a Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 13:07:05 +0000 Subject: [PATCH] feat(api): default enabled transport paths in specs Treat enabled built-in transports as having their default paths when callers omit an explicit override. This keeps manual SpecBuilder usage aligned with the engine defaults and prevents Swagger, GraphQL, WebSocket, and SSE metadata from disappearing from generated documents.\n\nCo-Authored-By: Virgil --- openapi.go | 48 +++++++++++++++++++++++++++++++-------- openapi_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/openapi.go b/openapi.go index 27c0db8..d00ef59 100644 --- a/openapi.go +++ b/openapi.go @@ -107,7 +107,7 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { delete(spec["info"].(map[string]any), "summary") } - if swaggerPath := strings.TrimSpace(sb.SwaggerPath); swaggerPath != "" { + if swaggerPath := sb.effectiveSwaggerPath(); swaggerPath != "" { spec["x-swagger-ui-path"] = normaliseSwaggerPath(swaggerPath) } if sb.SwaggerEnabled { @@ -120,13 +120,13 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { spec["x-graphql-path"] = normaliseOpenAPIPath(graphqlPath) spec["x-graphql-playground"] = sb.GraphQLPlayground } - if wsPath := strings.TrimSpace(sb.WSPath); wsPath != "" { + if wsPath := sb.effectiveWSPath(); wsPath != "" { spec["x-ws-path"] = normaliseOpenAPIPath(wsPath) } if sb.WSEnabled { spec["x-ws-enabled"] = true } - if ssePath := strings.TrimSpace(sb.SSEPath); ssePath != "" { + if ssePath := sb.effectiveSSEPath(); ssePath != "" { spec["x-sse-path"] = normaliseOpenAPIPath(ssePath) } if sb.SSEEnabled { @@ -279,12 +279,12 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any { } } - if wsPath := strings.TrimSpace(sb.WSPath); wsPath != "" { + if wsPath := sb.effectiveWSPath(); wsPath != "" { wsPath = normaliseOpenAPIPath(wsPath) paths[wsPath] = wsPathItem(wsPath, operationIDs) } - if ssePath := strings.TrimSpace(sb.SSEPath); ssePath != "" { + if ssePath := sb.effectiveSSEPath(); ssePath != "" { ssePath = normaliseOpenAPIPath(ssePath) paths[ssePath] = ssePathItem(ssePath, operationIDs) } @@ -800,7 +800,7 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any { seen["graphql"] = true } - if ssePath := strings.TrimSpace(sb.SSEPath); ssePath != "" && !seen["events"] { + if ssePath := sb.effectiveSSEPath(); ssePath != "" && !seen["events"] { tags = append(tags, map[string]any{ "name": "events", "description": "Server-Sent Events endpoints", @@ -1849,16 +1849,46 @@ func sseResponseHeaders() map[string]any { } } -// effectiveGraphQLPath returns the configured GraphQL path, or the default -// GraphQL path when playground mode is enabled without an explicit path. +// effectiveGraphQLPath returns the configured GraphQL path or the default +// GraphQL path when GraphQL is enabled without an explicit path. func (sb *SpecBuilder) effectiveGraphQLPath() string { graphqlPath := strings.TrimSpace(sb.GraphQLPath) - if graphqlPath == "" && sb.GraphQLPlayground { + if graphqlPath == "" && (sb.GraphQLEnabled || sb.GraphQLPlayground) { return defaultGraphQLPath } return graphqlPath } +// effectiveSwaggerPath returns the configured Swagger UI path or the default +// path when Swagger is enabled without an explicit override. +func (sb *SpecBuilder) effectiveSwaggerPath() string { + swaggerPath := strings.TrimSpace(sb.SwaggerPath) + if swaggerPath == "" && sb.SwaggerEnabled { + return defaultSwaggerPath + } + return swaggerPath +} + +// effectiveWSPath returns the configured WebSocket path or the default path +// when WebSockets are enabled without an explicit override. +func (sb *SpecBuilder) effectiveWSPath() string { + wsPath := strings.TrimSpace(sb.WSPath) + if wsPath == "" && sb.WSEnabled { + return defaultWSPath + } + return wsPath +} + +// effectiveSSEPath returns the configured SSE path or the default path when +// SSE is enabled without an explicit override. +func (sb *SpecBuilder) effectiveSSEPath() string { + ssePath := strings.TrimSpace(sb.SSEPath) + if ssePath == "" && sb.SSEEnabled { + return defaultSSEPath + } + return ssePath +} + // documentedResponseHeaders converts route-specific response header metadata // into OpenAPI header objects. func documentedResponseHeaders(headers map[string]string) map[string]any { diff --git a/openapi_test.go b/openapi_test.go index 824379c..9c78c44 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -584,6 +584,66 @@ func TestSpecBuilder_Good_GraphQLPlaygroundDefaultsToGraphQLTag(t *testing.T) { } } +func TestSpecBuilder_Good_EnabledTransportsUseDefaultPaths(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Version: "1.0.0", + SwaggerEnabled: true, + GraphQLEnabled: true, + WSEnabled: true, + SSEEnabled: true, + } + + 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) + } + + if got := spec["x-swagger-ui-path"]; got != "/swagger" { + t.Fatalf("expected default swagger path, got %v", got) + } + if got := spec["x-graphql-path"]; got != "/graphql" { + t.Fatalf("expected default graphql path, got %v", got) + } + if got := spec["x-ws-path"]; got != "/ws" { + t.Fatalf("expected default websocket path, got %v", got) + } + if got := spec["x-sse-path"]; got != "/events" { + t.Fatalf("expected default sse path, got %v", got) + } + + paths := spec["paths"].(map[string]any) + for _, path := range []string{"/graphql", "/ws", "/events"} { + if _, ok := paths[path].(map[string]any); !ok { + t.Fatalf("expected %s path in spec", path) + } + } + + tags := spec["tags"].([]any) + foundGraphQL := false + foundEvents := false + for _, tag := range tags { + tm := tag.(map[string]any) + switch tm["name"] { + case "graphql": + foundGraphQL = true + case "events": + foundEvents = true + } + } + if !foundGraphQL { + t.Fatal("expected graphql tag when GraphQL is enabled") + } + if !foundEvents { + t.Fatal("expected events tag when SSE is enabled") + } +} + func TestSpecBuilder_Good_WebSocketEndpoint(t *testing.T) { sb := &api.SpecBuilder{ Title: "Test",