Files
go-openapi-compat/pkg/openapi/tool_helper.go
T
2026-05-10 11:40:53 +02:00

139 lines
3.4 KiB
Go

package openapi
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
)
// ReflectRegistry provides helper methods for registering tools using reflection.
type ReflectRegistry struct {
*MapRegistry
}
// NewReflectRegistry creates a new ReflectRegistry wrapping a MapRegistry.
func NewReflectRegistry(r *MapRegistry) *ReflectRegistry {
return &ReflectRegistry{MapRegistry: r}
}
// RegisterTool registers a tool with a typed handler.
// The handler's third argument must be a struct that defines the tool's parameters.
func RegisterTool[T any](r *MapRegistry, name, description string, handler func(context.Context, *Session, T) (any, error)) error {
var arg T
schema, err := generateSchema(reflect.TypeOf(arg))
if err != nil {
return fmt.Errorf("generate schema: %w", err)
}
wrappedHandler := func(ctx context.Context, s *Session, args json.RawMessage) (any, error) {
var params T
if err := json.Unmarshal(args, &params); err != nil {
return nil, fmt.Errorf("unmarshal arguments: %w", err)
}
return handler(ctx, s, params)
}
return r.Register(name, description, schema, wrappedHandler)
}
func generateSchema(t reflect.Type) (map[string]any, error) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("expected struct, got %s", t.Kind())
}
properties := make(map[string]any)
var required []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Split json tag to handle omitempty etc.
parts := strings.Split(jsonTag, ",")
name := parts[0]
prop := make(map[string]any)
switch field.Type.Kind() {
case reflect.String:
prop["type"] = "string"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
prop["type"] = "integer"
case reflect.Float32, reflect.Float64:
prop["type"] = "number"
case reflect.Bool:
prop["type"] = "boolean"
case reflect.Slice:
prop["type"] = "array"
// Simple slice handling, could be improved
itemType, _ := getJsonType(field.Type.Elem())
prop["items"] = map[string]string{"type": itemType}
case reflect.Struct:
subSchema, err := generateSchema(field.Type)
if err != nil {
return nil, err
}
prop = subSchema
default:
return nil, fmt.Errorf("unsupported field type: %s", field.Type.Kind())
}
if desc := field.Tag.Get("desc"); desc != "" {
prop["description"] = desc
}
properties[name] = prop
// Determine if field is required.
// Priority: 'required' tag > 'omitempty' in 'json' tag.
isRequired := true
for _, p := range parts[1:] {
if p == "omitempty" {
isRequired = false
break
}
}
if reqTag := field.Tag.Get("required"); reqTag != "" {
isRequired = (reqTag == "true")
}
if isRequired {
required = append(required, name)
}
}
schema := map[string]any{
"type": "object",
"properties": properties,
}
if len(required) > 0 {
schema["required"] = required
}
return schema, nil
}
func getJsonType(t reflect.Type) (string, error) {
switch t.Kind() {
case reflect.String:
return "string", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return "integer", nil
case reflect.Float32, reflect.Float64:
return "number", nil
case reflect.Bool:
return "boolean", nil
default:
return "", fmt.Errorf("unsupported item type: %s", t.Kind())
}
}