feat(api): register CLI spec groups

This commit is contained in:
Virgil 2026-04-01 18:29:45 +00:00
parent edb1cf0c1e
commit 3b26a15048
4 changed files with 99 additions and 7 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

39
spec_registry.go Normal file
View file

@ -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
}