From d225fd3178792d93002d8670f8e93396ec5154df Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 08:16:56 +0000 Subject: [PATCH] feat(api): add openapi info summary support Co-Authored-By: Virgil --- api.go | 1 + cmd/api/cmd_sdk.go | 7 +++++-- cmd/api/cmd_spec.go | 3 +++ cmd/api/cmd_test.go | 42 +++++++++++++++++++++++++++++++++++++ cmd/api/spec_builder.go | 2 ++ openapi.go | 9 +++++++- openapi_test.go | 24 +++++++++++++++++++++ options.go | 14 +++++++++++++ spec_builder_helper.go | 1 + spec_builder_helper_test.go | 4 ++++ 10 files changed, 104 insertions(+), 3 deletions(-) diff --git a/api.go b/api.go index f547f7d..32a98a3 100644 --- a/api.go +++ b/api.go @@ -42,6 +42,7 @@ type Engine struct { sseBroker *SSEBroker swaggerEnabled bool swaggerTitle string + swaggerSummary string swaggerDesc string swaggerVersion string swaggerPath string diff --git a/cmd/api/cmd_sdk.go b/cmd/api/cmd_sdk.go index 1ea0949..ab8a1c7 100644 --- a/cmd/api/cmd_sdk.go +++ b/cmd/api/cmd_sdk.go @@ -29,6 +29,7 @@ func addSDKCommand(parent *cli.Command) { specFile string packageName string title string + summary string description string version string swaggerPath string @@ -58,7 +59,7 @@ func addSDKCommand(parent *cli.Command) { // If no spec file provided, generate one to a temp file. if specFile == "" { - builder, err := sdkSpecBuilder(title, description, version, swaggerPath, graphqlPath, graphqlPlayground, ssePath, wsPath, pprofEnabled, expvarEnabled, termsURL, contactName, contactURL, contactEmail, licenseName, licenseURL, externalDocsDescription, externalDocsURL, servers, securitySchemes) + builder, err := sdkSpecBuilder(title, summary, description, version, swaggerPath, graphqlPath, graphqlPlayground, ssePath, wsPath, pprofEnabled, expvarEnabled, termsURL, contactName, contactURL, contactEmail, licenseName, licenseURL, externalDocsDescription, externalDocsURL, servers, securitySchemes) if err != nil { return err } @@ -111,6 +112,7 @@ func addSDKCommand(parent *cli.Command) { cli.StringFlag(cmd, &specFile, "spec", "s", "", "Path to an existing OpenAPI spec (generates a temporary spec from registered route groups and the built-in tool bridge if not provided)") cli.StringFlag(cmd, &packageName, "package", "p", "lethean", "Package name for generated SDK") cli.StringFlag(cmd, &title, "title", "t", defaultSDKTitle, "API title in generated spec") + cli.StringFlag(cmd, &summary, "summary", "", "", "OpenAPI info summary 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") @@ -134,9 +136,10 @@ func addSDKCommand(parent *cli.Command) { parent.AddCommand(cmd) } -func sdkSpecBuilder(title, description, version, swaggerPath, graphqlPath string, graphqlPlayground bool, ssePath, wsPath string, pprofEnabled, expvarEnabled bool, termsURL, contactName, contactURL, contactEmail, licenseName, licenseURL, externalDocsDescription, externalDocsURL, servers, securitySchemes string) (*goapi.SpecBuilder, error) { +func sdkSpecBuilder(title, summary, description, version, swaggerPath, graphqlPath string, graphqlPlayground bool, ssePath, wsPath string, pprofEnabled, expvarEnabled bool, termsURL, contactName, contactURL, contactEmail, licenseName, licenseURL, externalDocsDescription, externalDocsURL, servers, securitySchemes string) (*goapi.SpecBuilder, error) { return newSpecBuilder(specBuilderConfig{ title: title, + summary: summary, description: description, version: version, swaggerPath: swaggerPath, diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index 617653c..e39d615 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -18,6 +18,7 @@ func addSpecCommand(parent *cli.Command) { output string format string title string + summary string description string version string swaggerPath string @@ -43,6 +44,7 @@ func addSpecCommand(parent *cli.Command) { // Build spec from all route groups registered for CLI generation. builder, err := newSpecBuilder(specBuilderConfig{ title: title, + summary: summary, description: description, version: version, swaggerPath: swaggerPath, @@ -84,6 +86,7 @@ func addSpecCommand(parent *cli.Command) { cli.StringFlag(cmd, &output, "output", "o", "", "Write spec to file instead of stdout") cli.StringFlag(cmd, &format, "format", "f", "json", "Output format: json or yaml") cli.StringFlag(cmd, &title, "title", "t", "Lethean Core API", "API title in spec") + cli.StringFlag(cmd, &summary, "summary", "", "", "OpenAPI info summary 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") diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index 185d394..f03be1b 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -85,6 +85,9 @@ func TestAPISpecCmd_Good_JSON(t *testing.T) { if specCmd.Flag("title") == nil { t.Fatal("expected --title flag on spec command") } + if specCmd.Flag("summary") == nil { + t.Fatal("expected --summary flag on spec command") + } if specCmd.Flag("description") == nil { t.Fatal("expected --description flag on spec command") } @@ -178,6 +181,41 @@ func TestAPISpecCmd_Good_CustomDescription(t *testing.T) { } } +func TestAPISpecCmd_Good_SummaryPopulatesSpecInfo(t *testing.T) { + root := &cli.Command{Use: "root"} + AddAPICommands(root) + + outputFile := t.TempDir() + "/spec.json" + root.SetArgs([]string{ + "api", "spec", + "--summary", "Short API overview", + "--output", outputFile, + }) + root.SetErr(new(bytes.Buffer)) + + if err := root.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("expected spec file to be written: %v", err) + } + + var spec map[string]any + if err := json.Unmarshal(data, &spec); err != nil { + t.Fatalf("expected valid JSON spec, got error: %v", err) + } + + info, ok := spec["info"].(map[string]any) + if !ok { + t.Fatal("expected info object in generated spec") + } + if info["summary"] != "Short API overview" { + t.Fatalf("expected summary to be preserved, got %v", info["summary"]) + } +} + func TestAPISpecCmd_Good_GraphQLPlaygroundFlagPopulatesSpecPaths(t *testing.T) { root := &cli.Command{Use: "root"} AddAPICommands(root) @@ -758,6 +796,7 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) { builder, err := sdkSpecBuilder( "Custom SDK API", + "Custom SDK overview", "Custom SDK description", "9.9.9", "/docs", @@ -808,6 +847,9 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) { if info["description"] != "Custom SDK description" { t.Fatalf("expected custom description, got %v", info["description"]) } + if info["summary"] != "Custom SDK overview" { + t.Fatalf("expected custom summary, got %v", info["summary"]) + } if info["version"] != "9.9.9" { t.Fatalf("expected custom version, got %v", info["version"]) } diff --git a/cmd/api/spec_builder.go b/cmd/api/spec_builder.go index 9e66559..0318894 100644 --- a/cmd/api/spec_builder.go +++ b/cmd/api/spec_builder.go @@ -6,6 +6,7 @@ import goapi "dappco.re/go/core/api" type specBuilderConfig struct { title string + summary string description string version string swaggerPath string @@ -30,6 +31,7 @@ type specBuilderConfig struct { func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { builder := &goapi.SpecBuilder{ Title: cfg.title, + Summary: cfg.summary, Description: cfg.description, Version: cfg.version, SwaggerPath: cfg.swaggerPath, diff --git a/openapi.go b/openapi.go index f1f4660..63a9cb0 100644 --- a/openapi.go +++ b/openapi.go @@ -13,7 +13,7 @@ import ( ) // SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups. -// Title, Description, Version, and optional contact/licence/terms metadata populate the +// Title, Summary, Description, Version, and optional contact/licence/terms metadata populate the // OpenAPI info block. Top-level external documentation metadata is also supported. // // Example: @@ -22,6 +22,7 @@ import ( // spec, err := builder.Build(engine.Groups()) type SpecBuilder struct { Title string + Summary string Description string Version string SwaggerPath string @@ -65,6 +66,7 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { "jsonSchemaDialect": openAPIDialect, "info": map[string]any{ "title": sb.Title, + "summary": sb.Summary, "description": sb.Description, "version": sb.Version, }, @@ -86,6 +88,11 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { } spec["info"].(map[string]any)["license"] = license } + if sb.Summary != "" { + spec["info"].(map[string]any)["summary"] = sb.Summary + } else { + delete(spec["info"].(map[string]any), "summary") + } if swaggerPath := strings.TrimSpace(sb.SwaggerPath); swaggerPath != "" { spec["x-swagger-ui-path"] = normaliseSwaggerPath(swaggerPath) diff --git a/openapi_test.go b/openapi_test.go index 18fd5b7..51e9b6c 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -633,6 +633,30 @@ func TestSpecBuilder_Good_InfoIncludesLicenseMetadata(t *testing.T) { } } +func TestSpecBuilder_Good_InfoIncludesSummary(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Summary: "Concise API overview", + Description: "Summary test API", + Version: "1.2.3", + } + + 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) + } + + info := spec["info"].(map[string]any) + if info["summary"] != "Concise API overview" { + t.Fatalf("expected summary to be preserved, got %v", info["summary"]) + } +} + func TestSpecBuilder_Good_InfoIncludesContactMetadata(t *testing.T) { sb := &api.SpecBuilder{ Title: "Test", diff --git a/options.go b/options.go index ea7f4a6..3b3071f 100644 --- a/options.go +++ b/options.go @@ -195,6 +195,7 @@ func WithSunset(sunsetDate, replacement string) Option { // WithSwagger enables the Swagger UI at /swagger/ by default. // The title, description, and version populate the OpenAPI info block. +// Use WithSwaggerSummary() to set the optional info.summary field. // // Example: // @@ -208,6 +209,19 @@ func WithSwagger(title, description, version string) Option { } } +// WithSwaggerSummary adds the OpenAPI info.summary field to generated specs. +// +// Example: +// +// api.WithSwaggerSummary("Service overview") +func WithSwaggerSummary(summary string) Option { + return func(e *Engine) { + if summary != "" { + e.swaggerSummary = summary + } + } +} + // WithSwaggerPath sets a custom URL path for the Swagger UI. // The default path is "/swagger". // diff --git a/spec_builder_helper.go b/spec_builder_helper.go index 23cd167..600f0f1 100644 --- a/spec_builder_helper.go +++ b/spec_builder_helper.go @@ -21,6 +21,7 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder { transport := e.TransportConfig() builder := &SpecBuilder{ Title: e.swaggerTitle, + Summary: e.swaggerSummary, Description: e.swaggerDesc, Version: e.swaggerVersion, TermsOfService: e.swaggerTermsOfService, diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index f5f5f9a..26d40f0 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.WithSwaggerSummary("Engine overview"), api.WithSwaggerPath("/docs"), api.WithSwaggerTermsOfService("https://example.com/terms"), api.WithSwaggerContact("API Support", "https://example.com/support", "support@example.com"), @@ -67,6 +68,9 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { if info["version"] != "2.0.0" { t.Fatalf("expected version 2.0.0, got %v", info["version"]) } + if info["summary"] != "Engine overview" { + t.Fatalf("expected summary Engine overview, got %v", info["summary"]) + } if got := spec["x-swagger-ui-path"]; got != "/docs" { t.Fatalf("expected x-swagger-ui-path=/docs, got %v", got)