feat(api): expose cache and i18n OpenAPI metadata

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 12:29:40 +00:00
parent 5de64a0a75
commit f919e8a3be
4 changed files with 118 additions and 3 deletions

View file

@ -10,11 +10,14 @@ import (
"strconv"
"strings"
"unicode"
"slices"
)
// SpecBuilder constructs an OpenAPI 3.1 specification from registered RouteGroups.
// Title, Summary, Description, Version, and optional contact/licence/terms metadata populate the
// OpenAPI info block. Top-level external documentation metadata is also supported.
// OpenAPI info block. Top-level external documentation metadata is also supported, along with
// additive extension fields that describe runtime transport, cache, and i18n settings.
//
// Example:
//
@ -46,6 +49,12 @@ type SpecBuilder struct {
ExternalDocsURL string
PprofEnabled bool
ExpvarEnabled bool
CacheEnabled bool
CacheTTL string
CacheMaxEntries int
CacheMaxBytes int
I18nDefaultLocale string
I18nSupportedLocales []string
}
type preparedRouteGroup struct {
@ -129,6 +138,24 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) {
if sb.ExpvarEnabled {
spec["x-expvar-enabled"] = true
}
if sb.CacheEnabled {
spec["x-cache-enabled"] = true
}
if ttl := strings.TrimSpace(sb.CacheTTL); ttl != "" {
spec["x-cache-ttl"] = ttl
}
if sb.CacheMaxEntries > 0 {
spec["x-cache-max-entries"] = sb.CacheMaxEntries
}
if sb.CacheMaxBytes > 0 {
spec["x-cache-max-bytes"] = sb.CacheMaxBytes
}
if locale := strings.TrimSpace(sb.I18nDefaultLocale); locale != "" {
spec["x-i18n-default-locale"] = locale
}
if len(sb.I18nSupportedLocales) > 0 {
spec["x-i18n-supported-locales"] = slices.Clone(sb.I18nSupportedLocales)
}
if sb.TermsOfService != "" {
spec["info"].(map[string]any)["termsOfService"] = sb.TermsOfService

View file

@ -7,6 +7,7 @@ import (
"iter"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
@ -364,6 +365,54 @@ func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) {
}
}
func TestSpecBuilder_Good_CacheAndI18nExtensions(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",
Description: "Runtime config test",
Version: "1.0.0",
CacheEnabled: true,
CacheTTL: (5 * time.Minute).String(),
CacheMaxEntries: 42,
CacheMaxBytes: 8192,
I18nDefaultLocale: "en-GB",
I18nSupportedLocales: []string{"en-GB", "fr"},
}
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)
}
if got := spec["x-cache-enabled"]; got != true {
t.Fatalf("expected x-cache-enabled=true, got %v", got)
}
if got := spec["x-cache-ttl"]; got != "5m0s" {
t.Fatalf("expected x-cache-ttl=5m0s, got %v", got)
}
if got := spec["x-cache-max-entries"]; got != float64(42) {
t.Fatalf("expected x-cache-max-entries=42, got %v", got)
}
if got := spec["x-cache-max-bytes"]; got != float64(8192) {
t.Fatalf("expected x-cache-max-bytes=8192, got %v", got)
}
if got := spec["x-i18n-default-locale"]; got != "en-GB" {
t.Fatalf("expected x-i18n-default-locale=en-GB, got %v", got)
}
locales, ok := spec["x-i18n-supported-locales"].([]any)
if !ok {
t.Fatalf("expected x-i18n-supported-locales array, got %T", spec["x-i18n-supported-locales"])
}
if len(locales) != 2 || locales[0] != "en-GB" || locales[1] != "fr" {
t.Fatalf("expected supported locales [en-GB fr], got %v", locales)
}
}
func TestSpecBuilder_Good_GraphQLEndpoint(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",

View file

@ -35,7 +35,7 @@ type SwaggerConfig struct {
}
// OpenAPISpecBuilder returns a SpecBuilder populated from the engine's current
// Swagger and transport metadata.
// Swagger, transport, cache, and i18n metadata.
//
// Example:
//
@ -76,6 +76,18 @@ func (e *Engine) OpenAPISpecBuilder() *SpecBuilder {
builder.PprofEnabled = transport.PprofEnabled
builder.ExpvarEnabled = transport.ExpvarEnabled
cache := e.CacheConfig()
builder.CacheEnabled = cache.Enabled
if cache.TTL > 0 {
builder.CacheTTL = cache.TTL.String()
}
builder.CacheMaxEntries = cache.MaxEntries
builder.CacheMaxBytes = cache.MaxBytes
i18n := e.I18nConfig()
builder.I18nDefaultLocale = i18n.DefaultLocale
builder.I18nSupportedLocales = slices.Clone(i18n.Supported)
return builder
}

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"net/http"
"testing"
"time"
"github.com/gin-gonic/gin"
@ -32,6 +33,11 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
},
}),
api.WithSwaggerExternalDocs("Developer guide", "https://example.com/docs"),
api.WithCacheLimits(5*time.Minute, 42, 8192),
api.WithI18n(api.I18nConfig{
DefaultLocale: "en-GB",
Supported: []string{"en-GB", "fr"},
}),
api.WithWSPath("/socket"),
api.WithWSHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})),
api.WithGraphQL(newTestSchema(), api.WithPlayground(), api.WithGraphQLPath("/gql")),
@ -105,6 +111,28 @@ func TestEngine_Good_OpenAPISpecBuilderCarriesEngineMetadata(t *testing.T) {
if got := spec["x-expvar-enabled"]; got != true {
t.Fatalf("expected x-expvar-enabled=true, got %v", got)
}
if got := spec["x-cache-enabled"]; got != true {
t.Fatalf("expected x-cache-enabled=true, got %v", got)
}
if got := spec["x-cache-ttl"]; got != "5m0s" {
t.Fatalf("expected x-cache-ttl=5m0s, got %v", got)
}
if got := spec["x-cache-max-entries"]; got != float64(42) {
t.Fatalf("expected x-cache-max-entries=42, got %v", got)
}
if got := spec["x-cache-max-bytes"]; got != float64(8192) {
t.Fatalf("expected x-cache-max-bytes=8192, got %v", got)
}
if got := spec["x-i18n-default-locale"]; got != "en-GB" {
t.Fatalf("expected x-i18n-default-locale=en-GB, got %v", got)
}
locales, ok := spec["x-i18n-supported-locales"].([]any)
if !ok {
t.Fatalf("expected x-i18n-supported-locales array, got %T", spec["x-i18n-supported-locales"])
}
if len(locales) != 2 || locales[0] != "en-GB" || locales[1] != "fr" {
t.Fatalf("expected supported locales [en-GB fr], got %v", locales)
}
contact, ok := info["contact"].(map[string]any)
if !ok {
@ -244,7 +272,6 @@ func TestEngine_Good_SwaggerConfigCarriesEngineMetadata(t *testing.T) {
if cfg.ExternalDocsURL != "https://example.com/docs" {
t.Fatalf("expected external docs URL https://example.com/docs, got %q", cfg.ExternalDocsURL)
}
if len(cfg.Servers) != 2 {
t.Fatalf("expected 2 normalised servers, got %d", len(cfg.Servers))
}