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