diff --git a/cmd/api/cmd_sdk.go b/cmd/api/cmd_sdk.go index 1b834af..383d549 100644 --- a/cmd/api/cmd_sdk.go +++ b/cmd/api/cmd_sdk.go @@ -39,6 +39,12 @@ func addSDKCommand(parent *cli.Command) { wsPath string pprofEnabled bool expvarEnabled bool + cacheEnabled bool + cacheTTL string + cacheMaxEntries int + cacheMaxBytes int + i18nDefaultLocale string + i18nSupportedLocales string termsURL string contactName string contactURL string @@ -84,6 +90,12 @@ func addSDKCommand(parent *cli.Command) { wsPath: wsPath, pprofEnabled: pprofEnabled, expvarEnabled: expvarEnabled, + cacheEnabled: cacheEnabled, + cacheTTL: cacheTTL, + cacheMaxEntries: cacheMaxEntries, + cacheMaxBytes: cacheMaxBytes, + i18nDefaultLocale: i18nDefaultLocale, + i18nSupportedLocales: i18nSupportedLocales, termsURL: termsURL, contactName: contactName, contactURL: contactURL, @@ -146,6 +158,12 @@ func addSDKCommand(parent *cli.Command) { cli.StringFlag(cmd, &wsPath, "ws-path", "", "", "WebSocket endpoint path in generated spec") cli.BoolFlag(cmd, &pprofEnabled, "pprof", "", false, "Include pprof endpoints in generated spec") cli.BoolFlag(cmd, &expvarEnabled, "expvar", "", false, "Include expvar endpoint in generated spec") + cli.BoolFlag(cmd, &cacheEnabled, "cache", "", false, "Include cache metadata in generated spec") + cli.StringFlag(cmd, &cacheTTL, "cache-ttl", "", "", "Cache TTL in generated spec") + cli.IntFlag(cmd, &cacheMaxEntries, "cache-max-entries", "", 0, "Cache max entries in generated spec") + cli.IntFlag(cmd, &cacheMaxBytes, "cache-max-bytes", "", 0, "Cache max bytes in generated spec") + cli.StringFlag(cmd, &i18nDefaultLocale, "i18n-default-locale", "", "", "Default locale in generated spec") + cli.StringFlag(cmd, &i18nSupportedLocales, "i18n-supported-locales", "", "", "Comma-separated supported locales in generated spec") cli.StringFlag(cmd, &termsURL, "terms-of-service", "", "", "OpenAPI terms of service URL in generated spec") cli.StringFlag(cmd, &contactName, "contact-name", "", "", "OpenAPI contact name in generated spec") cli.StringFlag(cmd, &contactURL, "contact-url", "", "", "OpenAPI contact URL in generated spec") @@ -173,6 +191,12 @@ func sdkSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { wsPath: cfg.wsPath, pprofEnabled: cfg.pprofEnabled, expvarEnabled: cfg.expvarEnabled, + cacheEnabled: cfg.cacheEnabled, + cacheTTL: cfg.cacheTTL, + cacheMaxEntries: cfg.cacheMaxEntries, + cacheMaxBytes: cfg.cacheMaxBytes, + i18nDefaultLocale: cfg.i18nDefaultLocale, + i18nSupportedLocales: cfg.i18nSupportedLocales, termsURL: cfg.termsURL, contactName: cfg.contactName, contactURL: cfg.contactURL, diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index e39d615..985408e 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -28,6 +28,12 @@ func addSpecCommand(parent *cli.Command) { wsPath string pprofEnabled bool expvarEnabled bool + cacheEnabled bool + cacheTTL string + cacheMaxEntries int + cacheMaxBytes int + i18nDefaultLocale string + i18nSupportedLocales string termsURL string contactName string contactURL string @@ -54,6 +60,12 @@ func addSpecCommand(parent *cli.Command) { wsPath: wsPath, pprofEnabled: pprofEnabled, expvarEnabled: expvarEnabled, + cacheEnabled: cacheEnabled, + cacheTTL: cacheTTL, + cacheMaxEntries: cacheMaxEntries, + cacheMaxBytes: cacheMaxBytes, + i18nDefaultLocale: i18nDefaultLocale, + i18nSupportedLocales: i18nSupportedLocales, termsURL: termsURL, contactName: contactName, contactURL: contactURL, @@ -96,6 +108,12 @@ func addSpecCommand(parent *cli.Command) { cli.StringFlag(cmd, &wsPath, "ws-path", "", "", "WebSocket endpoint path in generated spec") cli.BoolFlag(cmd, &pprofEnabled, "pprof", "", false, "Include pprof endpoints in generated spec") cli.BoolFlag(cmd, &expvarEnabled, "expvar", "", false, "Include expvar endpoint in generated spec") + cli.BoolFlag(cmd, &cacheEnabled, "cache", "", false, "Include cache metadata in generated spec") + cli.StringFlag(cmd, &cacheTTL, "cache-ttl", "", "", "Cache TTL in generated spec") + cli.IntFlag(cmd, &cacheMaxEntries, "cache-max-entries", "", 0, "Cache max entries in generated spec") + cli.IntFlag(cmd, &cacheMaxBytes, "cache-max-bytes", "", 0, "Cache max bytes in generated spec") + cli.StringFlag(cmd, &i18nDefaultLocale, "i18n-default-locale", "", "", "Default locale in generated spec") + cli.StringFlag(cmd, &i18nSupportedLocales, "i18n-supported-locales", "", "", "Comma-separated supported locales in generated spec") cli.StringFlag(cmd, &termsURL, "terms-of-service", "", "", "OpenAPI terms of service URL in spec") cli.StringFlag(cmd, &contactName, "contact-name", "", "", "OpenAPI contact name in spec") cli.StringFlag(cmd, &contactURL, "contact-url", "", "", "OpenAPI contact URL in spec") diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index 4df3385..8fadd18 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -115,6 +115,24 @@ func TestAPISpecCmd_Good_JSON(t *testing.T) { if specCmd.Flag("expvar") == nil { t.Fatal("expected --expvar flag on spec command") } + if specCmd.Flag("cache") == nil { + t.Fatal("expected --cache flag on spec command") + } + if specCmd.Flag("cache-ttl") == nil { + t.Fatal("expected --cache-ttl flag on spec command") + } + if specCmd.Flag("cache-max-entries") == nil { + t.Fatal("expected --cache-max-entries flag on spec command") + } + if specCmd.Flag("cache-max-bytes") == nil { + t.Fatal("expected --cache-max-bytes flag on spec command") + } + if specCmd.Flag("i18n-default-locale") == nil { + t.Fatal("expected --i18n-default-locale flag on spec command") + } + if specCmd.Flag("i18n-supported-locales") == nil { + t.Fatal("expected --i18n-supported-locales flag on spec command") + } if specCmd.Flag("terms-of-service") == nil { t.Fatal("expected --terms-of-service flag on spec command") } @@ -216,6 +234,61 @@ func TestAPISpecCmd_Good_SummaryPopulatesSpecInfo(t *testing.T) { } } +func TestAPISpecCmd_Good_CacheAndI18nFlagsPopulateSpec(t *testing.T) { + root := &cli.Command{Use: "root"} + AddAPICommands(root) + + outputFile := t.TempDir() + "/spec.json" + root.SetArgs([]string{ + "api", "spec", + "--cache", + "--cache-ttl", "5m0s", + "--cache-max-entries", "42", + "--cache-max-bytes", "8192", + "--i18n-default-locale", "en-GB", + "--i18n-supported-locales", "en-GB,fr,en-GB", + "--output", outputFile, + }) + root.SetErr(new(bytes.Buffer)) + + if err := root.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("expected spec file to be written: %v", err) + } + + var spec map[string]any + if err := json.Unmarshal(data, &spec); err != nil { + t.Fatalf("expected valid JSON spec, got error: %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 TestAPISpecCmd_Good_GraphQLPlaygroundFlagPopulatesSpecPaths(t *testing.T) { root := &cli.Command{Use: "root"} AddAPICommands(root) @@ -758,6 +831,24 @@ func TestAPISDKCmd_Good_ValidatesLanguage(t *testing.T) { if sdkCmd.Flag("expvar") == nil { t.Fatal("expected --expvar flag on sdk command") } + if sdkCmd.Flag("cache") == nil { + t.Fatal("expected --cache flag on sdk command") + } + if sdkCmd.Flag("cache-ttl") == nil { + t.Fatal("expected --cache-ttl flag on sdk command") + } + if sdkCmd.Flag("cache-max-entries") == nil { + t.Fatal("expected --cache-max-entries flag on sdk command") + } + if sdkCmd.Flag("cache-max-bytes") == nil { + t.Fatal("expected --cache-max-bytes flag on sdk command") + } + if sdkCmd.Flag("i18n-default-locale") == nil { + t.Fatal("expected --i18n-default-locale flag on sdk command") + } + if sdkCmd.Flag("i18n-supported-locales") == nil { + t.Fatal("expected --i18n-supported-locales flag on sdk command") + } if sdkCmd.Flag("terms-of-service") == nil { t.Fatal("expected --terms-of-service flag on sdk command") } @@ -795,25 +886,31 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) { api.RegisterSpecGroups(specCmdStubGroup{}) builder, err := sdkSpecBuilder(specBuilderConfig{ - title: "Custom SDK API", - summary: "Custom SDK overview", - description: "Custom SDK description", - version: "9.9.9", - swaggerPath: "/docs", - graphqlPath: "/gql", - graphqlPlayground: true, - ssePath: "/events", - wsPath: "/ws", - pprofEnabled: true, - expvarEnabled: true, - termsURL: "https://example.com/terms", - contactName: "SDK Support", - contactURL: "https://example.com/support", - contactEmail: "support@example.com", - licenseName: "EUPL-1.2", - licenseURL: "https://eupl.eu/1.2/en/", - servers: "https://api.example.com, /, https://api.example.com", - securitySchemes: `{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}`, + title: "Custom SDK API", + summary: "Custom SDK overview", + description: "Custom SDK description", + version: "9.9.9", + swaggerPath: "/docs", + graphqlPath: "/gql", + graphqlPlayground: true, + ssePath: "/events", + wsPath: "/ws", + pprofEnabled: true, + expvarEnabled: true, + cacheEnabled: true, + cacheTTL: "5m0s", + cacheMaxEntries: 42, + cacheMaxBytes: 8192, + i18nDefaultLocale: "en-GB", + i18nSupportedLocales: "en-GB,fr,en-GB", + termsURL: "https://example.com/terms", + contactName: "SDK Support", + contactURL: "https://example.com/support", + contactEmail: "support@example.com", + licenseName: "EUPL-1.2", + licenseURL: "https://eupl.eu/1.2/en/", + servers: "https://api.example.com, /, https://api.example.com", + securitySchemes: `{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}`, }) if err != nil { t.Fatalf("unexpected error building sdk spec: %v", err) @@ -878,6 +975,29 @@ func TestAPISDKCmd_Good_TempSpecUsesMetadataFlags(t *testing.T) { t.Fatal("expected expvar path to be included in generated spec") } + 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) + } + if info["termsOfService"] != "https://example.com/terms" { t.Fatalf("expected termsOfService to be preserved, got %v", info["termsOfService"]) } diff --git a/cmd/api/spec_builder.go b/cmd/api/spec_builder.go index 0318894..6134543 100644 --- a/cmd/api/spec_builder.go +++ b/cmd/api/spec_builder.go @@ -2,7 +2,11 @@ package api -import goapi "dappco.re/go/core/api" +import ( + "strings" + + goapi "dappco.re/go/core/api" +) type specBuilderConfig struct { title string @@ -16,6 +20,12 @@ type specBuilderConfig struct { wsPath string pprofEnabled bool expvarEnabled bool + cacheEnabled bool + cacheTTL string + cacheMaxEntries int + cacheMaxBytes int + i18nDefaultLocale string + i18nSupportedLocales string termsURL string contactName string contactURL string @@ -41,6 +51,11 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { WSPath: cfg.wsPath, PprofEnabled: cfg.pprofEnabled, ExpvarEnabled: cfg.expvarEnabled, + CacheEnabled: cfg.cacheEnabled || strings.TrimSpace(cfg.cacheTTL) != "" || cfg.cacheMaxEntries > 0 || cfg.cacheMaxBytes > 0, + CacheTTL: strings.TrimSpace(cfg.cacheTTL), + CacheMaxEntries: cfg.cacheMaxEntries, + CacheMaxBytes: cfg.cacheMaxBytes, + I18nDefaultLocale: strings.TrimSpace(cfg.i18nDefaultLocale), TermsOfService: cfg.termsURL, ContactName: cfg.contactName, ContactURL: cfg.contactURL, @@ -52,6 +67,11 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { ExternalDocsURL: cfg.externalDocsURL, } + builder.I18nSupportedLocales = parseLocales(cfg.i18nSupportedLocales) + if builder.I18nDefaultLocale == "" && len(builder.I18nSupportedLocales) > 0 { + builder.I18nDefaultLocale = "en" + } + if cfg.securitySchemes != "" { schemes, err := parseSecuritySchemes(cfg.securitySchemes) if err != nil { @@ -62,3 +82,7 @@ func newSpecBuilder(cfg specBuilderConfig) (*goapi.SpecBuilder, error) { return builder, nil } + +func parseLocales(raw string) []string { + return splitUniqueCSV(raw) +}