use crate::{JSONTools, JsonOutput};
use serde_json::Value;
#[cfg(test)]
pub fn extract_single(output: JsonOutput) -> String {
match output {
JsonOutput::Single(result) => result,
JsonOutput::Multiple(_) => panic!("Expected single result but got multiple"),
}
}
#[cfg(test)]
pub fn extract_multiple(output: JsonOutput) -> Vec<String> {
match output {
JsonOutput::Single(_) => panic!("Expected multiple results but got single"),
JsonOutput::Multiple(results) => results,
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_basic_flattening() {
let json = r#"{"a": {"b": {"c": 1}}}"#;
let result = JSONTools::new().flatten().execute(json).unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["a.b.c"], 1);
}
#[test]
fn test_basic_unflattening() {
let flattened = r#"{"user.name": "John", "user.age": 30}"#;
let result = JSONTools::new().unflatten().execute(flattened).unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["name"], "John");
assert_eq!(parsed["user"]["age"], 30);
}
#[test]
fn test_array_flattening() {
let json = r#"{"items": [1, 2, {"nested": "value"}]}"#;
let result = JSONTools::new().flatten().execute(json).unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["items.0"], 1);
assert_eq!(parsed["items.1"], 2);
assert_eq!(parsed["items.2.nested"], "value");
}
#[test]
fn test_array_unflattening() {
let flattened = r#"{"items.0": 1, "items.1": 2, "items.2.nested": "value"}"#;
let result = JSONTools::new().unflatten().execute(flattened).unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["items"][0], 1);
assert_eq!(parsed["items"][1], 2);
assert_eq!(parsed["items"][2]["nested"], "value");
}
#[test]
fn test_custom_separator() {
let json = r#"{"user": {"name": "John", "age": 30}}"#;
let result = JSONTools::new()
.flatten()
.separator("::")
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user::name"], "John");
assert_eq!(parsed["user::age"], 30);
}
#[test]
fn test_lowercase_keys() {
let json = r#"{"User": {"Name": "John", "Email": "john@example.com"}}"#;
let result = JSONTools::new()
.flatten()
.lowercase_keys(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user.name"], "John");
assert_eq!(parsed["user.email"], "john@example.com");
}
#[test]
fn test_key_replacement() {
let json = r#"{"user_name": "John", "admin_role": "super"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("user_", "")
.key_replacement("admin_", "")
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["name"], "John");
assert_eq!(parsed["role"], "super");
}
#[test]
fn test_value_replacement() {
let json = r#"{"email": "john@example.com", "role": "super"}"#;
let result = JSONTools::new()
.flatten()
.value_replacement("@example.com", "@company.org")
.value_replacement("super", "administrator")
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["email"], "john@company.org");
assert_eq!(parsed["role"], "administrator");
}
#[test]
fn test_regex_replacements() {
let json = r#"{"user_name": "John", "admin_role": "super", "temp_data": "test"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'^(user|admin)_'", "")
.value_replacement("r'^super$'", "administrator")
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["name"], "John");
assert_eq!(parsed["role"], "administrator");
assert_eq!(parsed["temp_data"], "test");
}
#[test]
fn test_bare_pattern_with_regex_metacharacters_is_literal() {
let json = r#"{"price": "$100", "note": "a.b.c", "status": "end$"}"#;
let result = JSONTools::new()
.flatten()
.value_replacement("$100", "USD 100")
.key_replacement("note", "dotted")
.execute(json)
.unwrap();
let parsed: Value = serde_json::from_str(&extract_single(result)).unwrap();
assert_eq!(parsed["price"], "USD 100");
assert_eq!(parsed["dotted"], "a.b.c");
assert_eq!(parsed["status"], "end$");
}
#[test]
fn test_r_quote_wrapped_pattern_is_regex() {
let json = r#"{"a.b.c": "x"}"#;
let result = JSONTools::new()
.unflatten()
.value_replacement("r'.'", "Y")
.execute(json)
.unwrap();
let parsed: Value = serde_json::from_str(&extract_single(result)).unwrap();
assert_eq!(parsed["a"]["b"]["c"], "Y");
}
#[test]
fn test_malformed_r_quote_regex_is_silently_ignored() {
let json = r#"{"key": "value"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'[invalid'", "replacement")
.value_replacement("r'*invalid'", "replacement")
.execute(json)
.unwrap();
let parsed: Value = serde_json::from_str(&extract_single(result)).unwrap();
assert_eq!(parsed["key"], "value");
}
#[test]
fn test_r_quote_pattern_edge_cases() {
let json = r#"{"a": "r'", "b": "not it"}"#;
let result = JSONTools::new()
.flatten()
.value_replacement("r'", "X")
.execute(json)
.unwrap();
let parsed: Value = serde_json::from_str(&extract_single(result)).unwrap();
assert_eq!(parsed["a"], "X"); assert_eq!(parsed["b"], "not it");
let json2 = r#"{"a": "hi"}"#;
let result2 = JSONTools::new()
.flatten()
.value_replacement("r''", "-")
.execute(json2)
.unwrap();
let parsed2: Value = serde_json::from_str(&extract_single(result2)).unwrap();
assert_eq!(parsed2["a"], "-h-i-");
}
#[test]
fn test_remove_empty_strings() {
let json = r#"{"user": {"name": "John", "bio": "", "age": 30}}"#;
let result = JSONTools::new()
.flatten()
.remove_empty_strings(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user.name"], "John");
assert_eq!(parsed["user.age"], 30);
assert!(!parsed.as_object().unwrap().contains_key("user.bio"));
}
#[test]
fn test_remove_nulls() {
let json = r#"{"user": {"name": "John", "age": null, "city": "NYC"}}"#;
let result = JSONTools::new()
.flatten()
.remove_nulls(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user.name"], "John");
assert_eq!(parsed["user.city"], "NYC");
assert!(!parsed.as_object().unwrap().contains_key("user.age"));
}
#[test]
fn test_remove_empty_objects() {
let json = r#"{"user": {"name": "John", "profile": {}, "settings": {"theme": "dark"}}}"#;
let result = JSONTools::new()
.flatten()
.remove_empty_objects(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user.name"], "John");
assert_eq!(parsed["user.settings.theme"], "dark");
let keys: Vec<&str> = parsed
.as_object()
.unwrap()
.keys()
.map(|s| s.as_str())
.collect();
assert!(!keys.iter().any(|k| k.starts_with("user.profile")));
}
#[test]
fn test_remove_empty_arrays() {
let json = r#"{"user": {"name": "John", "tags": [], "items": [1, 2]}}"#;
let result = JSONTools::new()
.flatten()
.remove_empty_arrays(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user.name"], "John");
assert_eq!(parsed["user.items.0"], 1);
assert_eq!(parsed["user.items.1"], 2);
let keys: Vec<&str> = parsed
.as_object()
.unwrap()
.keys()
.map(|s| s.as_str())
.collect();
assert!(!keys.iter().any(|k| k.starts_with("user.tags")));
}
#[test]
fn test_all_features_combined() {
let json = r#"{"user_profile": {"user_name": "John", "user_email": "john@example.com", "user_age": null, "user_bio": "", "user_tags": []}}"#;
let result = JSONTools::new()
.flatten()
.separator("::")
.lowercase_keys(true)
.key_replacement("user_", "")
.value_replacement("@example.com", "@company.org")
.remove_empty_strings(true)
.remove_nulls(true)
.remove_empty_objects(true)
.remove_empty_arrays(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["profile::name"], "John");
assert_eq!(parsed["profile::email"], "john@company.org");
assert!(!parsed.as_object().unwrap().contains_key("profile::age"));
assert!(!parsed.as_object().unwrap().contains_key("profile::bio"));
assert!(!parsed.as_object().unwrap().contains_key("profile::tags"));
}
#[test]
fn test_roundtrip_compatibility() {
let original =
r#"{"user": {"name": "John", "age": 30}, "items": [1, 2, {"nested": "value"}]}"#;
let flattened_result = JSONTools::new().flatten().execute(original).unwrap();
let flattened = extract_single(flattened_result);
let unflattened_result = JSONTools::new().unflatten().execute(&flattened).unwrap();
let unflattened = extract_single(unflattened_result);
let original_parsed: Value = serde_json::from_str(original).unwrap();
let result_parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(original_parsed, result_parsed);
}
#[test]
fn test_multiple_input_flatten() {
let json_list = vec![
r#"{"user": {"name": "John"}}"#,
r#"{"user": {"name": "Jane"}}"#,
];
let result = JSONTools::new()
.flatten()
.execute(json_list.as_slice())
.unwrap();
let results = extract_multiple(result);
assert_eq!(results.len(), 2);
let parsed1: Value = serde_json::from_str(&results[0]).unwrap();
let parsed2: Value = serde_json::from_str(&results[1]).unwrap();
assert_eq!(parsed1["user.name"], "John");
assert_eq!(parsed2["user.name"], "Jane");
}
#[test]
fn test_multiple_input_unflatten() {
let flattened_list = vec![r#"{"user.name": "John"}"#, r#"{"user.name": "Jane"}"#];
let result = JSONTools::new()
.unflatten()
.execute(flattened_list.as_slice())
.unwrap();
let results = extract_multiple(result);
assert_eq!(results.len(), 2);
let parsed1: Value = serde_json::from_str(&results[0]).unwrap();
let parsed2: Value = serde_json::from_str(&results[1]).unwrap();
assert_eq!(parsed1["user"]["name"], "John");
assert_eq!(parsed2["user"]["name"], "Jane");
}
#[test]
fn test_error_no_mode_set() {
let json = r#"{"user": {"name": "John"}}"#;
let result = JSONTools::new().execute(json);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Operation mode not set"));
}
#[test]
fn test_invalid_json() {
let invalid_json = r#"{"user": {"name": "John"}"#; let result = JSONTools::new().flatten().execute(invalid_json);
assert!(result.is_err());
}
#[test]
fn test_unflatten_remove_empty_strings() {
let flattened = r#"{"user.name": "John", "user.bio": "", "user.age": 30}"#;
let result = JSONTools::new()
.unflatten()
.remove_empty_strings(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["name"], "John");
assert_eq!(parsed["user"]["age"], 30);
assert!(!parsed["user"].as_object().unwrap().contains_key("bio"));
}
#[test]
fn test_unflatten_remove_nulls() {
let flattened = r#"{"user.name": "John", "user.age": null, "user.city": "NYC"}"#;
let result = JSONTools::new()
.unflatten()
.remove_nulls(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["name"], "John");
assert_eq!(parsed["user"]["city"], "NYC");
assert!(!parsed["user"].as_object().unwrap().contains_key("age"));
}
#[test]
fn test_unflatten_remove_empty_objects() {
let flattened =
r#"{"user.name": "John", "user.profile": {}, "user.settings.theme": "dark"}"#;
let result = JSONTools::new()
.unflatten()
.remove_empty_objects(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["name"], "John");
assert_eq!(parsed["user"]["settings"]["theme"], "dark");
assert!(!parsed["user"].as_object().unwrap().contains_key("profile"));
}
#[test]
fn test_unflatten_remove_empty_arrays() {
let flattened = r#"{"user.name": "John", "user.tags": [], "user.items.0": "first"}"#;
let result = JSONTools::new()
.unflatten()
.remove_empty_arrays(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["name"], "John");
assert_eq!(parsed["user"]["items"][0], "first");
assert!(!parsed["user"].as_object().unwrap().contains_key("tags"));
}
#[test]
fn test_unflatten_all_filters_combined() {
let flattened = r#"{"user.name": "John", "user.bio": "", "user.age": null, "user.profile": {}, "user.tags": [], "user.settings.theme": "dark"}"#;
let result = JSONTools::new()
.unflatten()
.remove_empty_strings(true)
.remove_nulls(true)
.remove_empty_objects(true)
.remove_empty_arrays(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["name"], "John");
assert_eq!(parsed["user"]["settings"]["theme"], "dark");
let user_obj = parsed["user"].as_object().unwrap();
assert!(!user_obj.contains_key("bio"));
assert!(!user_obj.contains_key("age"));
assert!(!user_obj.contains_key("profile"));
assert!(!user_obj.contains_key("tags"));
}
#[test]
fn test_unflatten_nested_filtering() {
let flattened = r#"{"users.0.name": "John", "users.0.bio": "", "users.1.name": "Jane", "users.1.age": null, "users.2.profile": {}}"#;
let result = JSONTools::new()
.unflatten()
.remove_empty_strings(true)
.remove_nulls(true)
.remove_empty_objects(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["users"][0]["name"], "John");
assert_eq!(parsed["users"][1]["name"], "Jane");
assert!(!parsed["users"][0].as_object().unwrap().contains_key("bio"));
assert!(!parsed["users"][1].as_object().unwrap().contains_key("age"));
if let Some(user2) = parsed["users"].get(2) {
if let Some(user2_obj) = user2.as_object() {
assert!(!user2_obj.contains_key("profile"));
}
}
}
#[test]
fn test_unflatten_with_replacements_and_filtering() {
let flattened = r#"{"user_name": "John", "user_bio": "", "user_email": "john@example.com", "user_age": null}"#;
let result = JSONTools::new()
.unflatten()
.key_replacement("user_", "profile.")
.value_replacement("@example.com", "@company.org")
.remove_empty_strings(true)
.remove_nulls(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["profile"]["name"], "John");
assert_eq!(parsed["profile"]["email"], "john@company.org");
let profile_obj = parsed["profile"].as_object().unwrap();
assert!(!profile_obj.contains_key("bio"));
assert!(!profile_obj.contains_key("age"));
}
#[test]
fn test_feature_parity_flatten_vs_unflatten() {
let original = r#"{"user_profile": {"user_name": "John", "user_email": "john@example.com", "user_age": null, "user_bio": "", "user_tags": []}}"#;
let flatten_result = JSONTools::new()
.flatten()
.separator("::")
.lowercase_keys(true)
.key_replacement("user_", "")
.value_replacement("@example.com", "@company.org")
.remove_empty_strings(true)
.remove_nulls(true)
.remove_empty_objects(true)
.remove_empty_arrays(true)
.execute(original)
.unwrap();
let unflatten_result = JSONTools::new()
.unflatten()
.separator("::")
.lowercase_keys(true)
.key_replacement("user_", "")
.value_replacement("@company.org", "@example.com")
.remove_empty_strings(true)
.remove_nulls(true)
.remove_empty_objects(true)
.remove_empty_arrays(true)
.execute(r#"{"profile::name": "John", "profile::email": "john@company.org", "profile::bio": "", "profile::age": null, "profile::tags": []}"#)
.unwrap();
let flattened = extract_single(flatten_result);
let unflattened = extract_single(unflatten_result);
let flattened_parsed: Value = serde_json::from_str(&flattened).unwrap();
let unflattened_parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(flattened_parsed["profile::name"], "John");
assert_eq!(flattened_parsed["profile::email"], "john@company.org");
assert!(!flattened_parsed
.as_object()
.unwrap()
.contains_key("profile::bio"));
assert!(!flattened_parsed
.as_object()
.unwrap()
.contains_key("profile::age"));
assert!(!flattened_parsed
.as_object()
.unwrap()
.contains_key("profile::tags"));
assert_eq!(unflattened_parsed["profile"]["name"], "John");
assert_eq!(unflattened_parsed["profile"]["email"], "john@example.com");
let profile_obj = unflattened_parsed["profile"].as_object().unwrap();
assert!(!profile_obj.contains_key("bio"));
assert!(!profile_obj.contains_key("age"));
assert!(!profile_obj.contains_key("tags"));
}
#[test]
fn test_handle_key_collision_flatten_arrays() {
let json = r#"{"User_name": "John", "Admin_name": "Jane", "Guest_name": "Bob"}"#;
let result = JSONTools::new()
.flatten()
.separator("::")
.key_replacement("r'(User|Admin|Guest)_'", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
let obj = parsed.as_object().unwrap();
assert_eq!(obj.len(), 1);
assert!(obj.contains_key("name"));
let arr = obj.get("name").unwrap().as_array().unwrap();
assert_eq!(arr.len(), 3);
}
#[test]
fn test_handle_key_collision_flatten() {
let json = r#"{"User_name": "John", "Admin_name": "Jane", "Guest_name": "Bob"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'(User|Admin|Guest)_'", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
if let Some(array) = parsed["name"].as_array() {
assert_eq!(array.len(), 3);
let values: Vec<&str> = array.iter().map(|v| v.as_str().unwrap()).collect();
assert!(values.contains(&"John"));
assert!(values.contains(&"Jane"));
assert!(values.contains(&"Bob"));
} else {
panic!("Expected array for 'name' key");
}
}
#[test]
fn test_handle_key_collision_unflatten() {
let flattened = r#"{"name::0": "John", "name::1": "Jane", "name::2": "Bob"}"#;
let result = JSONTools::new()
.unflatten()
.separator("::")
.key_replacement("r'name::\\d+'", "user_name")
.handle_key_collision(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
if let Some(array) = parsed["user_name"].as_array() {
assert_eq!(array.len(), 3);
let values: Vec<&str> = array.iter().map(|v| v.as_str().unwrap()).collect();
assert!(values.contains(&"John"));
assert!(values.contains(&"Jane"));
assert!(values.contains(&"Bob"));
} else {
panic!("Expected array for 'user_name' key");
}
}
#[test]
fn test_collision_precedence_collect_only() {
let json = r#"{"User_name": "John", "Admin_name": "Jane"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'(User|Admin)_'", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
let obj = parsed.as_object().unwrap();
assert_eq!(obj.len(), 1);
assert!(obj.contains_key("name"));
assert!(parsed["name"].is_array());
let arr = parsed["name"].as_array().unwrap();
assert_eq!(arr.len(), 2);
let mut values: Vec<&str> = arr.iter().map(|v| v.as_str().unwrap()).collect();
values.sort();
assert_eq!(values, vec!["Jane", "John"]);
}
#[test]
fn test_no_collision_no_change() {
let json = r#"{"User_name": "John", "Admin_email": "jane@example.com"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'(User|Admin)_'", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["name"], "John");
assert_eq!(parsed["email"], "jane@example.com");
}
#[test]
fn test_collision_with_custom_separator() {
let json = r#"{"User_name": "John", "Admin_name": "Jane"}"#;
let result = JSONTools::new()
.flatten()
.separator("__")
.key_replacement("r'(User|Admin)_'", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
let obj = parsed.as_object().unwrap();
assert_eq!(obj.len(), 1);
assert!(obj.contains_key("name"));
let arr = parsed["name"].as_array().unwrap();
assert_eq!(arr.len(), 2);
let mut values: Vec<&str> = arr.iter().map(|v| v.as_str().unwrap()).collect();
values.sort();
assert_eq!(values, vec!["Jane", "John"]);
}
#[test]
fn test_collision_with_mixed_value_types() {
let json = r#"{"User_name": "John", "Admin_name": 42, "Guest_name": true}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'(User|Admin|Guest)_'", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
if let Some(array) = parsed["name"].as_array() {
assert_eq!(array.len(), 3);
let has_john = array.iter().any(|v| v.as_str() == Some("John"));
let has_42 = array.iter().any(|v| v.as_i64() == Some(42));
let has_true = array.iter().any(|v| v.as_bool() == Some(true));
assert!(has_john);
assert!(has_42);
assert!(has_true);
} else {
panic!("Expected array for 'name' key");
}
}
#[test]
fn test_collision_with_filtering() {
let json = r#"{"User_name": "John", "Admin_name": "", "Guest_name": "Bob"}"#;
let result = JSONTools::new()
.flatten()
.key_replacement("r'(User|Admin|Guest)_'", "")
.remove_empty_strings(true)
.handle_key_collision(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
if let Some(array) = parsed["name"].as_array() {
assert_eq!(array.len(), 2);
let values: Vec<&str> = array.iter().map(|v| v.as_str().unwrap()).collect();
assert!(values.contains(&"John"));
assert!(values.contains(&"Bob"));
} else {
panic!("Expected array for 'name' key");
}
}
#[test]
fn test_basic_number_conversion() {
let json = r#"{"id": "123", "price": "45.67", "count": "-10"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["id"], 123);
assert_eq!(parsed["price"], 45.67);
assert_eq!(parsed["count"], -10);
}
#[test]
fn test_thousands_separator_us_format() {
let json = r#"{"amount": "1,234.56", "total": "1,000,000"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["amount"], 1234.56);
assert_eq!(parsed["total"], 1000000);
}
#[test]
fn test_thousands_separator_european_format() {
let json = r#"{"amount": "1.234,56", "total": "1.000.000,00"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["amount"], 1234.56);
assert_eq!(parsed["total"], 1000000.0);
}
#[test]
fn test_currency_symbols() {
let json = r#"{"usd": "$123.45", "eur": "€99.99", "gbp": "£50.00"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["usd"], 123.45);
assert_eq!(parsed["eur"], 99.99);
assert_eq!(parsed["gbp"], 50.0);
}
#[test]
fn test_scientific_notation() {
let json = r#"{"small": "1.23e-4", "large": "1e5", "negative": "-2.5e3"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["small"], 0.000123);
assert_eq!(parsed["large"], 100000.0);
assert_eq!(parsed["negative"], -2500.0);
}
#[test]
fn test_boolean_conversion() {
let json =
r#"{"a": "true", "b": "TRUE", "c": "True", "d": "false", "e": "FALSE", "f": "False"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["a"], true);
assert_eq!(parsed["b"], true);
assert_eq!(parsed["c"], true);
assert_eq!(parsed["d"], false);
assert_eq!(parsed["e"], false);
assert_eq!(parsed["f"], false);
}
#[test]
fn test_keep_invalid_strings() {
let json = r#"{"name": "John", "code": "ABC123", "maybe": "yes", "invalid": "12.34.56", "text": "hello123"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["name"], "John");
assert_eq!(parsed["code"], "ABC123"); assert_eq!(parsed["maybe"], true); assert_eq!(parsed["invalid"], "12.34.56"); assert_eq!(parsed["text"], "hello123"); }
#[test]
fn test_mixed_conversion() {
let json = r#"{"id": "123", "name": "Alice", "price": "$1,234.56", "active": "true", "code": "XYZ"}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["id"], 123);
assert_eq!(parsed["name"], "Alice");
assert_eq!(parsed["price"], 1234.56);
assert_eq!(parsed["active"], true);
assert_eq!(parsed["code"], "XYZ");
}
#[test]
fn test_nested_conversion() {
let json = r#"{"user": {"id": "456", "age": "25", "verified": "true"}}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["user.id"], 456);
assert_eq!(parsed["user.age"], 25);
assert_eq!(parsed["user.verified"], true);
}
#[test]
fn test_array_conversion() {
let json = r#"{"numbers": ["123", "45.6", "true", "invalid"]}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["numbers.0"], 123);
assert_eq!(parsed["numbers.1"], 45.6);
assert_eq!(parsed["numbers.2"], true);
assert_eq!(parsed["numbers.3"], "invalid");
}
#[test]
fn test_conversion_disabled_by_default() {
let json = r#"{"id": "123", "active": "true"}"#;
let result = JSONTools::new().flatten().execute(json).unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["id"], "123");
assert_eq!(parsed["active"], "true");
}
#[test]
fn test_unflatten_with_conversion() {
let json = r#"{"user.id": "789", "user.active": "false"}"#;
let result = JSONTools::new()
.unflatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["id"], 789);
assert_eq!(parsed["user"]["active"], false);
}
#[test]
fn test_normal_mode_with_conversion() {
let json = r#"{"user": {"id": "999", "enabled": "TRUE"}}"#;
let result = JSONTools::new()
.normal()
.auto_convert_types(true)
.execute(json)
.unwrap();
let processed = extract_single(result);
let parsed: Value = serde_json::from_str(&processed).unwrap();
assert_eq!(parsed["user"]["id"], 999);
assert_eq!(parsed["user"]["enabled"], true);
}
#[test]
fn test_normal_mode_collision_handling() {
let json = r#"{"user_name": "Alice", "admin_name": "Bob"}"#;
let result = JSONTools::new()
.normal()
.key_replacement("user_", "")
.key_replacement("admin_", "")
.handle_key_collision(true)
.execute(json)
.unwrap();
let processed = extract_single(result);
let parsed: Value = serde_json::from_str(&processed).unwrap();
assert!(parsed["name"].is_array());
let names: Vec<&str> = parsed["name"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(names.contains(&"Alice"));
assert!(names.contains(&"Bob"));
}
#[test]
fn test_normal_mode_deep_nesting_with_filtering() {
let inner = r#"{"a":{"b":{"keep":"hello","remove":"","null_val":null}}}"#;
let mut json = inner.to_string();
for i in 0..50 {
json = format!(r#"{{"level{}": {}}}"#, i, json);
}
let result = JSONTools::new()
.normal()
.remove_empty_strings(true)
.remove_nulls(true)
.execute(&json)
.unwrap();
let processed = extract_single(result);
let parsed: Value = serde_json::from_str(&processed).unwrap();
let mut current = &parsed;
for i in (0..50).rev() {
let key = format!("level{}", i);
assert!(current[&key].is_object(), "missing level{}", i);
current = ¤t[&key];
}
assert_eq!(current["a"]["b"]["keep"], "hello");
assert_eq!(current["a"]["b"]["remove"], Value::Null); assert_eq!(current["a"]["b"]["null_val"], Value::Null); }
#[test]
fn test_normal_mode_unicode_keys_with_lowercase() {
let json = r#"{"Ñoño": 1, "ÜBER": 2, "Hello": 3, "café": 4}"#;
let result = JSONTools::new()
.normal()
.lowercase_keys(true)
.execute(json)
.unwrap();
let processed = extract_single(result);
let parsed: Value = serde_json::from_str(&processed).unwrap();
assert_eq!(parsed["hello"], 3);
assert_eq!(parsed["ñoño"], 1);
assert_eq!(parsed["über"], 2);
assert_eq!(parsed["café"], 4);
}
}
#[cfg(test)]
mod memory_profiling_tests {
use crate::tests::extract_multiple;
use crate::JSONTools;
use serde_json::Value;
#[test]
fn test_parallel_memory_overhead() {
let json_docs: Vec<String> = (0..100)
.map(|i| {
format!(
r#"{{"id": {}, "name": "User {}", "email": "user{}@example.com", "age": {}, "active": true}}"#,
i, i, i, 20 + (i % 50)
)
})
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result_seq = JSONTools::new()
.flatten()
.parallel_threshold(usize::MAX) .execute(json_refs.clone())
.expect("Sequential processing failed");
let result_par = JSONTools::new()
.flatten()
.parallel_threshold(1) .execute(json_refs)
.expect("Parallel processing failed");
let (seq_results, par_results) = match (result_seq, result_par) {
(crate::JsonOutput::Multiple(seq), crate::JsonOutput::Multiple(par)) => (seq, par),
_ => panic!("Expected Multiple output"),
};
assert_eq!(
seq_results.len(),
par_results.len(),
"Result counts should match"
);
for (i, (seq, par)) in seq_results.iter().zip(par_results.iter()).enumerate() {
assert_eq!(seq, par, "Result {} should be identical", i);
}
let total_size: usize = seq_results.iter().map(|s| s.len()).sum();
let avg_size = total_size / seq_results.len();
println!("Average result size: {} bytes", avg_size);
println!(
"Total results size: {} bytes ({:.2} KB)",
total_size,
total_size as f64 / 1024.0
);
assert!(avg_size < 1000, "Average result size is unexpectedly large");
}
#[test]
fn test_effective_thread_count_respects_override() {
let with_override = crate::ProcessingConfig {
num_threads: Some(2),
..crate::ProcessingConfig::new()
};
assert_eq!(with_override.effective_thread_count(1000), 2);
assert_eq!(with_override.effective_thread_count(1), 1);
let zero_override = crate::ProcessingConfig {
num_threads: Some(0),
..crate::ProcessingConfig::new()
};
assert_eq!(zero_override.effective_thread_count(1000), 1);
let default_config = crate::ProcessingConfig::new();
let n = default_config.effective_thread_count(1000);
assert!(n >= 1);
assert_eq!(default_config.effective_thread_count(1), 1);
}
#[test]
fn test_num_threads_builder_option_produces_correct_batch_results() {
let json_docs: Vec<String> = (0..50)
.map(|i| format!(r#"{{"id": {i}, "nested": {{"value": {i}}}}}"#))
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.parallel_threshold(1) .num_threads(Some(2))
.execute(json_refs)
.expect("Batch processing with constrained num_threads failed");
let results = extract_multiple(result);
assert_eq!(results.len(), 50);
for (i, flat) in results.iter().enumerate() {
let parsed: Value = serde_json::from_str(flat).unwrap();
assert_eq!(parsed["id"], i);
assert_eq!(parsed["nested.value"], i);
}
}
#[test]
fn test_large_batch_memory_scaling() {
let batch_sizes = vec![10, 50, 100, 500];
let mut bytes_per_item = Vec::new();
for &size in &batch_sizes {
let json_docs: Vec<String> = (0..size)
.map(|i| {
format!(
r#"{{"id": {}, "data": "Item {}", "value": {}}}"#,
i,
i,
i * 100
)
})
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.parallel_threshold(10)
.execute(json_refs)
.expect("Processing failed");
if let crate::JsonOutput::Multiple(results) = result {
let total_bytes: usize = results
.iter()
.map(|s| s.len() + std::mem::size_of::<String>())
.sum();
let per_item = total_bytes / size;
bytes_per_item.push(per_item);
println!(
"Batch size {}: {} bytes total, {} bytes per item",
size, total_bytes, per_item
);
}
}
let min_per_item = *bytes_per_item.iter().min().unwrap();
let max_per_item = *bytes_per_item.iter().max().unwrap();
let ratio = max_per_item as f64 / min_per_item as f64;
println!("Memory per item ratio (max/min): {:.2}x", ratio);
assert!(
ratio < 2.0,
"Memory per item varies too much ({:.2}x), suggesting non-linear scaling",
ratio
);
}
#[test]
fn test_chunked_processing_memory() {
let size = 1500;
let json_docs: Vec<String> = (0..size)
.map(|i| format!(r#"{{"id": {}, "value": {}}}"#, i, i * 10))
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.parallel_threshold(10)
.execute(json_refs)
.expect("Chunked processing failed");
if let crate::JsonOutput::Multiple(results) = result {
assert_eq!(results.len(), size, "Should process all items");
let total_bytes: usize = results
.iter()
.map(|s| s.len() + std::mem::size_of::<String>())
.sum();
let per_item = total_bytes / size;
println!(
"Chunked processing (1500 items): {} bytes total, {} bytes per item",
total_bytes, per_item
);
assert!(
per_item < 1000,
"Per-item memory ({} bytes) is too high for chunked processing",
per_item
);
}
}
#[test]
fn test_memory_cleanup_after_processing() {
let json_docs: Vec<String> = (0..100)
.map(|i| format!(r#"{{"id": {}, "data": "test"}}"#, i))
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.parallel_threshold(10)
.execute(json_refs)
.expect("Processing failed");
if let crate::JsonOutput::Multiple(results) = result {
assert_eq!(results.len(), 100, "Should have 100 results");
for (i, result_str) in results.iter().enumerate() {
assert!(
result_str.contains("id"),
"Result {} should contain 'id' field",
i
);
assert!(
result_str.contains("data"),
"Result {} should contain 'data' field",
i
);
}
println!("Successfully processed and validated 100 items");
println!("Results can be dropped without issues");
}
}
#[test]
fn test_parallel_vs_sequential_same_output_size() {
let json_docs: Vec<String> = (0..200)
.map(|i| {
format!(
r#"{{"id": {}, "nested": {{"value": {}, "name": "Item {}"}}}}"#,
i,
i * 10,
i
)
})
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result_seq = JSONTools::new()
.flatten()
.parallel_threshold(usize::MAX)
.execute(json_refs.clone())
.expect("Sequential failed");
let result_par = JSONTools::new()
.flatten()
.parallel_threshold(1)
.execute(json_refs)
.expect("Parallel failed");
let (seq, par) = match (result_seq, result_par) {
(crate::JsonOutput::Multiple(seq), crate::JsonOutput::Multiple(par)) => (seq, par),
_ => panic!("Expected Multiple output"),
};
assert_eq!(seq.len(), par.len(), "Result counts should match");
for (i, (s, p)) in seq.iter().zip(par.iter()).enumerate() {
assert_eq!(s, p, "Result {} should be identical", i);
}
let seq_size: usize = seq.iter().map(|s| s.len()).sum();
let par_size: usize = par.iter().map(|s| s.len()).sum();
println!("Sequential total size: {} bytes", seq_size);
println!("Parallel total size: {} bytes", par_size);
assert_eq!(seq_size, par_size, "Output sizes should be identical");
}
}
#[cfg(test)]
mod nested_parallelism_tests {
use crate::{JSONTools, JsonOutput};
#[test]
fn test_nested_parallel_consistency() {
let json = r#"{
"users": [
{"id": 1, "name": "Alice", "tags": ["admin", "user"]},
{"id": 2, "name": "Bob", "tags": ["user"]},
{"id": 3, "name": "Charlie", "tags": ["moderator", "user"]},
{"id": 4, "name": "David", "tags": ["user"]},
{"id": 5, "name": "Eve", "tags": ["admin", "moderator"]}
],
"settings": {
"theme": "dark",
"notifications": {
"email": true,
"push": false,
"sms": true
},
"privacy": {
"profile_visible": true,
"show_email": false
}
},
"metadata": {
"version": "1.0",
"created": "2024-01-01",
"updated": "2024-01-15"
}
}"#;
let sequential_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(usize::MAX)
.execute(json)
.expect("Sequential flatten failed");
let parallel_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(2)
.execute(json)
.expect("Parallel flatten failed");
let sequential_json = match sequential_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let parallel_json = match parallel_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let sequential_value: serde_json::Value =
serde_json::from_str(&sequential_json).expect("Failed to parse sequential result");
let parallel_value: serde_json::Value =
serde_json::from_str(¶llel_json).expect("Failed to parse parallel result");
assert_eq!(
sequential_value, parallel_value,
"Nested parallel processing should produce identical results to sequential processing"
);
}
#[test]
fn test_large_object_nested_parallelism() {
let mut json = String::from("{");
for i in 0..150 {
if i > 0 {
json.push(',');
}
json.push_str(&format!(
r#""key_{}": {{"nested": "value_{}", "id": {}}}"#,
i, i, i
));
}
json.push('}');
let sequential_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(usize::MAX)
.execute(&json)
.expect("Sequential flatten failed");
let parallel_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(100)
.execute(&json)
.expect("Parallel flatten failed");
let sequential_json = match sequential_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let parallel_json = match parallel_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let sequential_value: serde_json::Value = serde_json::from_str(&sequential_json).unwrap();
let parallel_value: serde_json::Value = serde_json::from_str(¶llel_json).unwrap();
assert_eq!(sequential_value, parallel_value);
}
#[test]
fn test_large_array_nested_parallelism() {
let mut json = String::from("[");
for i in 0..150 {
if i > 0 {
json.push(',');
}
json.push_str(&format!(
r#"{{"id": {}, "value": "item_{}", "nested": {{"data": "test_{}"}} }}"#,
i, i, i
));
}
json.push(']');
let sequential_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(usize::MAX)
.execute(&json)
.expect("Sequential flatten failed");
let parallel_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(100)
.execute(&json)
.expect("Parallel flatten failed");
let sequential_json = match sequential_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let parallel_json = match parallel_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let sequential_value: serde_json::Value = serde_json::from_str(&sequential_json).unwrap();
let parallel_value: serde_json::Value = serde_json::from_str(¶llel_json).unwrap();
assert_eq!(sequential_value, parallel_value);
}
#[test]
fn test_small_structures_stay_sequential() {
let json = r#"{
"small_object": {"a": 1, "b": 2, "c": 3},
"small_array": [1, 2, 3, 4, 5]
}"#;
let result = JSONTools::new()
.flatten()
.nested_parallel_threshold(10)
.execute(json)
.expect("Flatten failed");
match result {
JsonOutput::Single(s) => {
let value: serde_json::Value = serde_json::from_str(&s).unwrap();
assert!(value.is_object());
}
_ => panic!("Expected single output"),
}
}
#[test]
fn test_environment_variable_nested_threshold() {
std::env::set_var("JSON_TOOLS_NESTED_PARALLEL_THRESHOLD", "50");
let tools = JSONTools::new();
let json = r#"{"test": "value"}"#;
let result = tools.flatten().execute(json);
assert!(result.is_ok());
std::env::remove_var("JSON_TOOLS_NESTED_PARALLEL_THRESHOLD");
}
#[test]
fn test_deeply_nested_parallelism() {
let json = r#"{
"level1": {
"items": [
{"id": 1, "data": {"values": [1, 2, 3, 4, 5]}},
{"id": 2, "data": {"values": [6, 7, 8, 9, 10]}},
{"id": 3, "data": {"values": [11, 12, 13, 14, 15]}}
],
"metadata": {
"created": "2024-01-01",
"tags": ["a", "b", "c", "d", "e"]
}
}
}"#;
let sequential_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(usize::MAX)
.execute(json)
.expect("Sequential flatten failed");
let parallel_result = JSONTools::new()
.flatten()
.nested_parallel_threshold(2)
.execute(json)
.expect("Parallel flatten failed");
let sequential_json = match sequential_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let parallel_json = match parallel_result {
JsonOutput::Single(s) => s,
_ => panic!("Expected single output"),
};
let sequential_value: serde_json::Value = serde_json::from_str(&sequential_json).unwrap();
let parallel_value: serde_json::Value = serde_json::from_str(¶llel_json).unwrap();
assert_eq!(sequential_value, parallel_value);
}
}
#[cfg(test)]
mod parallel_regex_cache_tests {
use super::extract_single;
use crate::{JSONTools, JsonOutput};
use serde_json::Value;
#[test]
fn test_parallel_processing_with_regex_replacements() {
let json_docs: Vec<String> = (0..100)
.map(|i| {
format!(
r#"{{"user_id": {}, "admin_name": "Admin{}", "guest_email": "guest{}@example.com"}}"#,
i, i, i
)
})
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.key_replacement("r'(user|admin|guest)_'", "")
.value_replacement("r'@example\\.com'", "@company.org")
.parallel_threshold(10)
.execute(json_refs)
.expect("Parallel processing failed");
if let JsonOutput::Multiple(results) = result {
assert_eq!(results.len(), 100);
for (i, result_str) in results.iter().enumerate() {
let value: serde_json::Value =
serde_json::from_str(result_str).expect("Failed to parse result");
assert!(
value.as_object().unwrap().contains_key("id"),
"Should have 'id' key"
);
assert!(
value.as_object().unwrap().contains_key("name"),
"Should have 'name' key"
);
assert!(
value.as_object().unwrap().contains_key("email"),
"Should have 'email' key"
);
let email = value["email"].as_str().unwrap();
assert!(
email.contains("@company.org"),
"Email should be replaced: {} (item {})",
email,
i
);
}
} else {
panic!("Expected Multiple output");
}
}
#[test]
fn test_parallel_vs_sequential_consistency() {
let json_docs: Vec<String> = (0..50)
.map(|i| {
format!(
r#"{{"temp_data": "value{}", "old_field": "test{}", "user_name": "User{}"}}"#,
i, i, i
)
})
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let sequential_result = JSONTools::new()
.flatten()
.key_replacement("r'^(temp|old|user)_'", "new_")
.value_replacement("^test", "updated")
.parallel_threshold(usize::MAX) .execute(json_refs.clone())
.expect("Sequential processing failed");
let parallel_result = JSONTools::new()
.flatten()
.key_replacement("r'^(temp|old|user)_'", "new_")
.value_replacement("^test", "updated")
.parallel_threshold(1) .execute(json_refs)
.expect("Parallel processing failed");
let (seq_results, par_results) = match (sequential_result, parallel_result) {
(JsonOutput::Multiple(seq), JsonOutput::Multiple(par)) => (seq, par),
_ => panic!("Expected Multiple output"),
};
assert_eq!(seq_results.len(), par_results.len());
for (i, (seq, par)) in seq_results.iter().zip(par_results.iter()).enumerate() {
assert_eq!(seq, par, "Result {} should be identical", i);
}
}
#[test]
fn test_thread_local_cache_isolation() {
let json_docs: Vec<String> = (0..200)
.map(|i| {
format!(
r#"{{"field_{}_name": "value{}", "data_{}_id": {}}}"#,
i % 10,
i,
i % 5,
i
)
})
.collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.key_replacement("r'field_[0-9]+_'", "f_")
.key_replacement("r'data_[0-9]+_'", "d_")
.value_replacement("r'value([0-9]+)'", "val_$1")
.parallel_threshold(10)
.execute(json_refs)
.expect("Processing failed");
if let JsonOutput::Multiple(results) = result {
assert_eq!(results.len(), 200);
for result_str in results.iter() {
let value: serde_json::Value = serde_json::from_str(result_str).unwrap();
let obj = value.as_object().unwrap();
assert!(
obj.keys()
.any(|k| k.starts_with("f_") || k.starts_with("d_")),
"Keys should have replacements applied"
);
}
}
}
#[test]
fn test_adaptive_threshold_behavior() {
let small_batch: Vec<String> = (0..5).map(|i| format!(r#"{{"user_id": {}}}"#, i)).collect();
let small_refs: Vec<&str> = small_batch.iter().map(|s| s.as_str()).collect();
let small_result = JSONTools::new()
.flatten()
.key_replacement("user_", "")
.parallel_threshold(10)
.execute(small_refs)
.expect("Small batch failed");
let large_batch: Vec<String> = (0..50)
.map(|i| format!(r#"{{"user_id": {}}}"#, i))
.collect();
let large_refs: Vec<&str> = large_batch.iter().map(|s| s.as_str()).collect();
let large_result = JSONTools::new()
.flatten()
.key_replacement("user_", "")
.parallel_threshold(10)
.execute(large_refs)
.expect("Large batch failed");
assert!(matches!(small_result, JsonOutput::Multiple(_)));
assert!(matches!(large_result, JsonOutput::Multiple(_)));
}
#[test]
fn test_environment_variable_threshold() {
std::env::set_var("JSON_TOOLS_PARALLEL_THRESHOLD", "5");
let json_docs: Vec<String> = (0..10).map(|i| format!(r#"{{"id": {}}}"#, i)).collect();
let json_refs: Vec<&str> = json_docs.iter().map(|s| s.as_str()).collect();
let result = JSONTools::new()
.flatten()
.execute(json_refs)
.expect("Processing failed");
assert!(matches!(result, JsonOutput::Multiple(_)));
std::env::remove_var("JSON_TOOLS_PARALLEL_THRESHOLD");
}
#[test]
fn test_type_conversion_extended_booleans() {
let json = r#"{
"yes_variants": {"a": "yes", "b": "YES", "c": "Yes"},
"no_variants": {"a": "no", "b": "NO", "c": "No"},
"y_n": {"a": "y", "b": "Y", "c": "n", "d": "N"},
"on_off": {"a": "on", "b": "ON", "c": "off", "d": "OFF"},
"numeric": {"a": "1", "b": "0"}
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["yes_variants.a"], true);
assert_eq!(parsed["yes_variants.b"], true);
assert_eq!(parsed["yes_variants.c"], true);
assert_eq!(parsed["no_variants.a"], false);
assert_eq!(parsed["no_variants.b"], false);
assert_eq!(parsed["no_variants.c"], false);
assert_eq!(parsed["y_n.a"], true);
assert_eq!(parsed["y_n.b"], true);
assert_eq!(parsed["y_n.c"], false);
assert_eq!(parsed["y_n.d"], false);
assert_eq!(parsed["on_off.a"], true);
assert_eq!(parsed["on_off.b"], true);
assert_eq!(parsed["on_off.c"], false);
assert_eq!(parsed["on_off.d"], false);
assert_eq!(parsed["numeric.a"], 1);
assert_eq!(parsed["numeric.b"], 0);
}
#[test]
fn test_type_conversion_percentages() {
let json = r#"{
"percent1": "50%",
"percent2": "100%",
"percent3": "0.5%",
"percent4": "25.75%",
"negative": "-10%"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["percent1"], 50.0);
assert_eq!(parsed["percent2"], 100.0);
assert_eq!(parsed["percent3"], 0.5);
assert_eq!(parsed["percent4"], 25.75);
assert_eq!(parsed["negative"], -10.0);
}
#[test]
fn test_type_conversion_null_strings() {
let json = r#"{
"null_variants": {"a": "null", "b": "NULL", "c": "Null"},
"nil_variants": {"a": "nil", "b": "NIL", "c": "Nil"},
"none_variants": {"a": "none", "b": "NONE", "c": "None"},
"na_variants": {"a": "N/A", "b": "n/a", "c": "NA", "d": "na"}
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["null_variants.a"], Value::Null);
assert_eq!(parsed["null_variants.b"], Value::Null);
assert_eq!(parsed["null_variants.c"], Value::Null);
assert_eq!(parsed["nil_variants.a"], Value::Null);
assert_eq!(parsed["nil_variants.b"], Value::Null);
assert_eq!(parsed["nil_variants.c"], Value::Null);
assert_eq!(parsed["none_variants.a"], Value::Null);
assert_eq!(parsed["none_variants.b"], Value::Null);
assert_eq!(parsed["none_variants.c"], Value::Null);
assert_eq!(parsed["na_variants.a"], Value::Null);
assert_eq!(parsed["na_variants.b"], Value::Null);
assert_eq!(parsed["na_variants.c"], Value::Null);
assert_eq!(parsed["na_variants.d"], Value::Null);
}
#[test]
fn test_type_conversion_mixed_values() {
let json = r#"{
"number": "123",
"percent": "50%",
"bool_yes": "yes",
"bool_no": "no",
"null_str": "null",
"regular_string": "hello",
"empty": "",
"actual_null": null,
"currency": "$1,234.56"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["number"], 123);
assert_eq!(parsed["percent"], 50.0);
assert_eq!(parsed["bool_yes"], true);
assert_eq!(parsed["bool_no"], false);
assert_eq!(parsed["null_str"], Value::Null);
assert_eq!(parsed["regular_string"], "hello");
assert_eq!(parsed["empty"], ""); assert_eq!(parsed["actual_null"], Value::Null);
assert_eq!(parsed["currency"], 1234.56);
}
#[test]
fn test_type_conversion_priority() {
let json = r#"{
"null_variant": "N/A",
"bool_variant": "yes",
"number_variant": "123",
"one_digit": "1",
"zero_digit": "0"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["null_variant"], Value::Null); assert_eq!(parsed["bool_variant"], true); assert_eq!(parsed["number_variant"], 123);
assert_eq!(parsed["one_digit"], 1);
assert_eq!(parsed["zero_digit"], 0);
}
#[test]
fn test_type_conversion_no_false_positives() {
let json = r#"{
"not_null": "nullify",
"not_yes": "yesterday",
"not_no": "normal",
"not_percent": "percentage",
"not_number": "1 apple",
"word_on": "onboard",
"word_off": "offhand"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["not_null"], "nullify");
assert_eq!(parsed["not_yes"], "yesterday");
assert_eq!(parsed["not_no"], "normal");
assert_eq!(parsed["not_percent"], "percentage");
assert_eq!(parsed["not_number"], "1 apple");
assert_eq!(parsed["word_on"], "onboard");
assert_eq!(parsed["word_off"], "offhand");
}
#[test]
fn test_type_conversion_with_unflatten() {
let flattened = r#"{
"user.id": "123",
"user.active": "true",
"user.score": "95.5%",
"user.status": "null"
}"#;
let result = JSONTools::new()
.unflatten()
.auto_convert_types(true)
.execute(flattened)
.unwrap();
let unflattened = extract_single(result);
let parsed: Value = serde_json::from_str(&unflattened).unwrap();
assert_eq!(parsed["user"]["id"], 123);
assert_eq!(parsed["user"]["active"], true);
assert_eq!(parsed["user"]["score"], 95.5);
assert_eq!(parsed["user"]["status"], Value::Null);
}
#[test]
fn test_enhanced_number_formats() {
let json = r#"{
"usd": "$1,234.56",
"eur": "EUR 999.99",
"gbp": "£50.00",
"brl": "R$123.45",
"aud": "A$75.50",
"neg_standard": "-123.45",
"neg_accounting": "(456.78)",
"neg_trailing": "789.12-",
"neg_bracket": "[321.09]",
"underscore": "1_000_000",
"space_sep": "2 500 000",
"plus": "+42.5"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["usd"], 1234.56);
assert_eq!(parsed["eur"], 999.99);
assert_eq!(parsed["gbp"], 50.0);
assert_eq!(parsed["brl"], 123.45);
assert_eq!(parsed["aud"], 75.5);
assert_eq!(parsed["neg_standard"], -123.45);
assert_eq!(parsed["neg_accounting"], -456.78);
assert_eq!(parsed["neg_trailing"], -789.12);
assert_eq!(parsed["neg_bracket"], -321.09);
assert_eq!(parsed["underscore"], 1000000);
assert_eq!(parsed["space_sep"], 2500000);
assert_eq!(parsed["plus"], 42.5);
}
#[test]
fn test_auto_convert_types_and_remove_nulls_interaction() {
let json = r#"{
"actual_null": null,
"string_null": "null",
"string_na": "N/A",
"string_nil": "nil",
"string_none": "none",
"regular_string": "hello",
"number": "42",
"boolean": "true"
}"#;
let result1 = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened1 = extract_single(result1);
let parsed1: Value = serde_json::from_str(&flattened1).unwrap();
assert_eq!(parsed1["actual_null"], Value::Null);
assert_eq!(parsed1["string_null"], Value::Null); assert_eq!(parsed1["string_na"], Value::Null); assert_eq!(parsed1["string_nil"], Value::Null); assert_eq!(parsed1["string_none"], Value::Null); assert_eq!(parsed1["regular_string"], "hello");
assert_eq!(parsed1["number"], 42);
assert_eq!(parsed1["boolean"], true);
let result2 = JSONTools::new()
.flatten()
.remove_nulls(true)
.execute(json)
.unwrap();
let flattened2 = extract_single(result2);
let parsed2: Value = serde_json::from_str(&flattened2).unwrap();
assert!(parsed2.get("actual_null").is_none()); assert_eq!(parsed2["string_null"], "null"); assert_eq!(parsed2["string_na"], "N/A"); assert_eq!(parsed2["string_nil"], "nil"); assert_eq!(parsed2["string_none"], "none"); assert_eq!(parsed2["regular_string"], "hello");
assert_eq!(parsed2["number"], "42"); assert_eq!(parsed2["boolean"], "true");
let result3 = JSONTools::new()
.flatten()
.auto_convert_types(true)
.remove_nulls(true)
.execute(json)
.unwrap();
let flattened3 = extract_single(result3);
let parsed3: Value = serde_json::from_str(&flattened3).unwrap();
assert!(parsed3.get("actual_null").is_none()); assert!(parsed3.get("string_null").is_none()); assert!(parsed3.get("string_na").is_none()); assert!(parsed3.get("string_nil").is_none()); assert!(parsed3.get("string_none").is_none()); assert_eq!(parsed3["regular_string"], "hello"); assert_eq!(parsed3["number"], 42); assert_eq!(parsed3["boolean"], true);
assert_eq!(parsed3.as_object().unwrap().len(), 3); }
#[test]
fn test_suffixed_numbers() {
let json = r#"{
"thousand": "5K",
"thousand_lower": "2.5k",
"million": "3M",
"million_decimal": "1.5m",
"billion": "2B",
"trillion": "1T"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["thousand"], 5000.0);
assert_eq!(parsed["thousand_lower"], 2500.0);
assert_eq!(parsed["million"], 3000000.0);
assert_eq!(parsed["million_decimal"], 1500000.0);
assert_eq!(parsed["billion"], 2000000000.0);
assert_eq!(parsed["trillion"], 1000000000000.0);
}
#[test]
fn test_fractions() {
let json = r#"{
"half": "1/2",
"quarter": "1/4",
"three_quarters": "3/4",
"mixed_positive": "2 1/2",
"mixed_negative": "-1 1/2",
"negative_fraction": "-3/4"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["half"], 0.5);
assert_eq!(parsed["quarter"], 0.25);
assert_eq!(parsed["three_quarters"], 0.75);
assert_eq!(parsed["mixed_positive"], 2.5);
assert_eq!(parsed["mixed_negative"], -1.5);
assert_eq!(parsed["negative_fraction"], -0.75);
}
#[test]
fn test_radix_numbers() {
let json = r#"{
"hex_lower": "0xff",
"hex_upper": "0xFF",
"hex_large": "0x1A2B",
"binary": "0b1010",
"binary_upper": "0B1111",
"octal": "0o777",
"octal_upper": "0O755",
"negative_hex": "-0x10"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["hex_lower"], 255.0);
assert_eq!(parsed["hex_upper"], 255.0);
assert_eq!(parsed["hex_large"], 6699.0); assert_eq!(parsed["binary"], 10.0); assert_eq!(parsed["binary_upper"], 15.0); assert_eq!(parsed["octal"], 511.0); assert_eq!(parsed["octal_upper"], 493.0); assert_eq!(parsed["negative_hex"], -16.0); }
#[test]
fn test_basis_points() {
let json = r#"{
"bp_suffix": "25bp",
"bps_suffix": "50bps",
"bp_space": "100 bp",
"bps_space": "75 bps"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["bp_suffix"], 0.0025); assert_eq!(parsed["bps_suffix"], 0.005); assert_eq!(parsed["bp_space"], 0.01); assert_eq!(parsed["bps_space"], 0.0075); }
#[test]
fn test_permille() {
let json = r#"{
"permille": "5‰",
"per_ten_thousand": "25‱"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["permille"], 0.005); assert_eq!(parsed["per_ten_thousand"], 0.0025); }
#[test]
fn test_indian_numbering() {
let json = r#"{
"one_lakh": "1,00,000",
"ten_lakh": "10,00,000",
"one_crore": "1,00,00,000",
"mixed": "12,34,567"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["one_lakh"], 100000.0); assert_eq!(parsed["ten_lakh"], 1000000.0); assert_eq!(parsed["one_crore"], 10000000.0); assert_eq!(parsed["mixed"], 1234567.0); }
#[test]
fn test_iso8601_date_detection() {
let json = r#"{
"date_only": "2024-01-15",
"datetime_utc": "2024-01-15T10:30:00Z",
"datetime_no_tz": "2024-01-15T10:30:00",
"datetime_with_offset": "2024-01-15T10:30:00+05:00",
"datetime_negative_offset": "2024-01-15T10:30:00-08:00",
"not_a_date": "2024-13-45",
"regular_number": "12345"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["date_only"], "2024-01-15");
assert_eq!(parsed["datetime_utc"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["datetime_no_tz"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["datetime_with_offset"], "2024-01-15T05:30:00Z");
assert_eq!(parsed["datetime_negative_offset"], "2024-01-15T18:30:00Z");
assert_eq!(parsed["not_a_date"], "2024-13-45");
assert_eq!(parsed["regular_number"], 12345);
}
#[test]
fn test_iso8601_datetime_with_fractional_seconds() {
let json = r#"{
"with_millis": "2024-06-15T14:30:45.123Z",
"with_micros": "2024-06-15T14:30:45.123456Z",
"no_tz_with_millis": "2024-06-15T14:30:45.500"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["with_millis"], "2024-06-15T14:30:45Z");
assert_eq!(parsed["with_micros"], "2024-06-15T14:30:45Z");
assert_eq!(parsed["no_tz_with_millis"], "2024-06-15T14:30:45Z");
}
#[test]
fn test_iso8601_space_separator() {
let json = r#"{
"space_datetime": "2024-01-15 10:30:00",
"space_with_tz": "2024-01-15 10:30:00+02:00"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["space_datetime"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["space_with_tz"], "2024-01-15T08:30:00Z");
}
#[test]
fn test_date_not_confused_with_numbers() {
let json = r#"{
"date": "2024-01-15",
"looks_like_math": "2024-01-15",
"timestamp": "2024-12-25T00:00:00Z"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert!(parsed["date"].is_string());
assert!(parsed["looks_like_math"].is_string());
assert!(parsed["timestamp"].is_string());
assert_eq!(parsed["date"], "2024-01-15");
assert_eq!(parsed["timestamp"], "2024-12-25T00:00:00Z");
}
#[test]
fn test_compact_date_format() {
let json = r#"{
"compact_date": "20240115",
"compact_datetime": "20240115T103000",
"compact_datetime_z": "20240115T103000Z",
"compact_with_offset": "20240115T103000+0530"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["compact_date"], "2024-01-15");
assert_eq!(parsed["compact_datetime"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["compact_datetime_z"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["compact_with_offset"], "2024-01-15T05:00:00Z");
}
#[test]
fn test_ordinal_date_format() {
let json = r#"{
"jan_15": "2024-015",
"dec_31": "2024-366",
"leap_year": "2024-060"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["jan_15"], "2024-01-15");
assert_eq!(parsed["dec_31"], "2024-12-31"); assert_eq!(parsed["leap_year"], "2024-02-29"); }
#[test]
fn test_week_date_format() {
let json = r#"{
"week_with_day": "2024-W03-1",
"week_friday": "2024-W03-5"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["week_with_day"], "2024-01-15"); assert_eq!(parsed["week_friday"], "2024-01-19"); }
#[test]
fn test_alternate_separators() {
let json = r#"{
"slash_date": "2024/01/15",
"dot_date": "2024.01.15",
"slash_datetime": "2024/01/15T10:30:00Z",
"dot_datetime": "2024.01.15T10:30:00+02:00"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["slash_date"], "2024-01-15");
assert_eq!(parsed["dot_date"], "2024-01-15");
assert_eq!(parsed["slash_datetime"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["dot_datetime"], "2024-01-15T08:30:00Z");
}
#[test]
fn test_timezone_offset_variants() {
let json = r#"{
"colon_offset": "2024-01-15T10:30:00+05:30",
"no_colon_offset": "2024-01-15T10:30:00+0530",
"hour_only_offset": "2024-01-15T10:30:00+05",
"negative_offset": "2024-01-15T10:30:00-0800",
"zulu": "2024-01-15T10:30:00Z"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["colon_offset"], "2024-01-15T05:00:00Z"); assert_eq!(parsed["no_colon_offset"], "2024-01-15T05:00:00Z"); assert_eq!(parsed["hour_only_offset"], "2024-01-15T05:30:00Z"); assert_eq!(parsed["negative_offset"], "2024-01-15T18:30:00Z"); assert_eq!(parsed["zulu"], "2024-01-15T10:30:00Z");
}
#[test]
fn test_datetime_without_seconds() {
let json = r#"{
"no_seconds": "2024-01-15T10:30",
"no_seconds_z": "2024-01-15T10:30Z"
}"#;
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["no_seconds"], "2024-01-15T10:30:00Z");
assert_eq!(parsed["no_seconds_z"], "2024-01-15T10:30:00Z");
}
}
#[cfg(test)]
mod json_output_tests {
use crate::JsonOutput;
#[test]
fn test_try_into_single_ok() {
let output = JsonOutput::Single(r#"{"a":1}"#.to_string());
assert_eq!(output.try_into_single().unwrap(), r#"{"a":1}"#);
}
#[test]
fn test_try_into_single_err() {
let output = JsonOutput::Multiple(vec![r#"{"a":1}"#.to_string()]);
assert!(output.try_into_single().is_err());
}
#[test]
fn test_try_into_multiple_ok() {
let output = JsonOutput::Multiple(vec![r#"{"a":1}"#.to_string()]);
assert_eq!(output.try_into_multiple().unwrap(), vec![r#"{"a":1}"#]);
}
#[test]
fn test_try_into_multiple_err() {
let output = JsonOutput::Single(r#"{"a":1}"#.to_string());
assert!(output.try_into_multiple().is_err());
}
}
#[cfg(test)]
mod max_array_index_tests {
use crate::JSONTools;
#[test]
fn test_max_array_index_rejects_huge_index() {
let json = r#"{"items.999999999": "value"}"#;
let result = JSONTools::new().unflatten().execute(json);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("exceeds maximum"));
}
#[test]
fn test_max_array_index_accepts_small_index() {
let json = r#"{"items.5": "value"}"#;
let result = JSONTools::new().unflatten().execute(json);
assert!(result.is_ok());
}
#[test]
fn test_max_array_index_custom_limit() {
let json = r#"{"items.11": "value"}"#;
let result = JSONTools::new()
.unflatten()
.max_array_index(10)
.execute(json);
assert!(result.is_err());
let json_ok = r#"{"items.9": "value"}"#;
let result_ok = JSONTools::new()
.unflatten()
.max_array_index(10)
.execute(json_ok);
assert!(result_ok.is_ok());
}
}
#[cfg(test)]
mod validation_and_edge_case_tests {
use crate::tests::extract_single;
use crate::JSONTools;
use serde_json::Value;
#[test]
fn test_empty_separator_returns_error() {
let result = JSONTools::new()
.flatten()
.separator("")
.execute(r#"{"a": 1}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Separator cannot be empty"));
}
#[test]
fn test_num_threads_zero_returns_error() {
let result = JSONTools::new()
.flatten()
.num_threads(Some(0))
.execute(r#"{"a": 1}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("num_threads must be at least 1"));
}
#[test]
fn test_execute_without_mode_returns_error() {
let result = JSONTools::new().execute(r#"{"a": 1}"#);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Operation mode not set"));
}
#[test]
fn test_max_array_index_enforcement() {
let json = r#"{"items.999999999": "value"}"#;
let result = JSONTools::new().unflatten().execute(json);
assert!(result.is_err());
}
#[test]
fn test_unicode_keys_flatten() {
let json = r#"{"用户": {"名前": "太郎", "émoji": "🎉"}}"#;
let result = JSONTools::new().flatten().execute(json).unwrap();
let flattened = extract_single(result);
let parsed: Value = serde_json::from_str(&flattened).unwrap();
assert_eq!(parsed["用户.名前"], "太郎");
assert_eq!(parsed["用户.émoji"], "🎉");
}
#[test]
fn test_unicode_keys_roundtrip() {
let json = r#"{"café": {"naïve": "résumé"}, "日本語": {"キー": "値"}}"#;
let flattened = JSONTools::new().flatten().execute(json).unwrap();
let flat_str = extract_single(flattened);
let unflattened = JSONTools::new()
.unflatten()
.execute(flat_str.as_str())
.unwrap();
let result = extract_single(unflattened);
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["café"]["naïve"], "résumé");
assert_eq!(parsed["日本語"]["キー"], "値");
}
#[test]
fn test_unicode_value_with_escape_sequence() {
let json =
r#"{"note": "café\nrésumé", "emoji": "hi 👍\tthere", "mixed": "日本語\"quoted\""}"#;
let flattened = JSONTools::new().flatten().execute(json).unwrap();
let flat_str = extract_single(flattened);
let parsed: Value = serde_json::from_str(&flat_str).unwrap();
assert_eq!(parsed["note"], "café\nrésumé");
assert_eq!(parsed["emoji"], "hi 👍\tthere");
assert_eq!(parsed["mixed"], "日本語\"quoted\"");
}
#[test]
fn test_escaped_value_falls_through_replacement_to_auto_convert() {
let json = r#"{"Count": "123\t", "Label": "hello\nworld"}"#;
let direct = JSONTools::new()
.flatten()
.value_replacement("NOMATCH", "x")
.auto_convert_types(true)
.execute(json)
.unwrap();
let direct_parsed: Value = serde_json::from_str(&extract_single(direct)).unwrap();
assert_eq!(direct_parsed["Count"], 123);
assert_eq!(direct_parsed["Label"], "hello\nworld");
let collecting = JSONTools::new()
.flatten()
.value_replacement("NOMATCH", "x")
.auto_convert_types(true)
.lowercase_keys(true)
.execute(json)
.unwrap();
let collecting_parsed: Value = serde_json::from_str(&extract_single(collecting)).unwrap();
assert_eq!(collecting_parsed["count"], 123);
assert_eq!(collecting_parsed["label"], "hello\nworld");
}
#[test]
fn test_scanner_detects_non_quote_escapes() {
let cases = [
(r#"{"n": "42\t"}"#, "n", 42),
(r#"{"n": "42\n"}"#, "n", 42),
(r#"{"n": "42\r"}"#, "n", 42),
(r#"{"n": "\t42"}"#, "n", 42),
(r#"{"n": "\n\r\t42\n\r\t"}"#, "n", 42),
];
for (json, key, expected) in cases {
let result = JSONTools::new()
.flatten()
.auto_convert_types(true)
.execute(json)
.unwrap();
let parsed: Value = serde_json::from_str(&extract_single(result)).unwrap();
assert_eq!(
parsed[key], expected,
"failed to auto-convert {json:?} (has_escape must be true for whitespace-only escapes)"
);
}
let json = "{\"Name\": \"Caf\\u00e9\"}";
let result = JSONTools::new()
.flatten()
.value_replacement("Café", "Kaffee")
.execute(json)
.unwrap();
let parsed: Value = serde_json::from_str(&extract_single(result)).unwrap();
assert_eq!(parsed["Name"], "Kaffee");
}
#[test]
fn test_normal_mode_lowercase_keys() {
let json = r#"{"UserName": "John", "UserAge": 30, "nested": {"InnerKey": true}}"#;
let result = JSONTools::new()
.normal()
.lowercase_keys(true)
.execute(json)
.unwrap();
let output = extract_single(result);
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["username"], "John");
assert_eq!(parsed["userage"], 30);
assert_eq!(parsed["nested"]["innerkey"], true);
}
#[test]
fn test_normal_mode_auto_convert_types() {
let json = r#"{"count": "42", "active": "true", "rate": "3.14"}"#;
let result = JSONTools::new()
.normal()
.auto_convert_types(true)
.execute(json)
.unwrap();
let output = extract_single(result);
let parsed: Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["count"], 42);
assert_eq!(parsed["active"], true);
assert!(parsed["rate"].is_f64());
}
#[test]
fn test_normal_mode_filtering() {
let json = r#"{"a": "", "b": null, "c": {}, "d": [], "e": "keep"}"#;
let result = JSONTools::new()
.normal()
.remove_empty_strings(true)
.remove_nulls(true)
.remove_empty_objects(true)
.remove_empty_arrays(true)
.execute(json)
.unwrap();
let output = extract_single(result);
let parsed: Value = serde_json::from_str(&output).unwrap();
assert!(parsed.get("a").is_none());
assert!(parsed.get("b").is_none());
assert!(parsed.get("c").is_none());
assert!(parsed.get("d").is_none());
assert_eq!(parsed["e"], "keep");
}
#[test]
fn test_batch_processing_error_includes_index() {
let inputs: Vec<&str> = vec![r#"{"a": 1}"#, "not valid json", r#"{"b": 2}"#];
let result = JSONTools::new().flatten().execute(inputs.as_slice());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("1") || err.to_string().contains("index"));
}
#[test]
fn test_json_output_try_into_single() {
let result = JSONTools::new().flatten().execute(r#"{"a": 1}"#).unwrap();
assert!(result.try_into_single().is_ok());
}
#[test]
fn test_json_output_try_into_multiple_from_single_errors() {
let result = JSONTools::new().flatten().execute(r#"{"a": 1}"#).unwrap();
assert!(result.try_into_multiple().is_err());
}
#[test]
fn test_json_output_into_vec_single() {
let result = JSONTools::new().flatten().execute(r#"{"a": 1}"#).unwrap();
let vec = result.into_vec();
assert_eq!(vec.len(), 1);
}
}