feat(api): add openapi info summary support

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 08:16:56 +00:00
parent be7616d437
commit d225fd3178
10 changed files with 104 additions and 3 deletions

1
api.go
View file

@ -42,6 +42,7 @@ type Engine struct {
sseBroker *SSEBroker
swaggerEnabled bool
swaggerTitle string
swaggerSummary string
swaggerDesc string
swaggerVersion string
swaggerPath string

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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".
//

View file

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

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.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)