186 lines
4.3 KiB
Go
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()
|
|
}
|