1#[cfg_attr(feature = "trace", crustrace::omni)]
2mod innermod {
3 use crate::genson_rs::{build_json_schema, get_builder, BuildConfig};
4 use serde::de::Error as DeError;
5 use serde::{Deserialize, Serialize};
6 use serde_json::Value;
7 use std::borrow::Cow;
8 use std::panic::{self, AssertUnwindSafe};
9
10 const MAX_JSON_ERROR_LENGTH: usize = 100;
12
13 #[derive(Debug, Clone, Serialize, Deserialize)]
14 pub struct SchemaInferenceConfig {
15 pub ignore_outer_array: bool,
17 pub delimiter: Option<u8>,
19 pub schema_uri: Option<String>,
21 pub map_threshold: usize,
23 pub map_max_required_keys: Option<usize>,
26 pub unify_maps: bool,
28 pub force_field_types: std::collections::HashMap<String, String>,
30 pub wrap_root: Option<String>,
34 #[cfg(feature = "avro")]
36 pub avro: bool,
37 }
38
39 impl Default for SchemaInferenceConfig {
40 fn default() -> Self {
41 Self {
42 ignore_outer_array: true,
43 delimiter: None,
44 schema_uri: Some("AUTO".to_string()),
45 map_threshold: 20,
46 map_max_required_keys: None,
47 unify_maps: false,
48 force_field_types: std::collections::HashMap::new(),
49 wrap_root: None,
50 #[cfg(feature = "avro")]
51 avro: false,
52 }
53 }
54 }
55
56 #[derive(Debug, Clone, Serialize, Deserialize)]
57 pub struct SchemaInferenceResult {
58 pub schema: Value,
59 pub processed_count: usize,
60 }
61
62 #[cfg(feature = "avro")]
63 impl SchemaInferenceResult {
64 pub fn to_avro_schema(
65 &self,
66 namespace: &str,
67 utility_namespace: Option<&str>,
68 base_uri: Option<&str>,
69 split_top_level: bool,
70 ) -> Value {
71 avrotize::converter::jsons_to_avro(
72 &self.schema,
73 namespace,
74 utility_namespace.unwrap_or(""),
75 base_uri.unwrap_or("genson-core"),
76 split_top_level,
77 )
78 }
79 }
80
81 fn validate_json(s: &str) -> Result<(), serde_json::Error> {
82 let mut de = serde_json::Deserializer::from_str(s);
83 serde::de::IgnoredAny::deserialize(&mut de)?; de.end()
85 }
86
87 fn validate_ndjson(s: &str) -> Result<(), serde_json::Error> {
88 for line in s.lines() {
89 let trimmed = line.trim();
90 if trimmed.is_empty() {
91 continue;
92 }
93 validate_json(trimmed)?; }
95 Ok(())
96 }
97
98 fn check_unifiable_schemas(schemas: &[Value]) -> Option<Value> {
120 if schemas.is_empty() {
121 return None;
122 }
123
124 if !schemas
126 .iter()
127 .all(|s| s.get("type") == Some(&Value::String("object".into())))
128 {
129 return None;
130 }
131
132 let mut all_fields = ordermap::OrderMap::new();
133 let mut field_counts = std::collections::HashMap::new();
134
135 for schema in schemas {
137 if let Some(Value::Object(props)) = schema.get("properties") {
138 for (field_name, field_schema) in props {
139 *field_counts.entry(field_name.clone()).or_insert(0) += 1;
140
141 match all_fields.entry(field_name.clone()) {
142 ordermap::map::Entry::Vacant(e) => {
143 e.insert(field_schema.clone());
144 }
145 ordermap::map::Entry::Occupied(mut e) => {
146 let existing = e.get().clone();
147 if existing != *field_schema {
148 if existing.get("type") == Some(&Value::String("object".into()))
150 && field_schema.get("type")
151 == Some(&Value::String("object".into()))
152 {
153 if let Some(unified) = check_unifiable_schemas(&[
154 existing.clone(),
155 field_schema.clone(),
156 ]) {
157 e.insert(unified);
158 } else {
159 return None;
160 }
161 } else {
162 return None; }
164 }
165 }
166 }
167 }
168 }
169 }
170
171 let total_schemas = schemas.len();
172 let mut unified_properties = serde_json::Map::new();
173
174 for (field_name, field_type) in &all_fields {
176 let count = field_counts.get(field_name).unwrap_or(&0);
177 if *count == total_schemas {
178 unified_properties.insert(field_name.clone(), field_type.clone());
179 }
180 }
181
182 for (field_name, field_type) in &all_fields {
184 let count = field_counts.get(field_name).unwrap_or(&0);
185 if *count < total_schemas {
186 unified_properties
187 .insert(field_name.clone(), serde_json::json!(["null", field_type]));
188 }
189 }
190
191 Some(serde_json::json!({
192 "type": "object",
193 "properties": unified_properties
194 }))
195 }
196
197 fn rewrite_objects(
211 schema: &mut Value,
212 field_name: Option<&str>,
213 config: &SchemaInferenceConfig,
214 ) {
215 if let Value::Object(obj) = schema {
216 if let Some(name) = field_name {
218 if let Some(forced) = config.force_field_types.get(name) {
219 match forced.as_str() {
220 "map" => {
221 obj.remove("properties");
222 obj.remove("required");
223 obj.insert(
224 "additionalProperties".to_string(),
225 serde_json::json!({ "type": "string" }),
226 );
227 return; }
229 "record" => {
230 if let Some(props) =
231 obj.get_mut("properties").and_then(|p| p.as_object_mut())
232 {
233 for (k, v) in props {
234 rewrite_objects(v, Some(k), config);
235 }
236 }
237 if let Some(items) = obj.get_mut("items") {
238 rewrite_objects(items, None, config);
239 }
240 return;
241 }
242 _ => {}
243 }
244 }
245 }
246
247 if let Some(props) = obj.get("properties").and_then(|p| p.as_object()) {
249 let key_count = props.len(); let above_threshold = key_count >= config.map_threshold;
251
252 let child_schemas: Vec<Value> = props.values().cloned().collect();
254
255 if above_threshold {
259 if let Some(first) = child_schemas.first() {
260 if first.get("type") == Some(&Value::String("object".into()))
261 && first.get("properties").is_some()
262 && child_schemas.len() > 1
263 {
264 let all_same = child_schemas.iter().all(|other| other == first);
265 if all_same {
266 obj.remove("properties");
267 obj.remove("required");
268 obj.insert("additionalProperties".to_string(), first.clone());
269 return;
270 }
271 }
272 }
273 }
274
275 let required_key_count = obj
277 .get("required")
278 .and_then(|r| r.as_array())
279 .map(|r| r.len())
280 .unwrap_or(0);
281
282 let mut unified_schema: Option<Value> = None;
284 if let Some(first_schema) = props.values().next() {
285 if props.values().all(|schema| schema == first_schema) {
286 unified_schema = Some(first_schema.clone());
287 } else if config.unify_maps {
288 if child_schemas
290 .iter()
291 .all(|s| s.get("type") == Some(&Value::String("array".into())))
292 {
293 let mut item_schemas = Vec::with_capacity(child_schemas.len());
295 let mut all_items_ok = true;
296 for s in &child_schemas {
297 if let Some(items) = s.get("items") {
298 item_schemas.push(items.clone());
299 } else {
300 all_items_ok = false;
301 break;
302 }
303 }
304 if all_items_ok {
305 if let Some(unified_items) = check_unifiable_schemas(&item_schemas)
306 {
307 unified_schema = Some(serde_json::json!({
308 "type": "array",
309 "items": unified_items
310 }));
311 }
312 }
313 } else {
314 unified_schema = check_unifiable_schemas(&child_schemas);
315 }
316 }
317 }
318
319 let should_be_map = if above_threshold && unified_schema.is_some() {
321 if let Some(max_required) = config.map_max_required_keys {
322 required_key_count <= max_required
323 } else {
324 true
325 }
326 } else {
327 false
328 };
329
330 if should_be_map {
331 if let Some(schema) = unified_schema {
332 obj.remove("properties");
333 obj.remove("required");
334 obj.insert("type".to_string(), Value::String("object".to_string()));
335 obj.insert("additionalProperties".to_string(), schema);
336 return;
337 }
338 }
339 }
340
341 if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
343 for (k, v) in props {
344 rewrite_objects(v, Some(k), config);
345 }
346 }
347 if let Some(items) = obj.get_mut("items") {
348 rewrite_objects(items, None, config);
349 }
350 for v in obj.values_mut() {
351 rewrite_objects(v, None, config);
352 }
353 } else if let Value::Array(arr) = schema {
354 for v in arr {
355 rewrite_objects(v, None, config);
356 }
357 }
358 }
359
360 pub fn reorder_unions(schema: &mut Value) {
364 match schema {
365 Value::Object(obj) => {
366 if let Some(Value::Array(types)) = obj.get_mut("type") {
367 if !(types.len() == 2 && types.iter().any(|t| t == "null")) {
369 types.sort_by_key(type_rank);
370 }
371 }
372 for v in obj.values_mut() {
374 reorder_unions(v);
375 }
376 }
377 Value::Array(arr) => {
378 for v in arr {
379 reorder_unions(v);
380 }
381 }
382 _ => {}
383 }
384 }
385
386 pub fn type_rank(val: &Value) -> usize {
394 match val {
395 Value::String(s) => type_string_rank(s),
396 Value::Object(obj) => {
397 if let Some(Value::String(t)) = obj.get("type") {
398 type_string_rank(t)
399 } else {
400 100 }
402 }
403 _ => 100, }
405 }
406
407 fn type_string_rank(s: &str) -> usize {
409 match s {
410 "null" => 0,
412
413 "map" => 1,
415 "array" => 2,
416 "object" | "record" => 3,
417
418 "boolean" => 10,
420 "integer" | "int" | "long" => 11,
421 "number" | "float" | "double" => 12,
422 "enum" => 13,
423 "string" => 14,
424 "fixed" => 15,
425 "bytes" => 16,
426
427 _ => 99,
429 }
430 }
431
432 pub fn infer_json_schema_from_strings(
434 json_strings: &[String],
435 config: SchemaInferenceConfig,
436 ) -> Result<SchemaInferenceResult, String> {
437 if json_strings.is_empty() {
438 return Err("No JSON strings provided".to_string());
439 }
440
441 let result = panic::catch_unwind(AssertUnwindSafe(
443 || -> Result<SchemaInferenceResult, String> {
444 let mut builder = get_builder(config.schema_uri.as_deref());
446
447 let build_config = BuildConfig {
449 delimiter: config.delimiter,
450 ignore_outer_array: config.ignore_outer_array,
451 };
452
453 let mut processed_count = 0;
454
455 for (i, json_str) in json_strings.iter().enumerate() {
457 if json_str.trim().is_empty() {
458 continue;
459 }
460
461 let validation_result = if let Some(delim) = config.delimiter {
463 if delim == b'\n' {
464 validate_ndjson(json_str)
465 } else {
466 Err(serde_json::Error::custom(format!(
467 "Unsupported delimiter: {:?}",
468 delim
469 )))
470 }
471 } else {
472 validate_json(json_str)
473 };
474
475 if let Err(parse_error) = validation_result {
476 let truncated_json = if json_str.len() > MAX_JSON_ERROR_LENGTH {
477 format!(
478 "{}... [truncated {} chars]",
479 &json_str[..MAX_JSON_ERROR_LENGTH],
480 json_str.len() - MAX_JSON_ERROR_LENGTH
481 )
482 } else {
483 json_str.clone()
484 };
485
486 return Err(format!(
487 "Invalid JSON input at index {}: {} - JSON: {}",
488 i + 1,
489 parse_error,
490 truncated_json
491 ));
492 }
493
494 let prepared_json: Cow<str> = if let Some(ref field) = config.wrap_root {
496 if config.delimiter == Some(b'\n') {
497 let mut wrapped_lines = Vec::new();
499 for line in json_str.lines() {
500 let trimmed = line.trim();
501 if trimmed.is_empty() {
502 continue;
503 }
504 let inner_val: Value =
505 serde_json::from_str(trimmed).map_err(|e| {
506 format!(
507 "Failed to parse NDJSON line before wrap_root: {}",
508 e
509 )
510 })?;
511 wrapped_lines
512 .push(serde_json::json!({ field: inner_val }).to_string());
513 }
514 Cow::Owned(wrapped_lines.join("\n"))
515 } else {
516 let inner_val: Value = serde_json::from_str(json_str).map_err(|e| {
518 format!("Failed to parse JSON before wrap_root: {}", e)
519 })?;
520 Cow::Owned(serde_json::json!({ field: inner_val }).to_string())
521 }
522 } else {
523 Cow::Borrowed(json_str)
524 };
525
526 let mut bytes = prepared_json.as_bytes().to_vec();
527
528 let _schema = build_json_schema(&mut builder, &mut bytes, &build_config);
530 processed_count += 1;
531 }
532
533 let mut final_schema = builder.to_schema();
535 rewrite_objects(&mut final_schema, None, &config);
536 reorder_unions(&mut final_schema);
537
538 #[cfg(feature = "avro")]
539 if config.avro {
540 let avro_schema = SchemaInferenceResult {
541 schema: final_schema.clone(),
542 processed_count,
543 }
544 .to_avro_schema(
545 "genson", Some(""),
547 Some(""), false, );
550 return Ok(SchemaInferenceResult {
551 schema: avro_schema,
552 processed_count,
553 });
554 }
555
556 Ok(SchemaInferenceResult {
557 schema: final_schema,
558 processed_count,
559 })
560 },
561 ));
562
563 match result {
565 Ok(Ok(schema_result)) => Ok(schema_result),
566 Ok(Err(e)) => Err(e),
567 Err(_panic) => {
568 Err("JSON schema inference failed due to invalid JSON input".to_string())
569 }
570 }
571 }
572
573 #[cfg(test)]
574 mod tests {
575 use super::*;
576 use predicates::prelude::*;
577 use serde_json::json;
578
579 #[test]
580 fn test_reorder_unions_string_float_null() {
581 let mut schema = json!({
583 "type": ["string", "float", "null"]
584 });
585
586 reorder_unions(&mut schema);
587
588 assert_eq!(
590 schema,
591 json!({
592 "type": ["null", "float", "string"]
593 })
594 );
595 }
596
597 #[test]
598 fn test_basic_schema_inference() {
599 let json_strings = vec![
600 r#"{"name": "Alice", "age": 30}"#.to_string(),
601 r#"{"name": "Bob", "age": 25, "city": "NYC"}"#.to_string(),
602 ];
603
604 let result =
605 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default())
606 .expect("Schema inference should succeed");
607
608 assert_eq!(result.processed_count, 2);
610
611 let schema_str = result.schema.to_string();
613
614 predicate::str::contains("\"type\"")
615 .and(predicate::str::contains("object"))
616 .eval(&schema_str);
617
618 predicate::str::contains("\"properties\"").eval(&schema_str);
619
620 predicate::str::contains("\"name\"")
622 .and(predicate::str::contains("\"age\""))
623 .eval(&schema_str);
624
625 println!(
626 "โ
Generated schema: {}",
627 serde_json::to_string_pretty(&result.schema).unwrap()
628 );
629 }
630
631 #[test]
632 fn test_empty_input() {
633 let json_strings = vec![];
634 let result =
635 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default());
636
637 assert!(result.is_err());
638
639 let error_msg = result.unwrap_err();
640 predicate::str::contains("No JSON strings provided").eval(&error_msg);
641
642 println!("โ
Empty input correctly rejected with: {}", error_msg);
643 }
644
645 #[test]
646 fn test_invalid_json_variants() {
647 let test_cases = vec![
648 (
649 r#"{"name": "Alice"}"#,
650 r#"{"invalid": json}"#,
651 "unquoted value",
652 ),
653 (
654 r#"{"valid": "json"}"#,
655 r#"{"incomplete":"#,
656 "incomplete string",
657 ),
658 (r#"{"good": "data"}"#, r#"{"trailing":,"#, "trailing comma"),
659 (
660 r#"{"working": true}"#,
661 r#"{invalid: "json"}"#,
662 "unquoted key",
663 ),
664 (
665 r#"{"normal": "object"}"#,
666 r#"{"nested": {"broken": json}}"#,
667 "nested broken JSON",
668 ),
669 ];
670
671 for (valid_json, invalid_json, description) in test_cases {
672 println!("๐งช Testing: {}", description);
673 println!(" Valid JSON: {}", valid_json);
674 println!(" Invalid JSON: {}", invalid_json);
675
676 let json_strings = vec![valid_json.to_string(), invalid_json.to_string()];
677
678 let result =
679 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default());
680
681 assert!(result.is_err(), "Expected error for case: {}", description);
683
684 let error_msg = result.unwrap_err();
685
686 predicate::str::contains("Invalid JSON input at position").eval(&error_msg);
688
689 if invalid_json.len() <= MAX_JSON_ERROR_LENGTH {
691 predicate::str::contains(invalid_json).eval(&error_msg);
692 } else {
693 predicate::str::contains("truncated").eval(&error_msg);
695 }
696
697 predicate::str::contains("panicked").not().eval(&error_msg);
699
700 predicate::str::contains("SIGABRT").not().eval(&error_msg);
701
702 println!(" โ Correctly failed with: {}", error_msg);
703 println!();
704 }
705 }
706
707 #[test]
708 fn test_mixed_valid_and_empty_strings() {
709 let json_strings = vec![
710 r#"{"name": "Alice", "age": 30}"#.to_string(),
711 "".to_string(), " ".to_string(), r#"{"name": "Bob", "age": 25}"#.to_string(),
714 ];
715
716 let result =
717 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default())
718 .expect("Should succeed with valid JSON, skipping empty strings");
719
720 assert_eq!(result.processed_count, 2);
722
723 let schema_str = result.schema.to_string();
724 predicate::str::contains("\"name\"")
725 .and(predicate::str::contains("\"age\""))
726 .eval(&schema_str);
727
728 println!(
729 "โ
Processed {} valid strings, skipped empty ones",
730 result.processed_count
731 );
732 }
733
734 #[test]
735 fn test_schema_config_variations() {
736 let json_strings = vec![r#"[{"item": "first"}, {"item": "second"}]"#.to_string()];
737
738 let config_array = SchemaInferenceConfig {
740 ignore_outer_array: false,
741 ..Default::default()
742 };
743
744 let result = infer_json_schema_from_strings(&json_strings, config_array)
745 .expect("Should handle array schema");
746
747 let schema_str = result.schema.to_string();
748 predicate::str::contains("\"type\"")
749 .and(predicate::str::contains("array"))
750 .eval(&schema_str);
751
752 println!(
753 "โ
Array schema: {}",
754 serde_json::to_string_pretty(&result.schema).unwrap()
755 );
756
757 let config_object = SchemaInferenceConfig {
759 ignore_outer_array: true,
760 ..Default::default()
761 };
762
763 let result = infer_json_schema_from_strings(&json_strings, config_object)
764 .expect("Should handle object schema from array items");
765
766 let schema_str = result.schema.to_string();
767 predicate::str::contains("\"type\"")
768 .and(predicate::str::contains("object"))
769 .eval(&schema_str);
770
771 predicate::str::contains("\"item\"").eval(&schema_str);
772
773 println!(
774 "โ
Object schema from array items: {}",
775 serde_json::to_string_pretty(&result.schema).unwrap()
776 );
777 }
778
779 #[test]
780 fn test_very_long_invalid_json() {
781 let long_value = "x".repeat(500); let long_invalid_json = format!(
784 r#"{{"field1": "{}", "field2": "{}", "field3": "{}", "field4": "{}", "invalid_syntax": }}"#,
785 long_value, long_value, long_value, long_value
786 );
787
788 let json_strings = vec![
789 r#"{"valid": "json"}"#.to_string(),
790 long_invalid_json.clone(),
791 ];
792
793 println!(
794 "๐งช Testing very long invalid JSON ({} chars)",
795 long_invalid_json.len()
796 );
797
798 let result =
799 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default());
800
801 assert!(result.is_err(), "Expected error for very long invalid JSON");
802
803 let error_msg = result.unwrap_err();
804
805 println!("The error message was: {}", error_msg);
806
807 predicate::str::contains("truncated").eval(&error_msg);
809
810 predicate::str::contains("Invalid JSON input at position 2").eval(&error_msg);
812
813 assert!(
815 error_msg.len() < long_invalid_json.len() / 2,
816 "Error message should be much shorter than original JSON"
817 );
818
819 println!(
820 " โ Correctly truncated long JSON in error: {}",
821 error_msg
822 );
823
824 assert!(
826 error_msg.len() < 500,
827 "Error message should be under 500 chars, got: {}",
828 error_msg.len()
829 );
830 }
831
832 #[test]
833 fn test_complex_nested_schema() {
834 let json_strings = vec![
835 json!({
836 "user": {
837 "id": 123,
838 "profile": {
839 "name": "Alice",
840 "preferences": ["dark_mode", "notifications"]
841 }
842 },
843 "metadata": {
844 "created_at": "2024-01-01",
845 "version": 1
846 }
847 })
848 .to_string(),
849 json!({
850 "user": {
851 "id": 456,
852 "profile": {
853 "name": "Bob",
854 "preferences": ["light_mode"]
855 }
856 },
857 "metadata": {
858 "created_at": "2024-01-02",
859 "version": 2
860 }
861 })
862 .to_string(),
863 ];
864
865 let result =
866 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default())
867 .expect("Should handle complex nested schema");
868
869 assert_eq!(result.processed_count, 2);
870
871 let schema_str = result.schema.to_string();
872
873 predicate::str::contains("\"user\"")
875 .and(predicate::str::contains("\"metadata\""))
876 .and(predicate::str::contains("\"profile\""))
877 .and(predicate::str::contains("\"preferences\""))
878 .eval(&schema_str);
879
880 println!("โ
Complex nested schema generated successfully");
881 println!(
882 "Schema: {}",
883 serde_json::to_string_pretty(&result.schema).unwrap()
884 );
885 }
886
887 #[test]
888 fn test_ndjson_parsing() {
889 let ndjson_input = r#"
891{"name": "Alice", "age": 30}
892{"name": "Bob", "age": 25, "city": "NYC"}
893{"name": "Charlie"}
894"#;
895
896 let json_strings = vec![ndjson_input.to_string()];
897
898 let config = SchemaInferenceConfig {
899 delimiter: Some(b'\n'),
900 ..Default::default()
901 };
902
903 let result = infer_json_schema_from_strings(&json_strings, config)
904 .expect("NDJSON schema inference should succeed");
905
906 assert_eq!(result.processed_count, 1,
908 "NDJSON should be counted as a single input string but parsed into multiple rows internally"
909 );
910
911 let schema_str = result.schema.to_string();
912
913 assert!(!schema_str.contains("Alice")); assert!(schema_str.contains("\"name\""));
916 assert!(schema_str.contains("\"age\""));
917 assert!(schema_str.contains("\"city\""));
918
919 println!(
920 "โ
NDJSON schema generated: {}",
921 serde_json::to_string_pretty(&result.schema).unwrap()
922 );
923 }
924
925 #[test]
926 fn test_invalid_ndjson_line() {
927 let ndjson_input = r#"
929{"valid": true}
930{"invalid": json}
931{"also_valid": 123}
932"#;
933
934 let json_strings = vec![ndjson_input.to_string()];
935
936 let config = SchemaInferenceConfig {
937 delimiter: Some(b'\n'),
938 ..Default::default()
939 };
940
941 let result = infer_json_schema_from_strings(&json_strings, config);
942
943 assert!(result.is_err(), "Expected error for malformed NDJSON line");
944
945 let err_msg = result.unwrap_err();
946 eprintln!("Got error: {}", err_msg);
947 assert!(
948 err_msg
949 .contains("Invalid JSON input at index 1: expected value at line 1 column 13"),
950 "Error message should report the failing line"
951 );
952 println!("โ
Correctly rejected malformed NDJSON: {}", err_msg);
953 }
954
955 #[test]
957 fn test_map_threshold_rewrite() {
958 let json_strings = vec![
959 r#"{"labels": {"en": "Hello", "fr": "Bonjour"}}"#.to_string(),
960 r#"{"labels": {"de": "Hallo", "es": "Hola"}}"#.to_string(),
961 ];
962
963 let config = SchemaInferenceConfig {
964 map_threshold: 2,
965 ..Default::default()
966 };
967 let result = infer_json_schema_from_strings(&json_strings, config).unwrap();
968
969 let labels = &result.schema["properties"]["labels"];
970 assert_eq!(labels["type"], "object");
971 assert!(labels.get("additionalProperties").is_some());
972 assert!(labels.get("properties").is_none());
973 }
974
975 #[test]
977 fn test_map_threshold_as_record() {
978 let json_strings = vec![
979 r#"{"labels": {"en": "Hello", "fr": "Bonjour"}}"#.to_string(),
980 r#"{"labels": {"de": "Hallo", "es": "Hola"}}"#.to_string(),
981 ];
982
983 let result =
984 infer_json_schema_from_strings(&json_strings, SchemaInferenceConfig::default())
985 .unwrap();
986
987 let labels = &result.schema["properties"]["labels"];
988 assert!(labels.get("properties").is_some());
989 assert!(labels.get("additionalProperties").is_none());
990 }
991
992 #[test]
993 fn test_wrap_root_inserts_single_required_field() {
994 let json_strings = vec![
995 r#"{"en":{"language":"en","value":"Hello"},"fr":{"language":"fr","value":"Bonjour"}}"#.to_string(),
996 ];
997
998 let cfg = SchemaInferenceConfig {
999 wrap_root: Some("labels".to_string()),
1000 ..Default::default()
1001 };
1002
1003 let out = infer_json_schema_from_strings(&json_strings, cfg).unwrap();
1004 let sch = out.schema;
1005
1006 assert_eq!(sch["type"], "object");
1007 assert_eq!(sch["required"], serde_json::json!(["labels"]));
1008 assert!(sch["properties"]["labels"].is_object());
1009 }
1010
1011 #[test]
1012 fn test_rewrite_objects_map_of_records() {
1013 use serde_json::json;
1014
1015 let mut schema = json!({
1017 "type": "object",
1018 "properties": {
1019 "en": {
1020 "type": "object",
1021 "properties": {
1022 "language": { "type": "string" },
1023 "value": { "type": "string" }
1024 },
1025 "required": ["language", "value"]
1026 },
1027 "fr": {
1028 "type": "object",
1029 "properties": {
1030 "language": { "type": "string" },
1031 "value": { "type": "string" }
1032 },
1033 "required": ["language", "value"]
1034 }
1035 },
1036 "required": ["en","fr"]
1037 });
1038
1039 let cfg = SchemaInferenceConfig {
1040 map_threshold: 2, ..Default::default()
1042 };
1043
1044 rewrite_objects(&mut schema, None, &cfg);
1045
1046 assert_eq!(schema["type"], "object");
1048 assert!(schema.get("properties").is_none());
1049 assert!(schema.get("required").is_none());
1050
1051 let ap = schema
1053 .get("additionalProperties")
1054 .expect("should insert additionalProperties");
1055
1056 assert_eq!(ap["type"], "object");
1057 assert_eq!(ap["properties"]["language"], json!({ "type": "string" }));
1058 assert_eq!(ap["properties"]["value"], json!({ "type": "string" }));
1059 }
1060
1061 #[test]
1062 fn test_map_of_strings_not_promoted_to_records() {
1063 let schema = json!({
1064 "type": "object",
1065 "properties": {
1066 "labels": {
1067 "type": "object",
1068 "properties": {
1069 "en": { "type": "string" },
1070 "fr": { "type": "string" }
1071 },
1072 "required": ["en", "fr"]
1073 }
1074 },
1075 "required": ["labels"]
1076 });
1077
1078 let mut sch = schema.clone();
1079 let cfg = SchemaInferenceConfig {
1080 map_threshold: 2,
1081 ..Default::default()
1082 };
1083 rewrite_objects(&mut sch, None, &cfg);
1084
1085 assert_eq!(
1086 sch["properties"]["labels"]["additionalProperties"]["type"],
1087 "string"
1088 );
1089 }
1090
1091 #[test]
1092 fn test_rewrite_objects_respects_map_max_required_keys() {
1093 let mut schema = json!({
1094 "type": "object",
1095 "properties": {
1096 "field1": {"type": "string"},
1097 "field2": {"type": "string"}
1098 },
1099 "required": ["field1", "field2"]
1100 });
1101
1102 let config = SchemaInferenceConfig {
1103 map_threshold: 2,
1104 map_max_required_keys: Some(1), ..Default::default()
1106 };
1107
1108 rewrite_objects(&mut schema, None, &config);
1109
1110 assert_eq!(schema["type"], "object");
1112 assert!(schema.get("properties").is_some());
1113 assert!(schema.get("additionalProperties").is_none());
1114 }
1115
1116 #[test]
1117 fn test_rewrite_objects_allows_map_with_few_required_keys() {
1118 let mut schema = json!({
1119 "type": "object",
1120 "properties": {
1121 "field1": {"type": "string"},
1122 "field2": {"type": "string"}
1123 },
1124 "required": ["field1"] });
1126
1127 let config = SchemaInferenceConfig {
1128 map_threshold: 2,
1129 map_max_required_keys: Some(1), ..Default::default()
1131 };
1132
1133 rewrite_objects(&mut schema, None, &config);
1134
1135 assert_eq!(schema["type"], "object");
1137 assert!(schema.get("additionalProperties").is_some());
1138 assert!(schema.get("properties").is_none());
1139 }
1140
1141 #[test]
1142 fn test_rewrite_objects_none_max_required_keys_preserves_behavior() {
1143 let mut schema = json!({
1144 "type": "object",
1145 "properties": {
1146 "field1": {"type": "string"},
1147 "field2": {"type": "string"},
1148 "field3": {"type": "string"}
1149 },
1150 "required": ["field1", "field2", "field3"]
1151 });
1152
1153 let config = SchemaInferenceConfig {
1154 map_threshold: 2,
1155 map_max_required_keys: None, ..Default::default()
1157 };
1158
1159 rewrite_objects(&mut schema, None, &config);
1160
1161 assert_eq!(schema["type"], "object");
1163 assert!(schema.get("additionalProperties").is_some());
1164 assert!(schema.get("properties").is_none());
1165 }
1166
1167 #[test]
1168 fn test_rewrite_objects_zero_max_required_keys() {
1169 let mut schema = json!({
1170 "type": "object",
1171 "properties": {
1172 "field1": {"type": "string"},
1173 "field2": {"type": "string"}
1174 },
1175 "required": ["field1"]
1176 });
1177
1178 let config = SchemaInferenceConfig {
1179 map_threshold: 2,
1180 map_max_required_keys: Some(0), ..Default::default()
1182 };
1183
1184 rewrite_objects(&mut schema, None, &config);
1185
1186 assert_eq!(schema["type"], "object");
1188 assert!(schema.get("properties").is_some());
1189 assert!(schema.get("additionalProperties").is_none());
1190 }
1191
1192 #[test]
1193 fn test_rewrite_objects_zero_required_keys_allowed() {
1194 let mut schema = json!({
1195 "type": "object",
1196 "properties": {
1197 "field1": {"type": "string"},
1198 "field2": {"type": "string"}
1199 }
1200 });
1202
1203 let config = SchemaInferenceConfig {
1204 map_threshold: 2,
1205 map_max_required_keys: Some(0), ..Default::default()
1207 };
1208
1209 rewrite_objects(&mut schema, None, &config);
1210
1211 assert_eq!(schema["type"], "object");
1213 assert!(schema.get("additionalProperties").is_some());
1214 assert!(schema.get("properties").is_none());
1215 }
1216
1217 #[test]
1218 fn test_rewrite_objects_force_override_wins() {
1219 let mut schema = json!({
1220 "type": "object",
1221 "properties": {
1222 "field1": {"type": "string"},
1223 "field2": {"type": "string"}
1224 },
1225 "required": ["field1", "field2"]
1226 });
1227
1228 let mut force_types = std::collections::HashMap::new();
1229 force_types.insert("test_field".to_string(), "map".to_string());
1230
1231 let config = SchemaInferenceConfig {
1232 map_threshold: 2,
1233 map_max_required_keys: Some(0), force_field_types: force_types,
1235 ..Default::default()
1236 };
1237
1238 rewrite_objects(&mut schema, Some("test_field"), &config);
1240
1241 assert_eq!(schema["type"], "object");
1243 assert!(schema.get("additionalProperties").is_some());
1244 assert!(schema.get("properties").is_none());
1245 }
1246
1247 #[test]
1248 fn test_rewrite_objects_non_homogeneous_values_not_rewritten() {
1249 let mut schema = json!({
1250 "type": "object",
1251 "properties": {
1252 "field1": {"type": "string"},
1253 "field2": {"type": "integer"} },
1255 "required": []
1256 });
1257
1258 let config = SchemaInferenceConfig {
1259 map_threshold: 2,
1260 map_max_required_keys: Some(5), ..Default::default()
1262 };
1263
1264 rewrite_objects(&mut schema, None, &config);
1265
1266 assert_eq!(schema["type"], "object");
1268 assert!(schema.get("properties").is_some());
1269 assert!(schema.get("additionalProperties").is_none());
1270 }
1271
1272 #[test]
1273 fn test_rewrite_objects_below_threshold_not_rewritten() {
1274 let mut schema = json!({
1275 "type": "object",
1276 "properties": {
1277 "field1": {"type": "string"}
1278 },
1279 "required": []
1280 });
1281
1282 let config = SchemaInferenceConfig {
1283 map_threshold: 5, map_max_required_keys: Some(5), ..Default::default()
1286 };
1287
1288 rewrite_objects(&mut schema, None, &config);
1289
1290 assert_eq!(schema["type"], "object");
1292 assert!(schema.get("properties").is_some());
1293 assert!(schema.get("additionalProperties").is_none());
1294 }
1295
1296 #[test]
1298 fn test_map_max_required_keys_with_wrap_root() {
1299 let json_strings = vec![
1300 r#"{"en":{"language":"en","value":"Hello"},"fr":{"language":"fr","value":"Bonjour"}}"#.to_string(),
1301 ];
1302
1303 let cfg = SchemaInferenceConfig {
1304 wrap_root: Some("labels".to_string()),
1305 map_threshold: 2,
1306 map_max_required_keys: Some(1), ..Default::default()
1308 };
1309
1310 let out = infer_json_schema_from_strings(&json_strings, cfg).unwrap();
1311 let sch = out.schema;
1312
1313 assert_eq!(sch["type"], "object");
1314 assert_eq!(sch["required"], serde_json::json!(["labels"]));
1315
1316 let labels_content = &sch["properties"]["labels"];
1318 assert!(labels_content.is_object());
1319
1320 println!("โ
map_max_required_keys works with wrap_root");
1321 }
1322 }
1323}
1324pub use innermod::*;