feat(api): merge custom OpenAPI security schemes

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 07:01:41 +00:00
parent 920c227e21
commit 8e1a424fc8
2 changed files with 75 additions and 8 deletions

View file

@ -36,6 +36,7 @@ type SpecBuilder struct {
Servers []string
LicenseName string
LicenseURL string
SecuritySchemes map[string]any
ExternalDocsDescription string
ExternalDocsURL string
PprofEnabled bool
@ -179,14 +180,8 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
},
},
},
"securitySchemes": map[string]any{
"bearerAuth": map[string]any{
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
},
},
"headers": deprecationHeaderComponents(),
"securitySchemes": securitySchemeComponents(sb.SecuritySchemes),
"headers": deprecationHeaderComponents(),
}
return json.MarshalIndent(spec, "", " ")
@ -636,6 +631,29 @@ func deprecationHeaderComponents() map[string]any {
}
}
// securitySchemeComponents builds the OpenAPI security scheme registry.
// bearerAuth stays available by default, while callers can add or override
// additional scheme definitions for custom security requirements.
func securitySchemeComponents(overrides map[string]any) map[string]any {
schemes := map[string]any{
"bearerAuth": map[string]any{
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
},
}
for name, scheme := range overrides {
name = strings.TrimSpace(name)
if name == "" || scheme == nil {
continue
}
schemes[name] = scheme
}
return schemes
}
// buildTags generates the tags array from all RouteGroups.
func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any {
tags := []map[string]any{

View file

@ -208,6 +208,55 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) {
}
}
func TestSpecBuilder_Good_CustomSecuritySchemesAreMerged(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",
Version: "1.0.0",
SecuritySchemes: map[string]any{
"apiKeyAuth": map[string]any{
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
},
},
}
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)
}
components := spec["components"].(map[string]any)
schemes := components["securitySchemes"].(map[string]any)
bearerAuth, ok := schemes["bearerAuth"].(map[string]any)
if !ok {
t.Fatal("expected default bearerAuth security scheme to remain present")
}
if bearerAuth["scheme"] != "bearer" {
t.Fatalf("expected bearerAuth scheme to stay bearer, got %v", bearerAuth["scheme"])
}
apiKeyAuth, ok := schemes["apiKeyAuth"].(map[string]any)
if !ok {
t.Fatal("expected custom apiKeyAuth security scheme to be merged")
}
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"])
}
}
func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",