use genson_core::{infer_json_schema_from_strings, SchemaInferenceConfig};
use serde_json::{json, Value};
fn forced_map_values(rows: &[&str]) -> Value {
let config = SchemaInferenceConfig {
force_field_types: [("m".to_string(), "map".to_string())].into(),
..SchemaInferenceConfig::default()
};
let schema = infer_json_schema_from_strings(rows, config).unwrap().schema;
let m = &schema["properties"]["m"];
assert!(m.get("properties").is_none(), "m should be a map: {m}");
m["additionalProperties"].clone()
}
#[test]
fn test_forced_map_keeps_integer_values() {
let values = forced_map_values(&[r#"{"m": {"a": 1, "b": 2}}"#, r#"{"m": {"c": 3}}"#]);
assert_eq!(values["type"], json!("integer"));
}
#[test]
fn test_forced_map_keeps_string_values() {
let values = forced_map_values(&[r#"{"m": {"en": "hi", "fr": "salut"}}"#]);
assert_eq!(values["type"], json!("string"));
}
#[test]
fn test_forced_map_of_records_keeps_the_record() {
let values = forced_map_values(&[r#"{"m": {"a": {"n": 1}, "b": {"n": 2}}}"#]);
assert_eq!(values["type"], json!("object"));
assert_eq!(values["properties"]["n"]["type"], json!("integer"));
}
#[test]
fn test_forced_map_unifies_differing_records() {
let values = forced_map_values(&[r#"{"m": {"a": {"n": 1}, "b": {"s": "x"}}}"#]);
let props = values["properties"].as_object().unwrap();
assert!(
props.contains_key("n") && props.contains_key("s"),
"{values}"
);
}
#[test]
fn test_forced_map_with_mixed_scalar_values_falls_back_to_string() {
let values = forced_map_values(&[r#"{"m": {"a": 1, "b": "x"}}"#]);
assert_eq!(values["type"], json!("string"));
}
#[test]
fn test_forced_map_unifies_empty_and_non_empty_arrays() {
let separate_rows = forced_map_values(&[
r#"{"m": {"enwiki": {"title": "A", "badges": ["Q17437796"]}}}"#,
r#"{"m": {"dewiki": {"title": "B", "badges": []}}}"#,
]);
let same_row = forced_map_values(&[
r#"{"m": {"enwiki": {"title": "A", "badges": ["Q17437796"]}, "dewiki": {"title": "B", "badges": []}}}"#,
]);
for values in [separate_rows, same_row] {
assert_eq!(values["type"], json!("object"), "{values}");
let badges = &values["properties"]["badges"];
assert_eq!(badges["type"], json!("array"), "{values}");
assert_eq!(badges["items"]["type"], json!("string"), "{values}");
}
}
#[test]
fn test_forced_map_unifies_records_with_only_empty_arrays() {
let values = forced_map_values(&[
r#"{"m": {"enwiki": {"title": "A", "badges": []}}}"#,
r#"{"m": {"dewiki": {"title": "B", "badges": []}, "frwiki": {"title": "C"}}}"#,
]);
assert_eq!(values["type"], json!("object"), "{values}");
assert_eq!(
values["properties"]["badges"]["type"],
json!(["null", "array"]),
"{values}"
);
}
fn forced_map_values_parallel(rows: &[&str]) -> Value {
assert!(rows.len() >= 10, "needs the parallel path");
forced_map_values(rows)
}
#[test]
fn test_forced_map_of_records_keeps_the_record_in_parallel() {
let row = r#"{"m": {"enwiki": {"title": "A", "badges": []}}}"#;
let values = forced_map_values_parallel(&[row; 12]);
assert_eq!(values["type"], json!("object"), "{values}");
assert_eq!(
values["properties"]["title"]["type"],
json!("string"),
"{values}"
);
}
#[test]
fn test_forced_map_with_empty_rows_keeps_the_record_in_parallel() {
let mut rows = vec![r#"{"m": {}}"#; 10];
rows.push(r#"{"m": {"specieswiki": {"title": "A", "badges": []}}}"#);
rows.push(r#"{"m": {"enwiki": {"title": "B", "badges": ["Q17437796"]}}}"#);
let values = forced_map_values_parallel(&rows);
assert_eq!(values["type"], json!("object"), "{values}");
let badges = &values["properties"]["badges"];
assert_eq!(badges["items"]["type"], json!("string"), "{values}");
}
#[test]
fn test_forced_map_all_empty_is_null_in_parallel() {
let values = forced_map_values_parallel(&[r#"{"m": {}}"#; 12]);
assert_eq!(values, json!({"type": "null"}));
}
#[test]
fn test_forced_map_integer_values_in_parallel() {
let rows: Vec<String> = (0..12)
.map(|i| format!(r#"{{"m": {{"k{i}": {i}}}}}"#))
.collect();
let rows: Vec<&str> = rows.iter().map(String::as_str).collect();
let values = forced_map_values_parallel(&rows);
assert_eq!(values["type"], json!("integer"), "{values}");
}