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() }