Skip to main content

wallfacer_core/
differential.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fs, io,
4    path::{Path, PathBuf},
5};
6
7use serde_json::{json, Map, Value};
8use thiserror::Error;
9
10use crate::corpus::sanitize_tool_name;
11
12#[derive(Debug, Error)]
13pub enum DifferentialError {
14    #[error("failed to create schema directory {path}: {source}")]
15    CreateDir { path: PathBuf, source: io::Error },
16    #[error("failed to write schema {path}: {source}")]
17    Write { path: PathBuf, source: io::Error },
18    #[error("failed to read schema {path}: {source}")]
19    Read { path: PathBuf, source: io::Error },
20    #[error("failed to parse schema {path}: {source}")]
21    Parse {
22        path: PathBuf,
23        source: serde_json::Error,
24    },
25}
26
27pub type Result<T> = std::result::Result<T, DifferentialError>;
28
29pub fn inferred_schema_dir() -> PathBuf {
30    PathBuf::from(".wallfacer/inferred_schemas")
31}
32
33pub fn schema_path(dir: &Path, tool_name: &str) -> PathBuf {
34    dir.join(format!("{}.json", sanitize_tool_name(tool_name)))
35}
36
37pub fn save_schema(dir: &Path, tool_name: &str, schema: &Value) -> Result<PathBuf> {
38    fs::create_dir_all(dir).map_err(|source| DifferentialError::CreateDir {
39        path: dir.to_path_buf(),
40        source,
41    })?;
42
43    let path = schema_path(dir, tool_name);
44    // `serde_json::to_string_pretty` on a `Value` is infallible.
45    let body = serde_json::to_string_pretty(schema).unwrap_or_else(|_| "{}".to_string());
46    fs::write(&path, body).map_err(|source| DifferentialError::Write {
47        path: path.clone(),
48        source,
49    })?;
50    Ok(path)
51}
52
53pub fn load_schema(dir: &Path, tool_name: &str) -> Result<Option<Value>> {
54    let path = schema_path(dir, tool_name);
55    if !path.is_file() {
56        return Ok(None);
57    }
58
59    let body = fs::read_to_string(&path).map_err(|source| DifferentialError::Read {
60        path: path.clone(),
61        source,
62    })?;
63    let schema = serde_json::from_str(&body).map_err(|source| DifferentialError::Parse {
64        path: path.clone(),
65        source,
66    })?;
67    Ok(Some(schema))
68}
69
70pub fn response_value(result: &rmcp::model::CallToolResult) -> Value {
71    result.structured_content.clone().unwrap_or_else(|| {
72        // `CallToolResult` is fully `Serialize`; fall back to `Null` if a future
73        // SDK revision introduces an un-serializable field rather than panicking.
74        serde_json::to_value(result).unwrap_or(Value::Null)
75    })
76}
77
78pub fn boundary_payload(schema: &Value) -> Value {
79    match boundary_value(schema) {
80        Value::Object(map) => Value::Object(map),
81        _ => Value::Object(Map::new()),
82    }
83}
84
85pub fn infer_schema(values: &[Value]) -> Value {
86    if values.is_empty() {
87        return json!({});
88    }
89    infer_values(values)
90}
91
92fn infer_values(values: &[Value]) -> Value {
93    if values.iter().all(Value::is_null) {
94        return json!({"type": "null"});
95    }
96
97    let mut types = values.iter().map(type_name).collect::<BTreeSet<_>>();
98    if types.len() > 1 {
99        let type_values = types.into_iter().map(Value::String).collect::<Vec<_>>();
100        return json!({ "type": type_values });
101    }
102
103    match types.pop_first().as_deref() {
104        Some("object") => infer_object(values),
105        Some("array") => infer_array(values),
106        Some("integer") => json!({"type": "integer"}),
107        Some("number") => json!({"type": "number"}),
108        Some("string") => json!({"type": "string"}),
109        Some("boolean") => json!({"type": "boolean"}),
110        Some("null") => json!({"type": "null"}),
111        _ => json!({}),
112    }
113}
114
115fn infer_object(values: &[Value]) -> Value {
116    let objects = values
117        .iter()
118        .filter_map(Value::as_object)
119        .collect::<Vec<_>>();
120    let mut property_values = BTreeMap::<String, Vec<Value>>::new();
121    let mut required = BTreeSet::<String>::new();
122
123    if let Some(first) = objects.first() {
124        required.extend(first.keys().cloned());
125    }
126
127    for object in &objects {
128        let keys = object.keys().cloned().collect::<BTreeSet<_>>();
129        required = required.intersection(&keys).cloned().collect();
130
131        for (key, value) in *object {
132            property_values
133                .entry(key.clone())
134                .or_default()
135                .push(value.clone());
136        }
137    }
138
139    let mut properties = Map::new();
140    for (key, values) in property_values {
141        properties.insert(key, infer_values(&values));
142    }
143
144    json!({
145        "type": "object",
146        "properties": properties,
147        "required": required.into_iter().collect::<Vec<_>>(),
148        "additionalProperties": true
149    })
150}
151
152fn infer_array(values: &[Value]) -> Value {
153    let items = values
154        .iter()
155        .filter_map(Value::as_array)
156        .flat_map(|items| items.iter().cloned())
157        .collect::<Vec<_>>();
158
159    json!({
160        "type": "array",
161        "items": infer_schema(&items)
162    })
163}
164
165fn boundary_value(schema: &Value) -> Value {
166    if let Some(value) = schema.get("const") {
167        return value.clone();
168    }
169
170    if let Some(values) = schema.get("enum").and_then(Value::as_array) {
171        return values.first().cloned().unwrap_or(Value::Null);
172    }
173
174    match schema_type(schema).as_deref() {
175        Some("object") | None => {
176            let mut object = Map::new();
177            let properties = schema
178                .get("properties")
179                .and_then(Value::as_object)
180                .cloned()
181                .unwrap_or_default();
182            let required = schema
183                .get("required")
184                .and_then(Value::as_array)
185                .map(|values| {
186                    values
187                        .iter()
188                        .filter_map(Value::as_str)
189                        .map(ToOwned::to_owned)
190                        .collect::<Vec<_>>()
191                })
192                .unwrap_or_else(|| properties.keys().cloned().collect());
193
194            for key in required {
195                if let Some(property_schema) = properties.get(&key) {
196                    object.insert(key, boundary_value(property_schema));
197                }
198            }
199            Value::Object(object)
200        }
201        Some("integer") => {
202            if let Some(max) = schema.get("maximum").and_then(Value::as_i64) {
203                json!(max)
204            } else if let Some(min) = schema.get("minimum").and_then(Value::as_i64) {
205                json!(min)
206            } else {
207                json!(1)
208            }
209        }
210        Some("number") => {
211            if let Some(max) = schema.get("maximum").and_then(Value::as_f64) {
212                json!(max)
213            } else if let Some(min) = schema.get("minimum").and_then(Value::as_f64) {
214                json!(min)
215            } else {
216                json!(1.0)
217            }
218        }
219        Some("string") => Value::String("wallfacer".to_string()),
220        Some("boolean") => Value::Bool(true),
221        Some("array") => Value::Array(Vec::new()),
222        Some("null") => Value::Null,
223        Some(_) => Value::Null,
224    }
225}
226
227fn schema_type(schema: &Value) -> Option<String> {
228    match schema.get("type") {
229        Some(Value::String(value)) => Some(value.clone()),
230        Some(Value::Array(values)) => values
231            .iter()
232            .filter_map(Value::as_str)
233            .find(|value| *value != "null")
234            .map(ToOwned::to_owned),
235        _ if schema.get("properties").is_some() => Some("object".to_string()),
236        _ => None,
237    }
238}
239
240fn type_name(value: &Value) -> String {
241    match value {
242        Value::Null => "null",
243        Value::Bool(_) => "boolean",
244        Value::Number(number) if number.is_i64() || number.is_u64() => "integer",
245        Value::Number(_) => "number",
246        Value::String(_) => "string",
247        Value::Array(_) => "array",
248        Value::Object(_) => "object",
249    }
250    .to_string()
251}