139 lines
3.4 KiB
Go
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, ¶ms); 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())
|
|
}
|
|
}
|