tokenfold_core/transforms/
schema.rs1use serde_json::Value;
11
12pub const TRANSFORM_ID: &str = "schema_compaction";
14pub const TRANSFORM_VERSION: &str = "1.0.0";
17
18#[derive(Debug, thiserror::Error)]
20pub enum SchemaCompactionError {
21 #[error("invalid json: {0}")]
22 Invalid(#[from] serde_json::Error),
23}
24
25pub 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
42fn 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}