api/bridge.go
Claude 43abce034e
chore(api): AX compliance sweep — banned imports, naming, test coverage
Replace fmt/errors/strings/encoding/json/os/os/exec/path/filepath with
core primitives; rename abbreviated variables; add Ugly test variants to
all test files; rename integration tests to TestFilename_Function_{Good,Bad,Ugly}.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 09:27:41 +01:00

128 lines
3.7 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api
import (
"iter"
"github.com/gin-gonic/gin"
)
// ToolDescriptor describes a tool that can be exposed as a REST endpoint.
type ToolDescriptor struct {
Name string // Tool name, e.g. "file_read" (becomes POST path segment)
Description string // Human-readable description
Group string // OpenAPI tag group, e.g. "files"
InputSchema map[string]any // JSON Schema for request body
OutputSchema map[string]any // JSON Schema for response data (optional)
}
// ToolBridge converts tool descriptors into REST endpoints and OpenAPI paths.
// It implements both RouteGroup and DescribableGroup.
type ToolBridge struct {
basePath string
name string
tools []boundTool
}
type boundTool struct {
descriptor ToolDescriptor
handler gin.HandlerFunc
}
// NewToolBridge creates a bridge that mounts tool endpoints at basePath.
//
// bridge := api.NewToolBridge("/tools")
// bridge.Add(api.ToolDescriptor{Name: "file_read"}, fileReadHandler)
// engine.Register(bridge)
func NewToolBridge(basePath string) *ToolBridge {
return &ToolBridge{
basePath: basePath,
name: "tools",
}
}
// Add registers a tool with its HTTP handler.
//
// bridge.Add(api.ToolDescriptor{Name: "file_read", Group: "files"}, fileReadHandler)
func (b *ToolBridge) Add(desc ToolDescriptor, handler gin.HandlerFunc) {
b.tools = append(b.tools, boundTool{descriptor: desc, handler: handler})
}
// Name returns the bridge identifier.
func (b *ToolBridge) Name() string { return b.name }
// BasePath returns the URL prefix for all tool endpoints.
func (b *ToolBridge) BasePath() string { return b.basePath }
// RegisterRoutes mounts POST /{tool_name} for each registered tool.
func (b *ToolBridge) RegisterRoutes(rg *gin.RouterGroup) {
for _, tool := range b.tools {
rg.POST("/"+tool.descriptor.Name, tool.handler)
}
}
// Describe returns OpenAPI route descriptions for all registered tools.
func (b *ToolBridge) Describe() []RouteDescription {
descs := make([]RouteDescription, 0, len(b.tools))
for _, tool := range b.tools {
tags := []string{tool.descriptor.Group}
if tool.descriptor.Group == "" {
tags = []string{b.name}
}
descs = append(descs, RouteDescription{
Method: "POST",
Path: "/" + tool.descriptor.Name,
Summary: tool.descriptor.Description,
Description: tool.descriptor.Description,
Tags: tags,
RequestBody: tool.descriptor.InputSchema,
Response: tool.descriptor.OutputSchema,
})
}
return descs
}
// DescribeIter returns an iterator over OpenAPI route descriptions for all registered tools.
func (b *ToolBridge) DescribeIter() iter.Seq[RouteDescription] {
return func(yield func(RouteDescription) bool) {
for _, tool := range b.tools {
tags := []string{tool.descriptor.Group}
if tool.descriptor.Group == "" {
tags = []string{b.name}
}
rd := RouteDescription{
Method: "POST",
Path: "/" + tool.descriptor.Name,
Summary: tool.descriptor.Description,
Description: tool.descriptor.Description,
Tags: tags,
RequestBody: tool.descriptor.InputSchema,
Response: tool.descriptor.OutputSchema,
}
if !yield(rd) {
return
}
}
}
}
// Tools returns all registered tool descriptors.
func (b *ToolBridge) Tools() []ToolDescriptor {
descs := make([]ToolDescriptor, len(b.tools))
for i, tool := range b.tools {
descs[i] = tool.descriptor
}
return descs
}
// ToolsIter returns an iterator over all registered tool descriptors.
func (b *ToolBridge) ToolsIter() iter.Seq[ToolDescriptor] {
return func(yield func(ToolDescriptor) bool) {
for _, tool := range b.tools {
if !yield(tool.descriptor) {
return
}
}
}
}