Skip to main content

soma_codemode/
schema.rs

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}