1use serde_json::Value;
2
3use crate::ToolError;
4
5pub fn validate_code_mode_params_against_schema(
6 params: &Value,
7 schema: Option<&Value>,
8) -> Result<(), ToolError> {
9 let Some(schema) = schema else {
10 return Ok(());
11 };
12 validate_value(params, schema, schema, "params")
13}
14
15fn validate_value(
16 value: &Value,
17 schema: &Value,
18 root: &Value,
19 path: &str,
20) -> Result<(), ToolError> {
21 let Some(object) = schema.as_object() else {
22 return Ok(());
23 };
24 if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
25 let Some(resolved) = reference
26 .strip_prefix('#')
27 .and_then(|pointer| root.pointer(pointer))
28 else {
29 return Err(invalid(path, "uses an unresolved local $ref"));
30 };
31 return validate_value(value, resolved, root, path);
32 }
33 if let Some(values) = object.get("enum").and_then(Value::as_array) {
34 if !values.iter().any(|candidate| candidate == value) {
35 return Err(invalid(path, "must match enum"));
36 }
37 }
38 if let Some(expected) = object.get("const") {
39 if expected != value {
40 return Err(invalid(path, "must match const"));
41 }
42 }
43 if let Some(kind) = object.get("type") {
44 let ok = match kind {
45 Value::String(kind) => matches_type(value, kind),
46 Value::Array(kinds) => kinds
47 .iter()
48 .filter_map(Value::as_str)
49 .any(|kind| matches_type(value, kind)),
50 _ => true,
51 };
52 if !ok {
53 return Err(invalid(path, "has wrong type"));
54 }
55 }
56 if let Some(object_value) = value.as_object() {
57 if let Some(required) = object.get("required").and_then(Value::as_array) {
58 for key in required.iter().filter_map(Value::as_str) {
59 if !object_value.contains_key(key) {
60 return Err(ToolError::MissingParam {
61 message: format!("callTool params missing required field `{key}`"),
62 param: key.to_string(),
63 });
64 }
65 }
66 }
67 if let Some(properties) = object.get("properties").and_then(Value::as_object) {
68 for (key, child_schema) in properties {
69 if let Some(child) = object_value.get(key) {
70 validate_value(child, child_schema, root, &format!("{path}.{key}"))?;
71 }
72 }
73 }
74 if object.get("additionalProperties").and_then(Value::as_bool) == Some(false) {
75 if let Some(properties) = object.get("properties").and_then(Value::as_object) {
76 for key in object_value.keys() {
77 if !properties.contains_key(key) {
78 return Err(invalid(&format!("{path}.{key}"), "is not allowed"));
79 }
80 }
81 }
82 }
83 }
84 if let Some(items) = object.get("items") {
85 if let Some(values) = value.as_array() {
86 for (index, child) in values.iter().enumerate() {
87 validate_value(child, items, root, &format!("{path}[{index}]"))?;
88 }
89 }
90 }
91 Ok(())
92}
93
94fn matches_type(value: &Value, kind: &str) -> bool {
95 match kind {
96 "string" => value.is_string(),
97 "integer" => value.as_i64().is_some() || value.as_u64().is_some(),
98 "number" => value.is_number(),
99 "boolean" => value.is_boolean(),
100 "object" => value.is_object(),
101 "array" => value.is_array(),
102 "null" => value.is_null(),
103 _ => true,
104 }
105}
106
107fn invalid(path: &str, reason: &str) -> ToolError {
108 ToolError::InvalidParam {
109 message: format!("{path} {reason}"),
110 param: path.to_string(),
111 }
112}