From d9ccd7c49a81943a11b4c209d41b8f734a6bb0c3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 03:16:08 +0000 Subject: [PATCH] feat(openapi): export swagger ui path metadata Preserve the Swagger UI mount path in generated OpenAPI output and expose it through the spec and sdk CLI builders.\n\nCo-Authored-By: Virgil --- cmd/api/cmd_sdk.go | 3 +++ cmd/api/cmd_spec.go | 3 +++ cmd/api/cmd_test.go | 12 +++++++++++- openapi.go | 5 +++++ openapi_test.go | 23 +++++++++++++++++++++++ spec_builder_helper.go | 1 + spec_builder_helper_test.go | 5 +++++ 7 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cmd/api/cmd_sdk.go b/cmd/api/cmd_sdk.go index 7c814fd..7c8568e 100644 --- a/cmd/api/cmd_sdk.go +++ b/cmd/api/cmd_sdk.go @@ -31,6 +31,7 @@ func addSDKCommand(parent *cli.Command) { title string description string version string + swaggerPath string graphqlPath string graphqlPlayground bool ssePath string @@ -57,6 +58,7 @@ func addSDKCommand(parent *cli.Command) { // If no spec file provided, generate one to a temp file. if specFile == "" { builder := sdkSpecBuilder(title, description, version, graphqlPath, graphqlPlayground, ssePath, wsPath, pprofEnabled, expvarEnabled, termsURL, contactName, contactURL, contactEmail, licenseName, licenseURL, externalDocsDescription, externalDocsURL, servers) + builder.SwaggerPath = swaggerPath groups := sdkSpecGroupsIter() tmpFile, err := os.CreateTemp("", "openapi-*.json") @@ -105,6 +107,7 @@ func addSDKCommand(parent *cli.Command) { cli.StringFlag(cmd, &title, "title", "t", defaultSDKTitle, "API title in generated spec") cli.StringFlag(cmd, &description, "description", "d", defaultSDKDescription, "API description in generated spec") cli.StringFlag(cmd, &version, "version", "V", defaultSDKVersion, "API version in generated spec") + cli.StringFlag(cmd, &swaggerPath, "swagger-path", "", "", "Swagger UI path in generated spec") cli.StringFlag(cmd, &graphqlPath, "graphql-path", "", "", "GraphQL endpoint path in generated spec") cli.BoolFlag(cmd, &graphqlPlayground, "graphql-playground", "", false, "Include the GraphQL playground endpoint in generated spec") cli.StringFlag(cmd, &ssePath, "sse-path", "", "", "SSE endpoint path in generated spec") diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index ffb443f..88ca19a 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -18,6 +18,7 @@ func addSpecCommand(parent *cli.Command) { title string description string version string + swaggerPath string graphqlPath string graphqlPlayground bool ssePath string @@ -41,6 +42,7 @@ func addSpecCommand(parent *cli.Command) { Title: title, Description: description, Version: version, + SwaggerPath: swaggerPath, GraphQLPath: graphqlPath, GraphQLPlayground: graphqlPlayground, SSEPath: ssePath, @@ -77,6 +79,7 @@ func addSpecCommand(parent *cli.Command) { cli.StringFlag(cmd, &title, "title", "t", "Lethean Core API", "API title in spec") cli.StringFlag(cmd, &description, "description", "d", "Lethean Core API", "API description in spec") cli.StringFlag(cmd, &version, "version", "V", "1.0.0", "API version in spec") + cli.StringFlag(cmd, &swaggerPath, "swagger-path", "", "", "Swagger UI path in generated spec") cli.StringFlag(cmd, &graphqlPath, "graphql-path", "", "", "GraphQL endpoint path in generated spec") cli.BoolFlag(cmd, &graphqlPlayground, "graphql-playground", "", false, "Include the GraphQL playground endpoint in generated spec") cli.StringFlag(cmd, &ssePath, "sse-path", "", "", "SSE endpoint path in generated spec") diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index cc81a5a..dd8c659 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -82,6 +82,9 @@ func TestAPISpecCmd_Good_JSON(t *testing.T) { if specCmd.Flag("version") == nil { t.Fatal("expected --version flag on spec command") } + if specCmd.Flag("swagger-path") == nil { + t.Fatal("expected --swagger-path flag on spec command") + } if specCmd.Flag("graphql-path") == nil { t.Fatal("expected --graphql-path flag on spec command") } @@ -134,7 +137,7 @@ func TestAPISpecCmd_Good_CustomDescription(t *testing.T) { AddAPICommands(root) outputFile := t.TempDir() + "/spec.json" - root.SetArgs([]string{"api", "spec", "--description", "Custom API description", "--output", outputFile}) + root.SetArgs([]string{"api", "spec", "--description", "Custom API description", "--swagger-path", "/docs", "--output", outputFile}) root.SetErr(new(bytes.Buffer)) if err := root.Execute(); err != nil { @@ -150,6 +153,10 @@ func TestAPISpecCmd_Good_CustomDescription(t *testing.T) { t.Fatalf("expected valid JSON spec, got error: %v", err) } + if got := spec["x-swagger-ui-path"]; got != "/docs" { + t.Fatalf("expected x-swagger-ui-path=/docs, got %v", got) + } + info, ok := spec["info"].(map[string]any) if !ok { t.Fatal("expected info object in generated spec") @@ -638,6 +645,9 @@ func TestAPISDKCmd_Good_ValidatesLanguage(t *testing.T) { if sdkCmd.Flag("version") == nil { t.Fatal("expected --version flag on sdk command") } + if sdkCmd.Flag("swagger-path") == nil { + t.Fatal("expected --swagger-path flag on sdk command") + } if sdkCmd.Flag("graphql-path") == nil { t.Fatal("expected --graphql-path flag on sdk command") } diff --git a/openapi.go b/openapi.go index 453ecf4..a08b8fb 100644 --- a/openapi.go +++ b/openapi.go @@ -24,6 +24,7 @@ type SpecBuilder struct { Title string Description string Version string + SwaggerPath string GraphQLPath string GraphQLPlayground bool WSPath string @@ -85,6 +86,10 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { spec["info"].(map[string]any)["license"] = license } + if swaggerPath := strings.TrimSpace(sb.SwaggerPath); swaggerPath != "" { + spec["x-swagger-ui-path"] = normaliseSwaggerPath(swaggerPath) + } + if sb.TermsOfService != "" { spec["info"].(map[string]any)["termsOfService"] = sb.TermsOfService } diff --git a/openapi_test.go b/openapi_test.go index 601fba6..3592ae0 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -208,6 +208,29 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) { } } +func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Description: "Swagger path test", + Version: "1.0.0", + SwaggerPath: "/docs/", + } + + 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 != "/docs" { + t.Fatalf("expected x-swagger-ui-path=/docs, got %v", got) + } +} + func TestSpecBuilder_Good_GraphQLEndpoint(t *testing.T) { sb := &api.SpecBuilder{ Title: "Test", diff --git a/spec_builder_helper.go b/spec_builder_helper.go index b165acd..1bf0f38 100644 --- a/spec_builder_helper.go +++ b/spec_builder_helper.go @@ -19,6 +19,7 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder { Title: e.swaggerTitle, Description: e.swaggerDesc, Version: e.swaggerVersion, + SwaggerPath: e.swaggerPath, TermsOfService: e.swaggerTermsOfService, ContactName: e.swaggerContactName, ContactURL: e.swaggerContactURL, diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index 4f339d2..56a829b 100644 --- a/spec_builder_helper_test.go +++ b/spec_builder_helper_test.go @@ -18,6 +18,7 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { broker := api.NewSSEBroker() e, err := api.New( api.WithSwagger("Engine API", "Engine metadata", "2.0.0"), + api.WithSwaggerPath("/docs"), api.WithSwaggerTermsOfService("https://example.com/terms"), api.WithSwaggerContact("API Support", "https://example.com/support", "support@example.com"), api.WithSwaggerServers("https://api.example.com", "/", "https://api.example.com"), @@ -59,6 +60,10 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { t.Fatalf("expected version 2.0.0, got %v", info["version"]) } + if got := spec["x-swagger-ui-path"]; got != "/docs" { + t.Fatalf("expected x-swagger-ui-path=/docs, got %v", got) + } + contact, ok := info["contact"].(map[string]any) if !ok { t.Fatal("expected contact metadata in generated spec")