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",