diff --git a/openapi.go b/openapi.go index c283cc9..27c0db8 100644 --- a/openapi.go +++ b/openapi.go @@ -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 diff --git a/openapi_test.go b/openapi_test.go index da1bb76..824379c 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -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", diff --git a/spec_builder_helper.go b/spec_builder_helper.go index 0d541ad..e8524b3 100644 --- a/spec_builder_helper.go +++ b/spec_builder_helper.go @@ -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 } diff --git a/spec_builder_helper_test.go b/spec_builder_helper_test.go index dc95486..3af37ab 100644 --- a/spec_builder_helper_test.go +++ b/spec_builder_helper_test.go @@ -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)) }