diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index 88ca19a..5c657c0 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -3,8 +3,10 @@ package api import ( + "encoding/json" "fmt" "os" + "strings" "forge.lthn.ai/core/cli/pkg/cli" @@ -34,6 +36,7 @@ func addSpecCommand(parent *cli.Command) { externalDocsDescription string externalDocsURL string servers string + securitySchemes string ) cmd := cli.NewCommand("spec", "Generate OpenAPI specification", "", func(cmd *cli.Command, args []string) error { @@ -60,6 +63,14 @@ func addSpecCommand(parent *cli.Command) { ExternalDocsURL: externalDocsURL, } + if securitySchemes != "" { + schemes, err := parseSecuritySchemes(securitySchemes) + if err != nil { + return err + } + builder.SecuritySchemes = schemes + } + bridge := goapi.NewToolBridge("/tools") groups := specGroupsIter(bridge) @@ -95,6 +106,7 @@ func addSpecCommand(parent *cli.Command) { cli.StringFlag(cmd, &externalDocsDescription, "external-docs-description", "", "", "OpenAPI external documentation description in spec") cli.StringFlag(cmd, &externalDocsURL, "external-docs-url", "", "", "OpenAPI external documentation URL in spec") cli.StringFlag(cmd, &servers, "server", "S", "", "Comma-separated OpenAPI server URL(s)") + cli.StringFlag(cmd, &securitySchemes, "security-schemes", "", "", "JSON object of custom OpenAPI security schemes") parent.AddCommand(cmd) } @@ -102,3 +114,16 @@ func addSpecCommand(parent *cli.Command) { func parseServers(raw string) []string { return splitUniqueCSV(raw) } + +func parseSecuritySchemes(raw string) (map[string]any, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + var schemes map[string]any + if err := json.Unmarshal([]byte(raw), &schemes); err != nil { + return nil, cli.Err("invalid security schemes JSON: %w", err) + } + return schemes, nil +} diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index 414da56..4d351f5 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -139,6 +139,9 @@ func TestAPISpecCmd_Good_JSON(t *testing.T) { if specCmd.Flag("server") == nil { t.Fatal("expected --server flag on spec command") } + if specCmd.Flag("security-schemes") == nil { + t.Fatal("expected --security-schemes flag on spec command") + } } func TestAPISpecCmd_Good_CustomDescription(t *testing.T) { @@ -259,6 +262,51 @@ func TestAPISpecCmd_Good_ContactFlagsPopulateSpecInfo(t *testing.T) { } } +func TestAPISpecCmd_Good_SecuritySchemesFlagPopulatesSpecComponents(t *testing.T) { + root := &cli.Command{Use: "root"} + AddAPICommands(root) + + outputFile := t.TempDir() + "/spec.json" + root.SetArgs([]string{ + "api", "spec", + "--security-schemes", `{"apiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"}}`, + "--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) + } + + securitySchemes, ok := spec["components"].(map[string]any)["securitySchemes"].(map[string]any) + if !ok { + t.Fatal("expected securitySchemes object in generated spec") + } + apiKeyAuth, ok := securitySchemes["apiKeyAuth"].(map[string]any) + if !ok { + t.Fatal("expected apiKeyAuth security scheme in generated spec") + } + 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 TestSpecGroupsIter_Good_DeduplicatesExtraBridge(t *testing.T) { snapshot := api.RegisteredSpecGroups() api.ResetSpecGroups()