ironflow_core/
schema_transform.rs1use serde_json::{Map, Value};
27use tracing::warn;
28
29pub fn transform_schema(schema: &str) -> String {
44 let mut value: Value = match serde_json::from_str(schema) {
45 Ok(v) => v,
46 Err(e) => {
47 warn!(error = %e, "failed to parse JSON schema for transformation, using original");
48 return schema.to_string();
49 }
50 };
51
52 let defs = extract_defs(&value);
53 remove_meta_fields(&mut value);
54 inline_refs(&mut value, &defs);
55 add_additional_properties_false(&mut value);
56
57 serde_json::to_string(&value).unwrap_or_else(|_| schema.to_string())
58}
59
60fn extract_defs(schema: &Value) -> Map<String, Value> {
62 schema
63 .get("$defs")
64 .or_else(|| schema.get("definitions"))
65 .and_then(|v| v.as_object())
66 .cloned()
67 .unwrap_or_default()
68}
69
70fn remove_meta_fields(value: &mut Value) {
72 if let Some(obj) = value.as_object_mut() {
73 obj.remove("$schema");
74 obj.remove("title");
75 obj.remove("default");
76 obj.remove("$defs");
77 obj.remove("definitions");
78
79 for (_, v) in obj.iter_mut() {
80 remove_meta_fields(v);
81 }
82 } else if let Some(arr) = value.as_array_mut() {
83 for item in arr.iter_mut() {
84 remove_meta_fields(item);
85 }
86 }
87}
88
89fn inline_refs(value: &mut Value, defs: &Map<String, Value>) {
91 match value {
92 Value::Object(obj) => {
93 if let Some(ref_val) = obj.get("$ref").and_then(|v| v.as_str()).map(String::from)
94 && let Some(resolved) = resolve_ref(&ref_val, defs)
95 {
96 let mut resolved = resolved.clone();
97 inline_refs(&mut resolved, defs);
98 *value = resolved;
99 return;
100 }
101
102 for (_, v) in obj.iter_mut() {
103 inline_refs(v, defs);
104 }
105 }
106 Value::Array(arr) => {
107 for item in arr.iter_mut() {
108 inline_refs(item, defs);
109 }
110 }
111 _ => {}
112 }
113}
114
115fn resolve_ref<'a>(ref_path: &str, defs: &'a Map<String, Value>) -> Option<&'a Value> {
117 let name = ref_path
118 .strip_prefix("#/$defs/")
119 .or_else(|| ref_path.strip_prefix("#/definitions/"))?;
120 defs.get(name)
121}
122
123fn add_additional_properties_false(value: &mut Value) {
125 if let Some(obj) = value.as_object_mut() {
126 let is_object_schema = obj.get("type").and_then(|t| t.as_str()) == Some("object");
127
128 if is_object_schema && !obj.contains_key("additionalProperties") {
129 obj.insert("additionalProperties".to_string(), Value::Bool(false));
130 }
131
132 for (_, v) in obj.iter_mut() {
133 add_additional_properties_false(v);
134 }
135 } else if let Some(arr) = value.as_array_mut() {
136 for item in arr.iter_mut() {
137 add_additional_properties_false(item);
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use serde_json::json;
146
147 #[test]
148 fn adds_additional_properties_to_simple_object() {
149 let schema = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
150 let result = transform_schema(schema);
151 let parsed: Value = serde_json::from_str(&result).unwrap();
152 assert_eq!(parsed["additionalProperties"], json!(false));
153 }
154
155 #[test]
156 fn adds_additional_properties_to_nested_objects() {
157 let schema = json!({
158 "type": "object",
159 "properties": {
160 "address": {
161 "type": "object",
162 "properties": {
163 "city": {"type": "string"}
164 }
165 }
166 }
167 });
168 let result = transform_schema(&schema.to_string());
169 let parsed: Value = serde_json::from_str(&result).unwrap();
170 assert_eq!(parsed["additionalProperties"], json!(false));
171 assert_eq!(
172 parsed["properties"]["address"]["additionalProperties"],
173 json!(false)
174 );
175 }
176
177 #[test]
178 fn preserves_existing_additional_properties() {
179 let schema = json!({
180 "type": "object",
181 "properties": {"x": {"type": "integer"}},
182 "additionalProperties": true
183 });
184 let result = transform_schema(&schema.to_string());
185 let parsed: Value = serde_json::from_str(&result).unwrap();
186 assert_eq!(parsed["additionalProperties"], json!(true));
187 }
188
189 #[test]
190 fn inlines_defs_refs() {
191 let schema = json!({
192 "type": "object",
193 "properties": {
194 "item": {"$ref": "#/$defs/Item"}
195 },
196 "$defs": {
197 "Item": {
198 "type": "object",
199 "properties": {
200 "name": {"type": "string"}
201 }
202 }
203 }
204 });
205 let result = transform_schema(&schema.to_string());
206 let parsed: Value = serde_json::from_str(&result).unwrap();
207
208 assert!(parsed.get("$defs").is_none());
209 assert_eq!(parsed["properties"]["item"]["type"], json!("object"));
210 assert_eq!(
211 parsed["properties"]["item"]["properties"]["name"]["type"],
212 json!("string")
213 );
214 assert_eq!(
215 parsed["properties"]["item"]["additionalProperties"],
216 json!(false)
217 );
218 }
219
220 #[test]
221 fn inlines_definitions_refs() {
222 let schema = json!({
223 "type": "object",
224 "properties": {
225 "item": {"$ref": "#/definitions/Item"}
226 },
227 "definitions": {
228 "Item": {
229 "type": "object",
230 "properties": {
231 "id": {"type": "integer"}
232 }
233 }
234 }
235 });
236 let result = transform_schema(&schema.to_string());
237 let parsed: Value = serde_json::from_str(&result).unwrap();
238
239 assert!(parsed.get("definitions").is_none());
240 assert_eq!(parsed["properties"]["item"]["type"], json!("object"));
241 }
242
243 #[test]
244 fn removes_meta_fields() {
245 let schema = json!({
246 "$schema": "http://json-schema.org/draft-07/schema#",
247 "title": "MySchema",
248 "type": "object",
249 "properties": {
250 "score": {
251 "type": "integer",
252 "title": "The Score",
253 "default": 0
254 }
255 }
256 });
257 let result = transform_schema(&schema.to_string());
258 let parsed: Value = serde_json::from_str(&result).unwrap();
259
260 assert!(parsed.get("$schema").is_none());
261 assert!(parsed.get("title").is_none());
262 assert!(parsed["properties"]["score"].get("title").is_none());
263 assert!(parsed["properties"]["score"].get("default").is_none());
264 }
265
266 #[test]
267 fn idempotent_on_already_transformed_schema() {
268 let schema = json!({
269 "type": "object",
270 "properties": {
271 "x": {"type": "integer"}
272 },
273 "additionalProperties": false
274 });
275 let input = schema.to_string();
276 let first = transform_schema(&input);
277 let second = transform_schema(&first);
278 assert_eq!(first, second);
279 }
280
281 #[test]
282 fn handles_invalid_json_gracefully() {
283 let bad = "not valid json{";
284 let result = transform_schema(bad);
285 assert_eq!(result, bad);
286 }
287
288 #[test]
289 fn handles_non_object_schema() {
290 let schema = r#"{"type":"string"}"#;
291 let result = transform_schema(schema);
292 let parsed: Value = serde_json::from_str(&result).unwrap();
293 assert!(parsed.get("additionalProperties").is_none());
294 }
295
296 #[test]
297 fn inlines_nested_refs() {
298 let schema = json!({
299 "type": "object",
300 "properties": {
301 "items": {
302 "type": "array",
303 "items": {"$ref": "#/$defs/Item"}
304 }
305 },
306 "$defs": {
307 "Item": {
308 "type": "object",
309 "properties": {
310 "nested": {"$ref": "#/$defs/Nested"}
311 }
312 },
313 "Nested": {
314 "type": "object",
315 "properties": {
316 "value": {"type": "string"}
317 }
318 }
319 }
320 });
321 let result = transform_schema(&schema.to_string());
322 let parsed: Value = serde_json::from_str(&result).unwrap();
323
324 let item = &parsed["properties"]["items"]["items"];
325 assert_eq!(item["type"], json!("object"));
326 assert_eq!(item["additionalProperties"], json!(false));
327
328 let nested = &item["properties"]["nested"];
329 assert_eq!(nested["type"], json!("object"));
330 assert_eq!(nested["additionalProperties"], json!(false));
331 assert_eq!(nested["properties"]["value"]["type"], json!("string"));
332 }
333
334 #[test]
335 fn handles_schemars_generated_schema() {
336 let schema = json!({
337 "$schema": "http://json-schema.org/draft-07/schema#",
338 "title": "Review",
339 "type": "object",
340 "required": ["score", "summary"],
341 "properties": {
342 "score": {"type": "integer", "default": 5},
343 "summary": {"type": "string"}
344 }
345 });
346 let result = transform_schema(&schema.to_string());
347 let parsed: Value = serde_json::from_str(&result).unwrap();
348
349 assert!(parsed.get("$schema").is_none());
350 assert!(parsed.get("title").is_none());
351 assert!(parsed["properties"]["score"].get("default").is_none());
352 assert_eq!(parsed["additionalProperties"], json!(false));
353 assert_eq!(parsed["required"], json!(["score", "summary"]));
354 }
355}