Files
Cyrene_v2/internal/runtime/tool/validate.go
T

186 lines
4.3 KiB
Go

package tool
import (
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
)
// validateArgs performs a lightweight JSON Schema check on args against the
// tool's Parameters map: every name in "required" must be present, and each
// present argument matching a "properties" entry must satisfy its "type".
// Unknown argument fields are ignored (lenient) so a tool can accept optional
// extras without the dispatcher rejecting them. Full JSON Schema validation is
// deferred to M5 per the milestone plan.
func validateArgs(schema map[string]any, args map[string]any) error {
if schema == nil {
return nil
}
if args == nil {
args = map[string]any{}
}
if raw, ok := schema["required"]; ok {
reqs, ok := raw.([]any)
if !ok {
return fmt.Errorf("schema required must be an array")
}
for _, r := range reqs {
name, ok := r.(string)
if !ok {
return fmt.Errorf("schema required entries must be strings")
}
if _, present := args[name]; !present {
return fmt.Errorf("missing required argument %q", name)
}
}
}
props, _ := schema["properties"].(map[string]any)
for name, val := range args {
ps, ok := props[name]
if !ok {
continue // unknown field: ignored
}
pSchema, ok := ps.(map[string]any)
if !ok {
continue // unconstrained property: not checkable
}
if err := checkType(pSchema, name, val); err != nil {
return err
}
}
return nil
}
// checkType validates val against the "type" constraint of a property schema.
func checkType(schema map[string]any, name string, val any) error {
raw, ok := schema["type"]
if !ok || raw == nil {
return nil
}
switch t := raw.(type) {
case string:
return checkOne(t, name, val)
case []any:
for _, item := range t {
ts, ok := item.(string)
if !ok {
continue
}
if checkOne(ts, name, val) == nil {
return nil
}
}
return fmt.Errorf("argument %q has type %s, want one of %v", name, typeName(val), t)
default:
return nil
}
}
// checkOne validates val against a single JSON Schema type name.
func checkOne(t, name string, val any) error {
if matchesType(t, val) {
return nil
}
return fmt.Errorf("argument %q has type %s, want %s", name, typeName(val), t)
}
// matchesType reports whether v satisfies a single JSON Schema type name.
// Unknown type names are treated as unconstrained (lenient) and the types
// "any"/"" impose no constraint.
func matchesType(t string, v any) bool {
switch t {
case "any", "":
return true
case "string":
_, ok := v.(string)
return ok
case "boolean":
_, ok := v.(bool)
return ok
case "number":
return isNumber(v)
case "integer":
return isInteger(v)
case "array":
return isArrayLike(v)
case "object":
return isMapLike(v)
case "null":
return v == nil
default:
return true
}
}
// isNumber reports whether v is any integer or floating-point numeric type.
func isNumber(v any) bool {
switch v.(type) {
case json.Number, float32, float64,
int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64:
return true
default:
return false
}
}
// isInteger reports whether v is an integer type or a floating-point / json
// Number that holds an integral value.
func isInteger(v any) bool {
switch x := v.(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64:
return true
case float32:
return math.Trunc(float64(x)) == float64(x)
case float64:
return math.Trunc(x) == x
case json.Number:
_, err := strconv.ParseInt(x.String(), 10, 64)
return err == nil
default:
return false
}
}
// isArrayLike reports whether v is a slice or array.
func isArrayLike(v any) bool {
rv := reflect.ValueOf(v)
return rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array)
}
// isMapLike reports whether v is a map.
func isMapLike(v any) bool {
rv := reflect.ValueOf(v)
return rv.IsValid() && rv.Kind() == reflect.Map
}
// typeName renders a short, human-readable name for v for error messages.
func typeName(v any) string {
switch v.(type) {
case nil:
return "null"
case bool:
return "boolean"
case string:
return "string"
case []any:
return "array"
case map[string]any:
return "object"
default:
return fmt.Sprintf("%s", kindName(v))
}
}
// kindName maps a reflected kind to a readable label.
func kindName(v any) string {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
return "invalid"
}
return rv.Kind().String()
}