api/spec_builder_helper.go
Snider d90a5be936 refactor: AX compliance sweep — replace banned stdlib imports with core primitives
Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.

Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-13 09:32:00 +01:00

284 lines
8 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api
import (
"reflect"
"slices"
core "dappco.re/go/core"
)
// SwaggerConfig captures the configured Swagger/OpenAPI metadata for an Engine.
//
// It is intentionally small and serialisable so callers can inspect the active
// documentation surface without rebuilding an OpenAPI document.
//
// Example:
//
// cfg := api.SwaggerConfig{Title: "Service", Summary: "Public API"}
type SwaggerConfig struct {
Enabled bool
Path string
Title string
Summary string
Description string
Version string
TermsOfService string
ContactName string
ContactURL string
ContactEmail string
Servers []string
LicenseName string
LicenseURL string
SecuritySchemes map[string]any
ExternalDocsDescription string
ExternalDocsURL string
}
// OpenAPISpecBuilder returns a SpecBuilder populated from the engine's current
// Swagger, transport, cache, i18n, and Authentik metadata.
//
// Example:
//
// builder := engine.OpenAPISpecBuilder()
func (e *Engine) OpenAPISpecBuilder() *SpecBuilder {
if e == nil {
return &SpecBuilder{}
}
runtime := e.RuntimeConfig()
builder := &SpecBuilder{
Title: runtime.Swagger.Title,
Summary: runtime.Swagger.Summary,
Description: runtime.Swagger.Description,
Version: runtime.Swagger.Version,
SwaggerEnabled: runtime.Swagger.Enabled,
TermsOfService: runtime.Swagger.TermsOfService,
ContactName: runtime.Swagger.ContactName,
ContactURL: runtime.Swagger.ContactURL,
ContactEmail: runtime.Swagger.ContactEmail,
Servers: slices.Clone(runtime.Swagger.Servers),
LicenseName: runtime.Swagger.LicenseName,
LicenseURL: runtime.Swagger.LicenseURL,
SecuritySchemes: cloneSecuritySchemes(runtime.Swagger.SecuritySchemes),
ExternalDocsDescription: runtime.Swagger.ExternalDocsDescription,
ExternalDocsURL: runtime.Swagger.ExternalDocsURL,
}
builder.SwaggerPath = runtime.Transport.SwaggerPath
builder.GraphQLEnabled = runtime.GraphQL.Enabled
builder.GraphQLPath = runtime.GraphQL.Path
builder.GraphQLPlayground = runtime.GraphQL.Playground
builder.GraphQLPlaygroundPath = runtime.GraphQL.PlaygroundPath
builder.WSPath = runtime.Transport.WSPath
builder.WSEnabled = runtime.Transport.WSEnabled
builder.SSEPath = runtime.Transport.SSEPath
builder.SSEEnabled = runtime.Transport.SSEEnabled
builder.PprofEnabled = runtime.Transport.PprofEnabled
builder.ExpvarEnabled = runtime.Transport.ExpvarEnabled
builder.CacheEnabled = runtime.Cache.Enabled
if runtime.Cache.TTL > 0 {
builder.CacheTTL = runtime.Cache.TTL.String()
}
builder.CacheMaxEntries = runtime.Cache.MaxEntries
builder.CacheMaxBytes = runtime.Cache.MaxBytes
builder.I18nDefaultLocale = runtime.I18n.DefaultLocale
builder.I18nSupportedLocales = slices.Clone(runtime.I18n.Supported)
builder.AuthentikIssuer = runtime.Authentik.Issuer
builder.AuthentikClientID = runtime.Authentik.ClientID
builder.AuthentikTrustedProxy = runtime.Authentik.TrustedProxy
builder.AuthentikPublicPaths = slices.Clone(runtime.Authentik.PublicPaths)
return builder
}
// SwaggerConfig returns the currently configured Swagger metadata for the engine.
//
// The result snapshots the Engine state at call time and clones slices/maps so
// callers can safely reuse or modify the returned value.
//
// Example:
//
// cfg := engine.SwaggerConfig()
func (e *Engine) SwaggerConfig() SwaggerConfig {
if e == nil {
return SwaggerConfig{}
}
cfg := SwaggerConfig{
Enabled: e.swaggerEnabled,
Title: e.swaggerTitle,
Summary: e.swaggerSummary,
Description: e.swaggerDesc,
Version: e.swaggerVersion,
TermsOfService: e.swaggerTermsOfService,
ContactName: e.swaggerContactName,
ContactURL: e.swaggerContactURL,
ContactEmail: e.swaggerContactEmail,
Servers: slices.Clone(e.swaggerServers),
LicenseName: e.swaggerLicenseName,
LicenseURL: e.swaggerLicenseURL,
SecuritySchemes: cloneSecuritySchemes(e.swaggerSecuritySchemes),
ExternalDocsDescription: e.swaggerExternalDocsDescription,
ExternalDocsURL: e.swaggerExternalDocsURL,
}
if core.Trim(e.swaggerPath) != "" {
cfg.Path = normaliseSwaggerPath(e.swaggerPath)
}
return cfg
}
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] = cloneOpenAPIValue(scheme)
}
return out
}
func cloneRouteDescription(rd RouteDescription) RouteDescription {
out := rd
out.Tags = slices.Clone(rd.Tags)
out.Security = cloneSecurityRequirements(rd.Security)
out.Parameters = cloneParameterDescriptions(rd.Parameters)
out.RequestBody = cloneOpenAPIObject(rd.RequestBody)
out.RequestExample = cloneOpenAPIValue(rd.RequestExample)
out.Response = cloneOpenAPIObject(rd.Response)
out.ResponseExample = cloneOpenAPIValue(rd.ResponseExample)
out.ResponseHeaders = cloneStringMap(rd.ResponseHeaders)
return out
}
func cloneParameterDescriptions(params []ParameterDescription) []ParameterDescription {
if params == nil {
return nil
}
if len(params) == 0 {
return []ParameterDescription{}
}
out := make([]ParameterDescription, len(params))
for i, param := range params {
out[i] = param
out[i].Schema = cloneOpenAPIObject(param.Schema)
out[i].Example = cloneOpenAPIValue(param.Example)
}
return out
}
func cloneSecurityRequirements(security []map[string][]string) []map[string][]string {
if security == nil {
return nil
}
if len(security) == 0 {
return []map[string][]string{}
}
out := make([]map[string][]string, len(security))
for i, requirement := range security {
if len(requirement) == 0 {
continue
}
cloned := make(map[string][]string, len(requirement))
for name, scopes := range requirement {
cloned[name] = slices.Clone(scopes)
}
out[i] = cloned
}
return out
}
func cloneOpenAPIObject(v map[string]any) map[string]any {
if v == nil {
return nil
}
if len(v) == 0 {
return map[string]any{}
}
cloned, _ := cloneOpenAPIValue(v).(map[string]any)
return cloned
}
func cloneStringMap(v map[string]string) map[string]string {
if v == nil {
return nil
}
if len(v) == 0 {
return map[string]string{}
}
out := make(map[string]string, len(v))
for key, value := range v {
out[key] = value
}
return out
}
// cloneOpenAPIValue recursively copies JSON-like OpenAPI values so callers can
// safely retain and reuse their original maps after configuring an engine.
func cloneOpenAPIValue(v any) any {
switch value := v.(type) {
case map[string]any:
out := make(map[string]any, len(value))
for k, nested := range value {
out[k] = cloneOpenAPIValue(nested)
}
return out
case []any:
out := make([]any, len(value))
for i, nested := range value {
out[i] = cloneOpenAPIValue(nested)
}
return out
default:
rv := reflect.ValueOf(v)
if !rv.IsValid() {
return nil
}
switch rv.Kind() {
case reflect.Map:
out := reflect.MakeMapWithSize(rv.Type(), rv.Len())
for _, key := range rv.MapKeys() {
cloned := cloneOpenAPIValue(rv.MapIndex(key).Interface())
if cloned == nil {
out.SetMapIndex(key, reflect.Zero(rv.Type().Elem()))
continue
}
out.SetMapIndex(key, reflect.ValueOf(cloned))
}
return out.Interface()
case reflect.Slice:
if rv.IsNil() {
return v
}
out := reflect.MakeSlice(rv.Type(), rv.Len(), rv.Len())
for i := 0; i < rv.Len(); i++ {
cloned := cloneOpenAPIValue(rv.Index(i).Interface())
if cloned == nil {
out.Index(i).Set(reflect.Zero(rv.Type().Elem()))
continue
}
out.Index(i).Set(reflect.ValueOf(cloned))
}
return out.Interface()
default:
return value
}
}
}