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 <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 03:16:08 +00:00
parent c3143a5029
commit d9ccd7c49a
7 changed files with 51 additions and 1 deletions

View file

@ -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")

View file

@ -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")

View file

@ -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")
}

View file

@ -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
}

View file

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

View file

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

View file

@ -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")