feat(api): expose swagger security schemes

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 07:06:09 +00:00
parent 8e1a424fc8
commit fb7702df67
4 changed files with 71 additions and 0 deletions

1
api.go
View file

@ -52,6 +52,7 @@ type Engine struct {
swaggerContactEmail string
swaggerLicenseName string
swaggerLicenseURL string
swaggerSecuritySchemes map[string]any
swaggerExternalDocsDescription string
swaggerExternalDocsURL string
pprofEnabled bool

View file

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

View file

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

View file

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