diff --git a/cmd/api/cmd_sdk.go b/cmd/api/cmd_sdk.go index c8c0b0a..1bc2e07 100644 --- a/cmd/api/cmd_sdk.go +++ b/cmd/api/cmd_sdk.go @@ -39,7 +39,7 @@ func addSDKCommand(parent *cli.Command) { } bridge := goapi.NewToolBridge("/tools") - groups := []goapi.RouteGroup{bridge} + groups := append(goapi.RegisteredSpecGroups(), bridge) tmpFile, err := os.CreateTemp("", "openapi-*.json") if err != nil { diff --git a/cmd/api/cmd_spec.go b/cmd/api/cmd_spec.go index 97b3033..782340f 100644 --- a/cmd/api/cmd_spec.go +++ b/cmd/api/cmd_spec.go @@ -22,8 +22,7 @@ func addSpecCommand(parent *cli.Command) { ) cmd := cli.NewCommand("spec", "Generate OpenAPI specification", "", func(cmd *cli.Command, args []string) error { - // Build spec from registered route groups. - // Additional groups can be added here as the platform grows. + // Build spec from all route groups registered for CLI generation. builder := &goapi.SpecBuilder{ Title: title, Description: description, @@ -31,11 +30,8 @@ func addSpecCommand(parent *cli.Command) { Servers: parseServers(servers), } - // Start with the default tool bridge — future versions will - // auto-populate from the MCP tool registry once the bridge - // integration lands in the local go-ai module. bridge := goapi.NewToolBridge("/tools") - groups := []goapi.RouteGroup{bridge} + groups := append(goapi.RegisteredSpecGroups(), bridge) if output != "" { if err := goapi.ExportSpecToFile(output, format, builder, groups); err != nil { diff --git a/cmd/api/cmd_test.go b/cmd/api/cmd_test.go index 778e59a..2f60b4a 100644 --- a/cmd/api/cmd_test.go +++ b/cmd/api/cmd_test.go @@ -8,9 +8,32 @@ import ( "os" "testing" + "github.com/gin-gonic/gin" + "forge.lthn.ai/core/cli/pkg/cli" + + api "dappco.re/go/core/api" ) +type specCmdStubGroup struct{} + +func (specCmdStubGroup) Name() string { return "registered" } +func (specCmdStubGroup) BasePath() string { return "/registered" } +func (specCmdStubGroup) RegisterRoutes(rg *gin.RouterGroup) {} +func (specCmdStubGroup) Describe() []api.RouteDescription { + return []api.RouteDescription{ + { + Method: "GET", + Path: "/ping", + Summary: "Ping registered group", + Tags: []string{"registered"}, + Response: map[string]any{ + "type": "string", + }, + }, + } +} + func TestAPISpecCmd_Good_CommandStructure(t *testing.T) { root := &cli.Command{Use: "root"} AddAPICommands(root) @@ -131,6 +154,40 @@ func TestAPISpecCmd_Good_ServerFlagAddsServers(t *testing.T) { } } +func TestAPISpecCmd_Good_RegisteredSpecGroups(t *testing.T) { + api.RegisterSpecGroups(specCmdStubGroup{}) + + root := &cli.Command{Use: "root"} + AddAPICommands(root) + + outputFile := t.TempDir() + "/spec.json" + root.SetArgs([]string{"api", "spec", "--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) + } + + paths, ok := spec["paths"].(map[string]any) + if !ok { + t.Fatalf("expected paths object in generated spec, got %T", spec["paths"]) + } + + if _, ok := paths["/registered/ping"]; !ok { + t.Fatal("expected registered route group path in generated spec") + } +} + func TestAPISDKCmd_Bad_EmptyLanguages(t *testing.T) { root := &cli.Command{Use: "root"} AddAPICommands(root) diff --git a/spec_registry.go b/spec_registry.go new file mode 100644 index 0000000..610806e --- /dev/null +++ b/spec_registry.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package api + +import "sync" + +// specRegistry stores RouteGroups that should be included in CLI-generated +// OpenAPI documents. Packages can register their groups during init and the +// API CLI will pick them up when building specs or SDKs. +var specRegistry struct { + mu sync.RWMutex + groups []RouteGroup +} + +// RegisterSpecGroups adds route groups to the package-level spec registry. +// Nil groups are ignored. Registered groups are returned by RegisteredSpecGroups +// in the order they were added. +func RegisterSpecGroups(groups ...RouteGroup) { + specRegistry.mu.Lock() + defer specRegistry.mu.Unlock() + + for _, group := range groups { + if group == nil { + continue + } + specRegistry.groups = append(specRegistry.groups, group) + } +} + +// RegisteredSpecGroups returns a copy of the route groups registered for +// CLI-generated OpenAPI documents. +func RegisteredSpecGroups() []RouteGroup { + specRegistry.mu.RLock() + defer specRegistry.mu.RUnlock() + + out := make([]RouteGroup, len(specRegistry.groups)) + copy(out, specRegistry.groups) + return out +}