1use std::borrow::Cow;
30
31use serde_json::{Map, Value};
32
33pub fn strip_schema_keyword(schema: &mut Value) {
58 if let Some(obj) = schema.as_object_mut() {
59 obj.remove("$schema");
60 }
61 recurse_into_subschemas(schema, strip_schema_keyword);
62}
63
64pub fn strip_conditional_keywords(schema: &mut Value) {
89 if let Some(obj) = schema.as_object_mut() {
90 obj.remove("if");
91 obj.remove("then");
92 obj.remove("else");
93 }
94 recurse_into_subschemas(schema, strip_conditional_keywords);
95}
96
97pub fn add_implicit_object_type(schema: &mut Value) {
118 if let Some(obj) = schema.as_object_mut()
119 && obj.contains_key("properties")
120 && !obj.contains_key("type")
121 {
122 obj.insert("type".to_string(), Value::String("object".to_string()));
123 }
124 recurse_into_subschemas(schema, add_implicit_object_type);
125}
126
127pub fn convert_const_to_enum(schema: &mut Value) {
148 if let Some(obj) = schema.as_object_mut()
149 && let Some(const_val) = obj.remove("const")
150 {
151 obj.insert("enum".to_string(), Value::Array(vec![const_val]));
152 }
153 recurse_into_subschemas(schema, convert_const_to_enum);
154}
155
156pub fn strip_unsupported_formats(schema: &mut Value, allowed: &[&str]) {
182 if let Some(obj) = schema.as_object_mut() {
183 let should_remove =
184 obj.get("format").and_then(|f| f.as_str()).is_some_and(|f| !allowed.contains(&f));
185 if should_remove {
186 obj.remove("format");
187 }
188 }
189 recurse_into_subschemas_with_context(schema, allowed, strip_unsupported_formats);
191}
192
193pub fn truncate_tool_name(name: &str, max_bytes: usize) -> Cow<'_, str> {
217 if name.len() <= max_bytes {
218 Cow::Borrowed(name)
219 } else {
220 let mut end = max_bytes;
222 while end > 0 && !name.is_char_boundary(end) {
223 end -= 1;
224 }
225 Cow::Owned(name[..end].to_string())
226 }
227}
228
229pub fn strip_null_from_enum(schema: &mut Value) {
250 if let Some(obj) = schema.as_object_mut()
251 && let Some(enum_val) = obj.get_mut("enum")
252 && let Some(arr) = enum_val.as_array_mut()
253 {
254 arr.retain(|v| !v.is_null());
255 if arr.is_empty() {
256 obj.remove("enum");
257 }
258 }
259 recurse_into_subschemas(schema, strip_null_from_enum);
260}
261
262pub fn resolve_refs(schema: &mut Value, definitions: &Map<String, Value>, depth: usize) {
303 if depth > 10 {
305 if schema.as_object().is_some_and(|obj| obj.contains_key("$ref")) {
307 *schema = serde_json::json!({"type": "object"});
308 }
309 return;
310 }
311
312 let Some(obj) = schema.as_object() else {
313 return;
314 };
315
316 if let Some(ref_val) = obj.get("$ref").and_then(|v| v.as_str()) {
317 let name =
319 ref_val.strip_prefix("#/definitions/").or_else(|| ref_val.strip_prefix("#/$defs/"));
320
321 if let Some(def_name) = name {
322 if let Some(def_schema) = definitions.get(def_name) {
323 *schema = def_schema.clone();
325 } else {
326 *schema = serde_json::json!({"type": "object"});
328 }
329 } else {
330 *schema = serde_json::json!({"type": "object"});
332 }
333
334 resolve_refs(schema, definitions, depth + 1);
336 } else {
337 resolve_refs_recurse(schema, definitions, depth);
339 }
340}
341
342fn resolve_refs_recurse(schema: &mut Value, definitions: &Map<String, Value>, depth: usize) {
344 let Some(obj) = schema.as_object_mut() else {
345 return;
346 };
347
348 if let Some(props) = obj.get_mut("properties")
350 && let Some(props_obj) = props.as_object_mut()
351 {
352 for value in props_obj.values_mut() {
353 resolve_refs(value, definitions, depth);
354 }
355 }
356
357 if let Some(items) = obj.get_mut("items") {
359 if items.is_object() {
360 resolve_refs(items, definitions, depth);
361 } else if let Some(arr) = items.as_array_mut() {
362 for item in arr.iter_mut() {
363 resolve_refs(item, definitions, depth);
364 }
365 }
366 }
367
368 if let Some(additional) = obj.get_mut("additionalProperties")
370 && additional.is_object()
371 {
372 resolve_refs(additional, definitions, depth);
373 }
374
375 for keyword in &["allOf", "anyOf", "oneOf"] {
377 if let Some(arr_val) = obj.get_mut(*keyword)
378 && let Some(arr) = arr_val.as_array_mut()
379 {
380 for sub in arr.iter_mut() {
381 resolve_refs(sub, definitions, depth);
382 }
383 }
384 }
385
386 if let Some(not_schema) = obj.get_mut("not")
388 && not_schema.is_object()
389 {
390 resolve_refs(not_schema, definitions, depth);
391 }
392
393 if let Some(pattern_props) = obj.get_mut("patternProperties")
395 && let Some(pp_obj) = pattern_props.as_object_mut()
396 {
397 for value in pp_obj.values_mut() {
398 resolve_refs(value, definitions, depth);
399 }
400 }
401
402 if let Some(prefix_items) = obj.get_mut("prefixItems")
404 && let Some(arr) = prefix_items.as_array_mut()
405 {
406 for item in arr.iter_mut() {
407 resolve_refs(item, definitions, depth);
408 }
409 }
410
411 for keyword in &["if", "then", "else"] {
413 if let Some(sub) = obj.get_mut(*keyword)
414 && sub.is_object()
415 {
416 resolve_refs(sub, definitions, depth);
417 }
418 }
419}
420
421fn recurse_into_subschemas(schema: &mut Value, transform: fn(&mut Value)) {
433 let Some(obj) = schema.as_object_mut() else {
434 return;
435 };
436
437 if let Some(props) = obj.get_mut("properties")
439 && let Some(props_obj) = props.as_object_mut()
440 {
441 for value in props_obj.values_mut() {
442 transform(value);
443 }
444 }
445
446 if let Some(items) = obj.get_mut("items") {
448 if items.is_object() {
449 transform(items);
450 } else if let Some(arr) = items.as_array_mut() {
451 for item in arr.iter_mut() {
452 transform(item);
453 }
454 }
455 }
456
457 if let Some(additional) = obj.get_mut("additionalProperties")
459 && additional.is_object()
460 {
461 transform(additional);
462 }
463
464 for keyword in &["allOf", "anyOf", "oneOf"] {
466 if let Some(arr_val) = obj.get_mut(*keyword)
467 && let Some(arr) = arr_val.as_array_mut()
468 {
469 for sub in arr.iter_mut() {
470 transform(sub);
471 }
472 }
473 }
474
475 if let Some(not_schema) = obj.get_mut("not")
477 && not_schema.is_object()
478 {
479 transform(not_schema);
480 }
481
482 if let Some(pattern_props) = obj.get_mut("patternProperties")
484 && let Some(pp_obj) = pattern_props.as_object_mut()
485 {
486 for value in pp_obj.values_mut() {
487 transform(value);
488 }
489 }
490
491 if let Some(prefix_items) = obj.get_mut("prefixItems")
493 && let Some(arr) = prefix_items.as_array_mut()
494 {
495 for item in arr.iter_mut() {
496 transform(item);
497 }
498 }
499
500 for keyword in &["if", "then", "else"] {
502 if let Some(sub) = obj.get_mut(*keyword)
503 && sub.is_object()
504 {
505 transform(sub);
506 }
507 }
508}
509
510fn recurse_into_subschemas_with_context<C: ?Sized>(
514 schema: &mut Value,
515 ctx: &C,
516 transform: fn(&mut Value, &C),
517) {
518 let Some(obj) = schema.as_object_mut() else {
519 return;
520 };
521
522 if let Some(props) = obj.get_mut("properties")
524 && let Some(props_obj) = props.as_object_mut()
525 {
526 for value in props_obj.values_mut() {
527 transform(value, ctx);
528 }
529 }
530
531 if let Some(items) = obj.get_mut("items") {
533 if items.is_object() {
534 transform(items, ctx);
535 } else if let Some(arr) = items.as_array_mut() {
536 for item in arr.iter_mut() {
537 transform(item, ctx);
538 }
539 }
540 }
541
542 if let Some(additional) = obj.get_mut("additionalProperties")
544 && additional.is_object()
545 {
546 transform(additional, ctx);
547 }
548
549 for keyword in &["allOf", "anyOf", "oneOf"] {
551 if let Some(arr_val) = obj.get_mut(*keyword)
552 && let Some(arr) = arr_val.as_array_mut()
553 {
554 for sub in arr.iter_mut() {
555 transform(sub, ctx);
556 }
557 }
558 }
559
560 if let Some(not_schema) = obj.get_mut("not")
562 && not_schema.is_object()
563 {
564 transform(not_schema, ctx);
565 }
566
567 if let Some(pattern_props) = obj.get_mut("patternProperties")
569 && let Some(pp_obj) = pattern_props.as_object_mut()
570 {
571 for value in pp_obj.values_mut() {
572 transform(value, ctx);
573 }
574 }
575
576 if let Some(prefix_items) = obj.get_mut("prefixItems")
578 && let Some(arr) = prefix_items.as_array_mut()
579 {
580 for item in arr.iter_mut() {
581 transform(item, ctx);
582 }
583 }
584
585 for keyword in &["if", "then", "else"] {
587 if let Some(sub) = obj.get_mut(*keyword)
588 && sub.is_object()
589 {
590 transform(sub, ctx);
591 }
592 }
593}
594
595pub fn collapse_combiners(schema: &mut Value) {
622 let Some(obj) = schema.as_object_mut() else {
623 return;
624 };
625
626 for keyword in &["anyOf", "oneOf"] {
627 if let Some(arr_val) = obj.remove(*keyword) {
628 if let Some(arr) = arr_val.as_array() {
629 let chosen = arr.iter().find(|sub| !is_null_schema(sub)).or_else(|| arr.first());
631
632 if let Some(chosen_schema) = chosen
633 && let Some(chosen_obj) = chosen_schema.as_object()
634 {
635 for (key, value) in chosen_obj {
637 obj.insert(key.clone(), value.clone());
638 }
639 }
640 }
641 break;
643 }
644 }
645
646 recurse_into_subschemas(schema, collapse_combiners);
647}
648
649pub fn merge_all_of(schema: &mut Value) {
677 let Some(obj) = schema.as_object_mut() else {
678 return;
679 };
680
681 if let Some(arr_val) = obj.remove("allOf")
682 && let Some(arr) = arr_val.as_array()
683 {
684 let mut merged_properties = Map::new();
685 let mut merged_required: Vec<Value> = Vec::new();
686 let mut merged_type: Option<Value> = None;
687 let mut other_fields = Map::new();
688
689 for sub in arr {
690 let Some(sub_obj) = sub.as_object() else {
691 continue;
692 };
693
694 for (key, value) in sub_obj {
695 match key.as_str() {
696 "properties" => {
697 if let Some(props) = value.as_object() {
698 for (pk, pv) in props {
699 merged_properties.insert(pk.clone(), pv.clone());
700 }
701 }
702 }
703 "required" => {
704 if let Some(req_arr) = value.as_array() {
705 for item in req_arr {
706 if !merged_required.contains(item) {
707 merged_required.push(item.clone());
708 }
709 }
710 }
711 }
712 "type" => {
713 if let Some(existing) = &merged_type {
714 if existing != value {
716 merged_type = Some(Value::String("object".to_string()));
717 }
718 } else {
719 merged_type = Some(value.clone());
720 }
721 }
722 _ => {
723 other_fields.insert(key.clone(), value.clone());
724 }
725 }
726 }
727 }
728
729 for (key, value) in other_fields {
731 obj.entry(key).or_insert(value);
732 }
733
734 if let Some(type_val) = merged_type {
736 obj.insert("type".to_string(), type_val);
737 }
738
739 if !merged_properties.is_empty() {
741 let existing_props =
742 obj.entry("properties").or_insert_with(|| Value::Object(Map::new()));
743 if let Some(existing_obj) = existing_props.as_object_mut() {
744 for (key, value) in merged_properties {
745 existing_obj.insert(key, value);
746 }
747 }
748 }
749
750 if !merged_required.is_empty() {
752 let existing_required =
753 obj.entry("required").or_insert_with(|| Value::Array(Vec::new()));
754 if let Some(existing_arr) = existing_required.as_array_mut() {
755 for item in merged_required {
756 if !existing_arr.contains(&item) {
757 existing_arr.push(item);
758 }
759 }
760 }
761 }
762 }
763
764 recurse_into_subschemas(schema, merge_all_of);
765}
766
767pub fn collapse_type_arrays(schema: &mut Value) {
789 if let Some(obj) = schema.as_object_mut()
790 && let Some(type_val) = obj.get("type").cloned()
791 && let Some(arr) = type_val.as_array()
792 {
793 let chosen = arr.iter().find(|t| t.as_str() != Some("null")).or_else(|| arr.first());
794
795 if let Some(chosen_type) = chosen {
796 obj.insert("type".to_string(), chosen_type.clone());
797 }
798 }
799 recurse_into_subschemas(schema, collapse_type_arrays);
800}
801
802pub fn enforce_nesting_depth(schema: &mut Value, max_depth: usize, current: usize) {
837 let Some(obj) = schema.as_object_mut() else {
838 return;
839 };
840
841 let is_object_schema = obj.get("type").and_then(|t| t.as_str()).is_some_and(|t| t == "object")
842 || obj.contains_key("properties");
843
844 if is_object_schema && current >= max_depth {
845 tracing::warn!(
846 depth = current,
847 max_depth,
848 "schema nesting depth exceeded, truncating to {{\"type\": \"object\"}}"
849 );
850 *schema = serde_json::json!({"type": "object"});
851 return;
852 }
853
854 let next_depth = if is_object_schema { current + 1 } else { current };
855
856 if let Some(props) = obj.get_mut("properties")
858 && let Some(props_obj) = props.as_object_mut()
859 {
860 for value in props_obj.values_mut() {
861 enforce_nesting_depth(value, max_depth, next_depth);
862 }
863 }
864
865 if let Some(items) = obj.get_mut("items") {
867 if items.is_object() {
868 enforce_nesting_depth(items, max_depth, next_depth);
869 } else if let Some(arr) = items.as_array_mut() {
870 for item in arr.iter_mut() {
871 enforce_nesting_depth(item, max_depth, next_depth);
872 }
873 }
874 }
875
876 if let Some(additional) = obj.get_mut("additionalProperties")
878 && additional.is_object()
879 {
880 enforce_nesting_depth(additional, max_depth, next_depth);
881 }
882
883 for keyword in &["allOf", "anyOf", "oneOf"] {
885 if let Some(arr_val) = obj.get_mut(*keyword)
886 && let Some(arr) = arr_val.as_array_mut()
887 {
888 for sub in arr.iter_mut() {
889 enforce_nesting_depth(sub, max_depth, next_depth);
890 }
891 }
892 }
893
894 if let Some(not_schema) = obj.get_mut("not")
896 && not_schema.is_object()
897 {
898 enforce_nesting_depth(not_schema, max_depth, next_depth);
899 }
900
901 if let Some(pattern_props) = obj.get_mut("patternProperties")
903 && let Some(pp_obj) = pattern_props.as_object_mut()
904 {
905 for value in pp_obj.values_mut() {
906 enforce_nesting_depth(value, max_depth, next_depth);
907 }
908 }
909}
910
911fn is_null_schema(schema: &Value) -> bool {
916 schema
917 .as_object()
918 .and_then(|obj| obj.get("type"))
919 .and_then(|t| t.as_str())
920 .is_some_and(|t| t == "null")
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926 use serde_json::json;
927
928 #[test]
931 fn test_strip_schema_keyword_top_level() {
932 let mut schema = json!({
933 "$schema": "http://json-schema.org/draft-07/schema#",
934 "type": "object"
935 });
936 strip_schema_keyword(&mut schema);
937 assert!(schema.get("$schema").is_none());
938 assert_eq!(schema["type"], "object");
939 }
940
941 #[test]
942 fn test_strip_schema_keyword_nested() {
943 let mut schema = json!({
944 "type": "object",
945 "properties": {
946 "child": {
947 "$schema": "http://json-schema.org/draft-07/schema#",
948 "type": "string"
949 }
950 }
951 });
952 strip_schema_keyword(&mut schema);
953 assert!(schema["properties"]["child"].get("$schema").is_none());
954 }
955
956 #[test]
957 fn test_strip_schema_keyword_no_op_when_absent() {
958 let mut schema = json!({"type": "string"});
959 let expected = schema.clone();
960 strip_schema_keyword(&mut schema);
961 assert_eq!(schema, expected);
962 }
963
964 #[test]
967 fn test_strip_conditional_keywords() {
968 let mut schema = json!({
969 "type": "object",
970 "if": { "properties": { "kind": { "const": "a" } } },
971 "then": { "required": ["extra"] },
972 "else": { "required": [] },
973 "properties": { "kind": { "type": "string" } }
974 });
975 strip_conditional_keywords(&mut schema);
976 assert!(schema.get("if").is_none());
977 assert!(schema.get("then").is_none());
978 assert!(schema.get("else").is_none());
979 assert!(schema.get("properties").is_some());
980 }
981
982 #[test]
983 fn test_strip_conditional_keywords_nested() {
984 let mut schema = json!({
985 "type": "object",
986 "properties": {
987 "child": {
988 "type": "object",
989 "if": { "const": true },
990 "then": { "type": "string" }
991 }
992 }
993 });
994 strip_conditional_keywords(&mut schema);
995 assert!(schema["properties"]["child"].get("if").is_none());
996 assert!(schema["properties"]["child"].get("then").is_none());
997 }
998
999 #[test]
1002 fn test_add_implicit_object_type() {
1003 let mut schema = json!({
1004 "properties": {
1005 "name": { "type": "string" }
1006 }
1007 });
1008 add_implicit_object_type(&mut schema);
1009 assert_eq!(schema["type"], "object");
1010 }
1011
1012 #[test]
1013 fn test_add_implicit_object_type_no_op_when_type_present() {
1014 let mut schema = json!({
1015 "type": "object",
1016 "properties": {
1017 "name": { "type": "string" }
1018 }
1019 });
1020 let expected = schema.clone();
1021 add_implicit_object_type(&mut schema);
1022 assert_eq!(schema, expected);
1023 }
1024
1025 #[test]
1026 fn test_add_implicit_object_type_nested() {
1027 let mut schema = json!({
1028 "type": "object",
1029 "properties": {
1030 "nested": {
1031 "properties": {
1032 "field": { "type": "number" }
1033 }
1034 }
1035 }
1036 });
1037 add_implicit_object_type(&mut schema);
1038 assert_eq!(schema["properties"]["nested"]["type"], "object");
1039 }
1040
1041 #[test]
1044 fn test_convert_const_to_enum() {
1045 let mut schema = json!({
1046 "type": "string",
1047 "const": "fixed"
1048 });
1049 convert_const_to_enum(&mut schema);
1050 assert!(schema.get("const").is_none());
1051 assert_eq!(schema["enum"], json!(["fixed"]));
1052 }
1053
1054 #[test]
1055 fn test_convert_const_to_enum_null() {
1056 let mut schema = json!({
1057 "const": null
1058 });
1059 convert_const_to_enum(&mut schema);
1060 assert!(schema.get("const").is_none());
1061 assert_eq!(schema["enum"], json!([null]));
1062 }
1063
1064 #[test]
1065 fn test_convert_const_to_enum_nested() {
1066 let mut schema = json!({
1067 "type": "object",
1068 "properties": {
1069 "status": {
1070 "type": "string",
1071 "const": "active"
1072 }
1073 }
1074 });
1075 convert_const_to_enum(&mut schema);
1076 assert_eq!(schema["properties"]["status"]["enum"], json!(["active"]));
1077 }
1078
1079 #[test]
1082 fn test_strip_unsupported_formats_removes_unsupported() {
1083 let mut schema = json!({
1084 "type": "string",
1085 "format": "hostname"
1086 });
1087 strip_unsupported_formats(&mut schema, &["date-time", "email"]);
1088 assert!(schema.get("format").is_none());
1089 }
1090
1091 #[test]
1092 fn test_strip_unsupported_formats_keeps_allowed() {
1093 let mut schema = json!({
1094 "type": "string",
1095 "format": "email"
1096 });
1097 strip_unsupported_formats(&mut schema, &["date-time", "email"]);
1098 assert_eq!(schema["format"], "email");
1099 }
1100
1101 #[test]
1102 fn test_strip_unsupported_formats_nested() {
1103 let mut schema = json!({
1104 "type": "object",
1105 "properties": {
1106 "created": { "type": "string", "format": "date-time" },
1107 "hostname": { "type": "string", "format": "hostname" }
1108 }
1109 });
1110 strip_unsupported_formats(&mut schema, &["date-time"]);
1111 assert_eq!(schema["properties"]["created"]["format"], "date-time");
1112 assert!(schema["properties"]["hostname"].get("format").is_none());
1113 }
1114
1115 #[test]
1118 fn test_truncate_tool_name_short() {
1119 let result = truncate_tool_name("short_name", 64);
1120 assert_eq!(result, "short_name");
1121 assert!(matches!(result, Cow::Borrowed(_)));
1122 }
1123
1124 #[test]
1125 fn test_truncate_tool_name_exact_boundary() {
1126 let name = "a".repeat(64);
1127 let result = truncate_tool_name(&name, 64);
1128 assert_eq!(result.len(), 64);
1129 assert!(matches!(result, Cow::Borrowed(_)));
1130 }
1131
1132 #[test]
1133 fn test_truncate_tool_name_over_limit() {
1134 let name = "a".repeat(100);
1135 let result = truncate_tool_name(&name, 64);
1136 assert_eq!(result.len(), 64);
1137 assert!(matches!(result, Cow::Owned(_)));
1138 }
1139
1140 #[test]
1141 fn test_truncate_tool_name_multibyte_boundary() {
1142 let name = "a".repeat(63) + "é"; let result = truncate_tool_name(&name, 64);
1145 assert_eq!(result.len(), 63);
1147 assert!(result.is_char_boundary(result.len()));
1148 }
1149
1150 #[test]
1151 fn test_truncate_tool_name_emoji() {
1152 let name = "a".repeat(62) + "🎯"; let result = truncate_tool_name(&name, 64);
1155 assert_eq!(result.len(), 62);
1157 }
1158
1159 #[test]
1160 fn test_truncate_tool_name_empty() {
1161 let result = truncate_tool_name("", 64);
1162 assert_eq!(result, "");
1163 assert!(matches!(result, Cow::Borrowed(_)));
1164 }
1165
1166 #[test]
1169 fn test_strip_null_from_enum() {
1170 let mut schema = json!({
1171 "type": "string",
1172 "enum": ["a", null, "b"]
1173 });
1174 strip_null_from_enum(&mut schema);
1175 assert_eq!(schema["enum"], json!(["a", "b"]));
1176 }
1177
1178 #[test]
1179 fn test_strip_null_from_enum_all_null() {
1180 let mut schema = json!({
1181 "type": "string",
1182 "enum": [null]
1183 });
1184 strip_null_from_enum(&mut schema);
1185 assert!(schema.get("enum").is_none());
1186 }
1187
1188 #[test]
1189 fn test_strip_null_from_enum_no_null() {
1190 let mut schema = json!({
1191 "type": "string",
1192 "enum": ["a", "b"]
1193 });
1194 let expected = schema.clone();
1195 strip_null_from_enum(&mut schema);
1196 assert_eq!(schema, expected);
1197 }
1198
1199 #[test]
1200 fn test_strip_null_from_enum_nested() {
1201 let mut schema = json!({
1202 "type": "object",
1203 "properties": {
1204 "status": {
1205 "type": "string",
1206 "enum": ["active", null, "inactive"]
1207 }
1208 }
1209 });
1210 strip_null_from_enum(&mut schema);
1211 assert_eq!(schema["properties"]["status"]["enum"], json!(["active", "inactive"]));
1212 }
1213
1214 #[test]
1217 fn test_recursion_into_any_of() {
1218 let mut schema = json!({
1219 "anyOf": [
1220 { "$schema": "draft-07", "type": "string" },
1221 { "$schema": "draft-07", "type": "number" }
1222 ]
1223 });
1224 strip_schema_keyword(&mut schema);
1225 assert!(schema["anyOf"][0].get("$schema").is_none());
1226 assert!(schema["anyOf"][1].get("$schema").is_none());
1227 }
1228
1229 #[test]
1230 fn test_recursion_into_all_of() {
1231 let mut schema = json!({
1232 "allOf": [
1233 { "properties": { "a": { "type": "string" } } },
1234 { "properties": { "b": { "type": "number" } } }
1235 ]
1236 });
1237 add_implicit_object_type(&mut schema);
1238 assert_eq!(schema["allOf"][0]["type"], "object");
1239 assert_eq!(schema["allOf"][1]["type"], "object");
1240 }
1241
1242 #[test]
1243 fn test_recursion_into_items() {
1244 let mut schema = json!({
1245 "type": "array",
1246 "items": {
1247 "$schema": "draft-07",
1248 "type": "string",
1249 "format": "hostname"
1250 }
1251 });
1252 strip_schema_keyword(&mut schema);
1253 strip_unsupported_formats(&mut schema, &["date-time"]);
1254 assert!(schema["items"].get("$schema").is_none());
1255 assert!(schema["items"].get("format").is_none());
1256 }
1257
1258 #[test]
1259 fn test_recursion_into_additional_properties() {
1260 let mut schema = json!({
1261 "type": "object",
1262 "additionalProperties": {
1263 "$schema": "draft-07",
1264 "type": "string"
1265 }
1266 });
1267 strip_schema_keyword(&mut schema);
1268 assert!(schema["additionalProperties"].get("$schema").is_none());
1269 }
1270
1271 #[test]
1272 fn test_recursion_into_not() {
1273 let mut schema = json!({
1274 "not": {
1275 "$schema": "draft-07",
1276 "type": "null"
1277 }
1278 });
1279 strip_schema_keyword(&mut schema);
1280 assert!(schema["not"].get("$schema").is_none());
1281 }
1282
1283 #[test]
1284 fn test_deeply_nested_recursion() {
1285 let mut schema = json!({
1286 "type": "object",
1287 "properties": {
1288 "level1": {
1289 "type": "object",
1290 "properties": {
1291 "level2": {
1292 "type": "object",
1293 "properties": {
1294 "level3": {
1295 "$schema": "draft-07",
1296 "type": "string",
1297 "const": "deep"
1298 }
1299 }
1300 }
1301 }
1302 }
1303 }
1304 });
1305 strip_schema_keyword(&mut schema);
1306 convert_const_to_enum(&mut schema);
1307 let deep = &schema["properties"]["level1"]["properties"]["level2"]["properties"]["level3"];
1308 assert!(deep.get("$schema").is_none());
1309 assert_eq!(deep["enum"], json!(["deep"]));
1310 }
1311
1312 #[test]
1315 fn test_resolve_refs_simple_definitions() {
1316 let mut defs = Map::new();
1317 defs.insert(
1318 "Address".to_string(),
1319 json!({"type": "object", "properties": {"street": {"type": "string"}}}),
1320 );
1321
1322 let mut schema = json!({
1323 "type": "object",
1324 "properties": {
1325 "home": { "$ref": "#/definitions/Address" }
1326 }
1327 });
1328
1329 resolve_refs(&mut schema, &defs, 0);
1330 assert_eq!(schema["properties"]["home"]["type"], "object");
1331 assert!(schema["properties"]["home"].get("$ref").is_none());
1332 assert_eq!(schema["properties"]["home"]["properties"]["street"]["type"], "string");
1333 }
1334
1335 #[test]
1336 fn test_resolve_refs_simple_defs_format() {
1337 let mut defs = Map::new();
1338 defs.insert("Name".to_string(), json!({"type": "string", "minLength": 1}));
1339
1340 let mut schema = json!({
1341 "type": "object",
1342 "properties": {
1343 "name": { "$ref": "#/$defs/Name" }
1344 }
1345 });
1346
1347 resolve_refs(&mut schema, &defs, 0);
1348 assert_eq!(schema["properties"]["name"]["type"], "string");
1349 assert_eq!(schema["properties"]["name"]["minLength"], 1);
1350 assert!(schema["properties"]["name"].get("$ref").is_none());
1351 }
1352
1353 #[test]
1354 fn test_resolve_refs_nested_refs() {
1355 let mut defs = Map::new();
1356 defs.insert("Inner".to_string(), json!({"type": "string"}));
1357 defs.insert(
1358 "Outer".to_string(),
1359 json!({
1360 "type": "object",
1361 "properties": {
1362 "value": { "$ref": "#/definitions/Inner" }
1363 }
1364 }),
1365 );
1366
1367 let mut schema = json!({
1368 "type": "object",
1369 "properties": {
1370 "wrapper": { "$ref": "#/definitions/Outer" }
1371 }
1372 });
1373
1374 resolve_refs(&mut schema, &defs, 0);
1375 assert_eq!(schema["properties"]["wrapper"]["type"], "object");
1377 assert_eq!(schema["properties"]["wrapper"]["properties"]["value"]["type"], "string");
1379 assert!(schema["properties"]["wrapper"]["properties"]["value"].get("$ref").is_none());
1380 }
1381
1382 #[test]
1383 fn test_resolve_refs_unresolvable_ref() {
1384 let defs = Map::new(); let mut schema = json!({
1387 "type": "object",
1388 "properties": {
1389 "missing": { "$ref": "#/definitions/DoesNotExist" }
1390 }
1391 });
1392
1393 resolve_refs(&mut schema, &defs, 0);
1394 assert_eq!(schema["properties"]["missing"], json!({"type": "object"}));
1396 }
1397
1398 #[test]
1399 fn test_resolve_refs_unsupported_ref_format() {
1400 let defs = Map::new();
1401
1402 let mut schema = json!({
1403 "type": "object",
1404 "properties": {
1405 "external": { "$ref": "https://example.com/schema.json" }
1406 }
1407 });
1408
1409 resolve_refs(&mut schema, &defs, 0);
1410 assert_eq!(schema["properties"]["external"], json!({"type": "object"}));
1412 }
1413
1414 #[test]
1415 fn test_resolve_refs_circular_self_reference() {
1416 let mut defs = Map::new();
1417 defs.insert(
1418 "Node".to_string(),
1419 json!({
1420 "type": "object",
1421 "properties": {
1422 "child": { "$ref": "#/definitions/Node" }
1423 }
1424 }),
1425 );
1426
1427 let mut schema = json!({ "$ref": "#/definitions/Node" });
1428
1429 resolve_refs(&mut schema, &defs, 0);
1430 assert_eq!(schema["type"], "object");
1432 let mut current = &schema;
1435 let mut found_termination = false;
1436 for _ in 0..15 {
1437 if let Some(child) = current.get("properties").and_then(|p| p.get("child")) {
1438 if child == &json!({"type": "object"}) {
1439 found_termination = true;
1440 break;
1441 }
1442 current = child;
1443 } else {
1444 found_termination = true;
1445 break;
1446 }
1447 }
1448 assert!(found_termination, "circular ref chain should terminate within depth limit");
1449 }
1450
1451 #[test]
1452 fn test_resolve_refs_mutual_circular_reference() {
1453 let mut defs = Map::new();
1454 defs.insert(
1455 "A".to_string(),
1456 json!({
1457 "type": "object",
1458 "properties": {
1459 "b": { "$ref": "#/definitions/B" }
1460 }
1461 }),
1462 );
1463 defs.insert(
1464 "B".to_string(),
1465 json!({
1466 "type": "object",
1467 "properties": {
1468 "a": { "$ref": "#/definitions/A" }
1469 }
1470 }),
1471 );
1472
1473 let mut schema = json!({ "$ref": "#/definitions/A" });
1474
1475 resolve_refs(&mut schema, &defs, 0);
1476 assert_eq!(schema["type"], "object");
1478 }
1479
1480 #[test]
1481 fn test_resolve_refs_depth_limit_exact() {
1482 let mut defs = Map::new();
1484 defs.insert("Foo".to_string(), json!({"type": "number"}));
1485
1486 let mut schema = json!({ "$ref": "#/definitions/Foo" });
1487
1488 resolve_refs(&mut schema, &defs, 11);
1489 assert_eq!(schema, json!({"type": "object"}));
1491 }
1492
1493 #[test]
1494 fn test_resolve_refs_depth_limit_no_ref_passthrough() {
1495 let defs = Map::new();
1497 let mut schema = json!({"type": "string", "minLength": 5});
1498 let expected = schema.clone();
1499
1500 resolve_refs(&mut schema, &defs, 11);
1501 assert_eq!(schema, expected);
1502 }
1503
1504 #[test]
1505 fn test_resolve_refs_depth_10_still_resolves() {
1506 let mut defs = Map::new();
1507 defs.insert("Foo".to_string(), json!({"type": "number"}));
1508
1509 let mut schema = json!({ "$ref": "#/definitions/Foo" });
1510
1511 resolve_refs(&mut schema, &defs, 10);
1513 assert_eq!(schema, json!({"type": "number"}));
1514 }
1515
1516 #[test]
1517 fn test_resolve_refs_in_array_items() {
1518 let mut defs = Map::new();
1519 defs.insert("Item".to_string(), json!({"type": "string"}));
1520
1521 let mut schema = json!({
1522 "type": "array",
1523 "items": { "$ref": "#/definitions/Item" }
1524 });
1525
1526 resolve_refs(&mut schema, &defs, 0);
1527 assert_eq!(schema["items"]["type"], "string");
1528 assert!(schema["items"].get("$ref").is_none());
1529 }
1530
1531 #[test]
1532 fn test_resolve_refs_in_any_of() {
1533 let mut defs = Map::new();
1534 defs.insert("Str".to_string(), json!({"type": "string"}));
1535 defs.insert("Num".to_string(), json!({"type": "number"}));
1536
1537 let mut schema = json!({
1538 "anyOf": [
1539 { "$ref": "#/definitions/Str" },
1540 { "$ref": "#/$defs/Num" }
1541 ]
1542 });
1543
1544 resolve_refs(&mut schema, &defs, 0);
1545 assert_eq!(schema["anyOf"][0], json!({"type": "string"}));
1546 assert_eq!(schema["anyOf"][1], json!({"type": "number"}));
1547 }
1548
1549 #[test]
1550 fn test_resolve_refs_no_ref_passthrough() {
1551 let defs = Map::new();
1552 let mut schema = json!({
1553 "type": "object",
1554 "properties": {
1555 "name": { "type": "string" },
1556 "age": { "type": "integer" }
1557 }
1558 });
1559 let expected = schema.clone();
1560
1561 resolve_refs(&mut schema, &defs, 0);
1562 assert_eq!(schema, expected);
1563 }
1564
1565 #[test]
1566 fn test_resolve_refs_both_definitions_and_defs() {
1567 let mut defs = Map::new();
1569 defs.insert("FromDefs".to_string(), json!({"type": "boolean"}));
1570 defs.insert("FromDefinitions".to_string(), json!({"type": "integer"}));
1571
1572 let mut schema = json!({
1573 "type": "object",
1574 "properties": {
1575 "a": { "$ref": "#/$defs/FromDefs" },
1576 "b": { "$ref": "#/definitions/FromDefinitions" }
1577 }
1578 });
1579
1580 resolve_refs(&mut schema, &defs, 0);
1581 assert_eq!(schema["properties"]["a"], json!({"type": "boolean"}));
1582 assert_eq!(schema["properties"]["b"], json!({"type": "integer"}));
1583 }
1584
1585 #[test]
1588 fn test_collapse_combiners_any_of_picks_first_non_null() {
1589 let mut schema = json!({
1590 "anyOf": [
1591 {"type": "null"},
1592 {"type": "string", "minLength": 1}
1593 ]
1594 });
1595 collapse_combiners(&mut schema);
1596 assert_eq!(schema["type"], "string");
1597 assert_eq!(schema["minLength"], 1);
1598 assert!(schema.get("anyOf").is_none());
1599 }
1600
1601 #[test]
1602 fn test_collapse_combiners_one_of_picks_first_non_null() {
1603 let mut schema = json!({
1604 "oneOf": [
1605 {"type": "null"},
1606 {"type": "integer", "minimum": 0}
1607 ]
1608 });
1609 collapse_combiners(&mut schema);
1610 assert_eq!(schema["type"], "integer");
1611 assert_eq!(schema["minimum"], 0);
1612 assert!(schema.get("oneOf").is_none());
1613 }
1614
1615 #[test]
1616 fn test_collapse_combiners_all_null_uses_first() {
1617 let mut schema = json!({
1618 "anyOf": [
1619 {"type": "null"},
1620 {"type": "null"}
1621 ]
1622 });
1623 collapse_combiners(&mut schema);
1624 assert_eq!(schema["type"], "null");
1625 assert!(schema.get("anyOf").is_none());
1626 }
1627
1628 #[test]
1629 fn test_collapse_combiners_no_null() {
1630 let mut schema = json!({
1631 "anyOf": [
1632 {"type": "string"},
1633 {"type": "number"}
1634 ]
1635 });
1636 collapse_combiners(&mut schema);
1637 assert_eq!(schema["type"], "string");
1638 assert!(schema.get("anyOf").is_none());
1639 }
1640
1641 #[test]
1642 fn test_collapse_combiners_nested() {
1643 let mut schema = json!({
1644 "type": "object",
1645 "properties": {
1646 "field": {
1647 "oneOf": [
1648 {"type": "null"},
1649 {"type": "boolean"}
1650 ]
1651 }
1652 }
1653 });
1654 collapse_combiners(&mut schema);
1655 assert_eq!(schema["properties"]["field"]["type"], "boolean");
1656 assert!(schema["properties"]["field"].get("oneOf").is_none());
1657 }
1658
1659 #[test]
1660 fn test_collapse_combiners_preserves_existing_fields() {
1661 let mut schema = json!({
1662 "description": "A nullable string",
1663 "anyOf": [
1664 {"type": "null"},
1665 {"type": "string", "maxLength": 100}
1666 ]
1667 });
1668 collapse_combiners(&mut schema);
1669 assert_eq!(schema["description"], "A nullable string");
1670 assert_eq!(schema["type"], "string");
1671 assert_eq!(schema["maxLength"], 100);
1672 }
1673
1674 #[test]
1677 fn test_merge_all_of_combines_properties() {
1678 let mut schema = json!({
1679 "allOf": [
1680 {"type": "object", "properties": {"a": {"type": "string"}}},
1681 {"properties": {"b": {"type": "number"}}}
1682 ]
1683 });
1684 merge_all_of(&mut schema);
1685 assert!(schema.get("allOf").is_none());
1686 assert_eq!(schema["properties"]["a"]["type"], "string");
1687 assert_eq!(schema["properties"]["b"]["type"], "number");
1688 }
1689
1690 #[test]
1691 fn test_merge_all_of_combines_required() {
1692 let mut schema = json!({
1693 "allOf": [
1694 {"required": ["a", "b"]},
1695 {"required": ["b", "c"]}
1696 ]
1697 });
1698 merge_all_of(&mut schema);
1699 let required = schema["required"].as_array().unwrap();
1700 assert!(required.contains(&json!("a")));
1701 assert!(required.contains(&json!("b")));
1702 assert!(required.contains(&json!("c")));
1703 assert_eq!(required.len(), 3);
1705 }
1706
1707 #[test]
1708 fn test_merge_all_of_conflicting_type_prefers_object() {
1709 let mut schema = json!({
1710 "allOf": [
1711 {"type": "string"},
1712 {"type": "number"}
1713 ]
1714 });
1715 merge_all_of(&mut schema);
1716 assert_eq!(schema["type"], "object");
1717 }
1718
1719 #[test]
1720 fn test_merge_all_of_same_type_no_conflict() {
1721 let mut schema = json!({
1722 "allOf": [
1723 {"type": "object", "properties": {"a": {"type": "string"}}},
1724 {"type": "object", "properties": {"b": {"type": "number"}}}
1725 ]
1726 });
1727 merge_all_of(&mut schema);
1728 assert_eq!(schema["type"], "object");
1729 }
1730
1731 #[test]
1732 fn test_merge_all_of_nested() {
1733 let mut schema = json!({
1734 "type": "object",
1735 "properties": {
1736 "nested": {
1737 "allOf": [
1738 {"properties": {"x": {"type": "integer"}}},
1739 {"properties": {"y": {"type": "integer"}}}
1740 ]
1741 }
1742 }
1743 });
1744 merge_all_of(&mut schema);
1745 assert!(schema["properties"]["nested"].get("allOf").is_none());
1746 assert_eq!(schema["properties"]["nested"]["properties"]["x"]["type"], "integer");
1747 assert_eq!(schema["properties"]["nested"]["properties"]["y"]["type"], "integer");
1748 }
1749
1750 #[test]
1751 fn test_merge_all_of_other_fields() {
1752 let mut schema = json!({
1753 "allOf": [
1754 {"type": "object", "description": "First"},
1755 {"title": "Second"}
1756 ]
1757 });
1758 merge_all_of(&mut schema);
1759 assert_eq!(schema["description"], "First");
1760 assert_eq!(schema["title"], "Second");
1761 }
1762
1763 #[test]
1766 fn test_collapse_type_arrays_string_null() {
1767 let mut schema = json!({"type": ["string", "null"]});
1768 collapse_type_arrays(&mut schema);
1769 assert_eq!(schema["type"], "string");
1770 }
1771
1772 #[test]
1773 fn test_collapse_type_arrays_null_first() {
1774 let mut schema = json!({"type": ["null", "integer"]});
1775 collapse_type_arrays(&mut schema);
1776 assert_eq!(schema["type"], "integer");
1777 }
1778
1779 #[test]
1780 fn test_collapse_type_arrays_all_null() {
1781 let mut schema = json!({"type": ["null"]});
1782 collapse_type_arrays(&mut schema);
1783 assert_eq!(schema["type"], "null");
1784 }
1785
1786 #[test]
1787 fn test_collapse_type_arrays_single_non_null() {
1788 let mut schema = json!({"type": ["boolean"]});
1789 collapse_type_arrays(&mut schema);
1790 assert_eq!(schema["type"], "boolean");
1791 }
1792
1793 #[test]
1794 fn test_collapse_type_arrays_already_string() {
1795 let mut schema = json!({"type": "string"});
1796 let expected = schema.clone();
1797 collapse_type_arrays(&mut schema);
1798 assert_eq!(schema, expected);
1799 }
1800
1801 #[test]
1802 fn test_collapse_type_arrays_nested() {
1803 let mut schema = json!({
1804 "type": "object",
1805 "properties": {
1806 "field": {"type": ["number", "null"]}
1807 }
1808 });
1809 collapse_type_arrays(&mut schema);
1810 assert_eq!(schema["properties"]["field"]["type"], "number");
1811 }
1812
1813 #[test]
1814 fn test_collapse_type_arrays_multiple_non_null() {
1815 let mut schema = json!({"type": ["string", "number", "null"]});
1816 collapse_type_arrays(&mut schema);
1817 assert_eq!(schema["type"], "string");
1819 }
1820
1821 #[test]
1824 fn test_enforce_nesting_depth_within_limit() {
1825 let mut schema = json!({
1826 "type": "object",
1827 "properties": {
1828 "name": {"type": "string"}
1829 }
1830 });
1831 let expected = schema.clone();
1832 enforce_nesting_depth(&mut schema, 5, 0);
1833 assert_eq!(schema, expected);
1834 }
1835
1836 #[test]
1837 fn test_enforce_nesting_depth_at_limit() {
1838 let mut schema = json!({
1839 "type": "object",
1840 "properties": {
1841 "deep": {
1842 "type": "object",
1843 "properties": {
1844 "deeper": {"type": "string"}
1845 }
1846 }
1847 }
1848 });
1849 enforce_nesting_depth(&mut schema, 1, 0);
1850 assert_eq!(schema["properties"]["deep"], json!({"type": "object"}));
1853 }
1854
1855 #[test]
1856 fn test_enforce_nesting_depth_exceeds_limit() {
1857 let mut schema = json!({
1858 "type": "object",
1859 "properties": {
1860 "level1": {
1861 "type": "object",
1862 "properties": {
1863 "level2": {
1864 "type": "object",
1865 "properties": {
1866 "level3": {"type": "string"}
1867 }
1868 }
1869 }
1870 }
1871 }
1872 });
1873 enforce_nesting_depth(&mut schema, 2, 0);
1874 assert_eq!(
1876 schema["properties"]["level1"]["properties"]["level2"],
1877 json!({"type": "object"})
1878 );
1879 }
1880
1881 #[test]
1882 fn test_enforce_nesting_depth_non_object_not_counted() {
1883 let mut schema = json!({
1884 "type": "object",
1885 "properties": {
1886 "arr": {
1887 "type": "array",
1888 "items": {
1889 "type": "object",
1890 "properties": {
1891 "name": {"type": "string"}
1892 }
1893 }
1894 }
1895 }
1896 });
1897 enforce_nesting_depth(&mut schema, 2, 0);
1898 assert_eq!(schema["properties"]["arr"]["items"]["properties"]["name"]["type"], "string");
1901 }
1902
1903 #[test]
1904 fn test_enforce_nesting_depth_gemini_5_levels() {
1905 let mut schema = json!({
1907 "type": "object",
1908 "properties": {
1909 "l1": {
1910 "type": "object",
1911 "properties": {
1912 "l2": {
1913 "type": "object",
1914 "properties": {
1915 "l3": {
1916 "type": "object",
1917 "properties": {
1918 "l4": {
1919 "type": "object",
1920 "properties": {
1921 "l5": {
1922 "type": "object",
1923 "properties": {
1924 "deep": {"type": "string"}
1925 }
1926 }
1927 }
1928 }
1929 }
1930 }
1931 }
1932 }
1933 }
1934 }
1935 }
1936 });
1937 enforce_nesting_depth(&mut schema, 5, 0);
1938 assert_eq!(
1940 schema["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]["l4"]
1941 ["properties"]["l5"],
1942 json!({"type": "object"})
1943 );
1944 assert!(
1946 schema["properties"]["l1"]["properties"]["l2"]["properties"]["l3"]["properties"]["l4"]
1947 .get("properties")
1948 .is_some()
1949 );
1950 }
1951
1952 #[test]
1953 fn test_enforce_nesting_depth_zero_truncates_root_object() {
1954 let mut schema = json!({
1955 "type": "object",
1956 "properties": {"a": {"type": "string"}}
1957 });
1958 enforce_nesting_depth(&mut schema, 0, 0);
1959 assert_eq!(schema, json!({"type": "object"}));
1960 }
1961
1962 #[test]
1965 fn test_is_null_schema_true() {
1966 assert!(is_null_schema(&json!({"type": "null"})));
1967 }
1968
1969 #[test]
1970 fn test_is_null_schema_false_for_string() {
1971 assert!(!is_null_schema(&json!({"type": "string"})));
1972 }
1973
1974 #[test]
1975 fn test_is_null_schema_false_for_non_object() {
1976 assert!(!is_null_schema(&json!("null")));
1977 assert!(!is_null_schema(&Value::Null));
1978 }
1979}