Skip to main content

tokenfold_core/transforms/
schema.rs

1//! `schema_compaction` (canonical transform id: `"schema_compaction"`, version `1.0.0`): a
2//! semantics-preserving transform for JSON payloads containing JSON-Schema-shaped fragments
3//! and/or OpenAI-style tool/function definitions.
4//!
5//! It never drops required semantic information — `description`, `required`, `enum`, `type`,
6//! `default`, and `name` fields (and every other field) survive byte-for-byte. The only thing
7//! this transform ever shortens is illustrative `"examples"` arrays, which are truncated down
8//! to a configured maximum length wherever they appear in the document.
9
10use serde_json::Value;
11
12/// Canonical stable id for this transform, as used in reports and `--disable`.
13pub const TRANSFORM_ID: &str = "schema_compaction";
14/// Semantic version of this transform's behavior (see `crate::modes` for the pipeline-wide
15/// version table this must stay in sync with).
16pub const TRANSFORM_VERSION: &str = "1.0.0";
17
18/// Errors produced while compacting a schema/tool-definition payload.
19#[derive(Debug, thiserror::Error)]
20pub enum SchemaCompactionError {
21    #[error("invalid json: {0}")]
22    Invalid(#[from] serde_json::Error),
23}
24
25/// Parses `input` as JSON, truncates every `"examples"` array found anywhere in the document
26/// (object or array nesting, at any depth) down to at most `max_examples` elements — keeping
27/// the first `max_examples` elements in their original order — and re-serializes the result in
28/// compact form.
29///
30/// Every other key and value in the document (including `description`, `required`, `enum`,
31/// `type`, `default`, and tool/function `name` fields) is left exactly as parsed: this function
32/// never renames, removes, or reorders anything other than shortening `"examples"` arrays.
33///
34/// Returns [`SchemaCompactionError::Invalid`] if `input` is not valid JSON.
35pub fn compact_schema(input: &[u8], max_examples: usize) -> Result<Vec<u8>, SchemaCompactionError> {
36    let mut value: Value = serde_json::from_slice(input)?;
37    shorten_examples(&mut value, max_examples);
38    let bytes = serde_json::to_vec(&value)?;
39    Ok(bytes)
40}
41
42/// Recursively walks `value`, truncating any `"examples"` array (on any object, at any depth)
43/// to `max_examples` elements. Objects without an `"examples"` key, and every scalar
44/// (string/number/bool/null), are left untouched.
45fn shorten_examples(value: &mut Value, max_examples: usize) {
46    match value {
47        Value::Object(map) => {
48            if let Some(Value::Array(examples)) = map.get_mut("examples") {
49                examples.truncate(max_examples);
50            }
51            for v in map.values_mut() {
52                shorten_examples(v, max_examples);
53            }
54        }
55        Value::Array(arr) => {
56            for v in arr.iter_mut() {
57                shorten_examples(v, max_examples);
58            }
59        }
60        Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {}
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use serde_json::json;
68
69    #[test]
70    fn description_field_preserved_byte_for_byte() {
71        let input = json!({
72            "name": "get_weather",
73            "description": "Fetches the current weather conditions for a named city or postal code.",
74            "parameters": {
75                "type": "object",
76                "properties": {
77                    "location": { "type": "string" }
78                }
79            }
80        });
81        let bytes = serde_json::to_vec(&input).unwrap();
82
83        let out = compact_schema(&bytes, 3).unwrap();
84        let parsed: Value = serde_json::from_slice(&out).unwrap();
85
86        assert_eq!(
87            parsed["description"],
88            json!("Fetches the current weather conditions for a named city or postal code.")
89        );
90    }
91
92    #[test]
93    fn required_array_preserved() {
94        let input = json!({
95            "parameters": {
96                "type": "object",
97                "properties": {
98                    "a": { "type": "string" },
99                    "b": { "type": "number" }
100                }
101            },
102            "required": ["a", "b"]
103        });
104        let bytes = serde_json::to_vec(&input).unwrap();
105
106        let out = compact_schema(&bytes, 2).unwrap();
107        let parsed: Value = serde_json::from_slice(&out).unwrap();
108
109        assert_eq!(parsed["required"], json!(["a", "b"]));
110    }
111
112    #[test]
113    fn enum_array_preserved() {
114        let input = json!({
115            "parameters": {
116                "properties": {
117                    "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
118                }
119            }
120        });
121        let bytes = serde_json::to_vec(&input).unwrap();
122
123        let out = compact_schema(&bytes, 1).unwrap();
124        let parsed: Value = serde_json::from_slice(&out).unwrap();
125
126        assert_eq!(
127            parsed["parameters"]["properties"]["unit"]["enum"],
128            json!(["celsius", "fahrenheit"])
129        );
130    }
131
132    #[test]
133    fn type_field_preserved() {
134        let input = json!({
135            "parameters": {
136                "type": "object",
137                "properties": {}
138            }
139        });
140        let bytes = serde_json::to_vec(&input).unwrap();
141
142        let out = compact_schema(&bytes, 1).unwrap();
143        let parsed: Value = serde_json::from_slice(&out).unwrap();
144
145        assert_eq!(parsed["parameters"]["type"], json!("object"));
146    }
147
148    #[test]
149    fn default_field_preserved() {
150        let input = json!({
151            "parameters": {
152                "properties": {
153                    "count": { "type": "integer", "default": 10 }
154                }
155            }
156        });
157        let bytes = serde_json::to_vec(&input).unwrap();
158
159        let out = compact_schema(&bytes, 1).unwrap();
160        let parsed: Value = serde_json::from_slice(&out).unwrap();
161
162        assert_eq!(
163            parsed["parameters"]["properties"]["count"]["default"],
164            json!(10)
165        );
166    }
167
168    #[test]
169    fn examples_array_shortened_to_max_examples_keeping_first_elements() {
170        let input = json!({
171            "parameters": {
172                "examples": ["one", "two", "three", "four", "five"]
173            }
174        });
175        let bytes = serde_json::to_vec(&input).unwrap();
176
177        let out = compact_schema(&bytes, 1).unwrap();
178        let parsed: Value = serde_json::from_slice(&out).unwrap();
179
180        assert_eq!(parsed["parameters"]["examples"], json!(["one"]));
181    }
182
183    #[test]
184    fn examples_array_shorter_than_max_is_left_unchanged() {
185        let input = json!({
186            "examples": ["only-one"]
187        });
188        let bytes = serde_json::to_vec(&input).unwrap();
189
190        let out = compact_schema(&bytes, 5).unwrap();
191        let parsed: Value = serde_json::from_slice(&out).unwrap();
192
193        assert_eq!(parsed["examples"], json!(["only-one"]));
194    }
195
196    #[test]
197    fn tool_name_field_preserved() {
198        let input = json!({
199            "name": "search_flights",
200            "parameters": {
201                "type": "object",
202                "properties": {}
203            }
204        });
205        let bytes = serde_json::to_vec(&input).unwrap();
206
207        let out = compact_schema(&bytes, 1).unwrap();
208        let parsed: Value = serde_json::from_slice(&out).unwrap();
209
210        assert_eq!(parsed["name"], json!("search_flights"));
211    }
212
213    #[test]
214    fn invalid_json_input_returns_error() {
215        let result = compact_schema(b"{not json", 1);
216
217        assert!(matches!(result, Err(SchemaCompactionError::Invalid(_))));
218    }
219
220    #[test]
221    fn document_with_no_examples_key_round_trips_without_panic() {
222        let input = json!({
223            "name": "lookup_stock_price",
224            "description": "Looks up the latest known stock price for a given ticker symbol.",
225            "parameters": {
226                "type": "object",
227                "properties": {
228                    "ticker": { "type": "string" }
229                },
230                "required": ["ticker"]
231            }
232        });
233        let bytes = serde_json::to_vec(&input).unwrap();
234
235        let out = compact_schema(&bytes, 3).unwrap();
236        let parsed: Value = serde_json::from_slice(&out).unwrap();
237
238        assert_eq!(parsed, input);
239    }
240}