diff --git a/api.go b/api.go index ebac90f..fc5963d 100644 --- a/api.go +++ b/api.go @@ -52,6 +52,7 @@ type Engine struct { swaggerContactEmail string swaggerLicenseName string swaggerLicenseURL string + swaggerSecuritySchemes map[string]any swaggerExternalDocsDescription string swaggerExternalDocsURL string pprofEnabled bool diff --git a/options.go b/options.go index bc2072a..3b99ee2 100644 --- a/options.go +++ b/options.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "slices" + "strings" "time" "github.com/99designs/gqlgen/graphql" @@ -239,6 +240,37 @@ func WithSwaggerLicense(name, url string) Option { } } +// WithSwaggerSecuritySchemes merges custom OpenAPI security schemes into the +// generated Swagger spec. Existing schemes are preserved unless the new map +// defines the same key, in which case the later definition wins. +// +// Example: +// +// api.WithSwaggerSecuritySchemes(map[string]any{ +// "apiKeyAuth": map[string]any{ +// "type": "apiKey", +// "in": "header", +// "name": "X-API-Key", +// }, +// }) +func WithSwaggerSecuritySchemes(schemes map[string]any) Option { + return func(e *Engine) { + if len(schemes) == 0 { + return + } + if e.swaggerSecuritySchemes == nil { + e.swaggerSecuritySchemes = make(map[string]any, len(schemes)) + } + for name, scheme := range schemes { + name = strings.TrimSpace(name) + if name == "" || scheme == nil { + continue + } + e.swaggerSecuritySchemes[name] = scheme + } + } +} + // WithSwaggerExternalDocs adds top-level external documentation metadata to // the generated Swagger spec. // Empty URLs are ignored; the description is optional. diff --git a/spec_builder_helper.go b/spec_builder_helper.go index 5b3ee4f..463b768 100644 --- a/spec_builder_helper.go +++ b/spec_builder_helper.go @@ -29,6 +29,7 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder { Servers: slices.Clone(e.swaggerServers), LicenseName: e.swaggerLicenseName, LicenseURL: e.swaggerLicenseURL, + SecuritySchemes: cloneSecuritySchemes(e.swaggerSecuritySchemes), ExternalDocsDescription: e.swaggerExternalDocsDescription, ExternalDocsURL: e.swaggerExternalDocsURL, } @@ -43,3 +44,15 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder { return builder } + +func cloneSecuritySchemes(schemes map[string]any) map[string]any { + if len(schemes) == 0 { + return nil + } + + out := make(map[string]any, len(schemes)) + for name, scheme := range schemes { + out[name] = scheme + } + return out +} diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index a2990be..e549889 100644 --- a/spec_builder_helper_test.go +++ b/spec_builder_helper_test.go @@ -23,6 +23,13 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { api.WithSwaggerContact("API Support", "https://example.com/support", "support@example.com"), api.WithSwaggerServers("https://api.example.com", "/", "https://api.example.com"), api.WithSwaggerLicense("EUPL-1.2", "https://eupl.eu/1.2/en/"), + api.WithSwaggerSecuritySchemes(map[string]any{ + "apiKeyAuth": map[string]any{ + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + }, + }), api.WithSwaggerExternalDocs("Developer guide", "https://example.com/docs"), api.WithWSPath("/socket"), api.WithWSHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})), @@ -103,6 +110,24 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) { t.Fatalf("expected termsOfService to be preserved, got %v", info["termsOfService"]) } + securitySchemes, ok := spec["components"].(map[string]any)["securitySchemes"].(map[string]any) + if !ok { + t.Fatal("expected securitySchemes metadata in generated spec") + } + apiKeyAuth, ok := securitySchemes["apiKeyAuth"].(map[string]any) + if !ok { + t.Fatal("expected apiKeyAuth security scheme in generated spec") + } + if apiKeyAuth["type"] != "apiKey" { + t.Fatalf("expected apiKeyAuth.type=apiKey, got %v", apiKeyAuth["type"]) + } + if apiKeyAuth["in"] != "header" { + t.Fatalf("expected apiKeyAuth.in=header, got %v", apiKeyAuth["in"]) + } + if apiKeyAuth["name"] != "X-API-Key" { + t.Fatalf("expected apiKeyAuth.name=X-API-Key, got %v", apiKeyAuth["name"]) + } + externalDocs, ok := spec["externalDocs"].(map[string]any) if !ok { t.Fatal("expected externalDocs metadata in generated spec")