use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
use zparse::{Value, from_str};
fn serialize_value(value: &Value) -> String {
match value {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => {
if !n.is_finite() {
"null".to_string()
} else if *n == 0.0 && n.is_sign_negative() {
"-0.0".to_string()
} else if n.fract() == 0.0 {
format!("{:.0}", n)
} else {
format!("{}", n)
}
}
Value::String(s) => format!("\"{}\"", escape_string(s)),
Value::Array(arr) => {
let elements: Vec<String> = arr.iter().map(serialize_value).collect();
format!("[{}]", elements.join(","))
}
Value::Object(obj) => {
let pairs: Vec<String> = obj
.iter()
.map(|(k, v)| format!("\"{}\":{}", escape_string(k), serialize_value(v)))
.collect();
format!("{{{}}}", pairs.join(","))
}
Value::Datetime(dt) => format!("\"{}\"", format_toml_datetime(dt)),
}
}
fn format_toml_datetime(dt: &zparse::TomlDatetime) -> String {
use time::format_description::well_known::Rfc3339;
use time::macros::format_description;
match dt {
zparse::TomlDatetime::OffsetDateTime(value) => value
.format(&Rfc3339)
.unwrap_or_else(|_| "1979-05-27T07:32:00Z".to_string()),
zparse::TomlDatetime::LocalDateTime(value) => value
.format(&format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second]"
))
.unwrap_or_else(|_| "1979-05-27T07:32:00".to_string()),
zparse::TomlDatetime::LocalDate(value) => value
.format(&format_description!("[year]-[month]-[day]"))
.unwrap_or_else(|_| "1979-05-27".to_string()),
zparse::TomlDatetime::LocalTime(value) => value
.format(&format_description!("[hour]:[minute]:[second]"))
.unwrap_or_else(|_| "07:32:00".to_string()),
}
}
fn escape_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\x08' => result.push_str("\\b"), '\x0C' => result.push_str("\\f"), '\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_control() => {
result.push_str(&format!("\\u{:04x}", u32::from(c)));
}
c => result.push(c),
}
}
result
}
fn arb_json_string() -> impl Strategy<Value = String> {
"[a-zA-Z0-9_]*".prop_map(|s| s)
}
fn arb_json_value() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
(-1e6f64..1e6f64)
.prop_filter("Non-finite f64", |f| f.is_finite())
.prop_map(Value::Number),
arb_json_string().prop_map(Value::String),
];
leaf.prop_recursive(8, 256, 10, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..10).prop_map(|v| Value::Array(v.into())),
prop::collection::hash_map(arb_json_string(), inner, 0..10)
.prop_map(|m| Value::Object(m.into_iter().collect())),
]
})
}
fn parse_or_fail(s: &str) -> Result<Value, TestCaseError> {
from_str(s).map_err(|err| TestCaseError::fail(format!("parse failed: {err}")))
}
proptest! {
#[test]
fn json_roundtrip(value in arb_json_value()) {
let serialized = serialize_value(&value);
let parsed = parse_or_fail(&serialized)?;
assert_values_equal(&parsed, &value)?;
}
#[test]
fn valid_json_parses(s in r#"\{(("[a-z0-9]+":[0-9]+)(,("[a-z0-9]+":[0-9]+))*)?\}"#) {
// This generates simple valid JSON patterns
let _result = from_str(&s);
// Should not panic - we don't care about the result
}
/// Test that arrays roundtrip correctly
#[test]
fn array_roundtrip(arr in prop::collection::vec(arb_json_value(), 0..20)) {
let value = Value::Array(arr.into());
let serialized = serialize_value(&value);
let parsed = parse_or_fail(&serialized)?;
assert_values_equal(&parsed, &value)?;
}
/// Test that objects roundtrip correctly
#[test]
fn object_roundtrip(obj in prop::collection::hash_map(arb_json_string(), arb_json_value(), 0..20)) {
let value: Value = obj.into_iter().collect::<zparse::Object>().into();
let serialized = serialize_value(&value);
let parsed = parse_or_fail(&serialized)?;
assert_values_equal(&parsed, &value)?;
}
}
/// Compare two values, handling float comparisons with tolerance
fn assert_values_equal(a: &Value, b: &Value) -> Result<(), TestCaseError> {
match (a, b) {
(Value::Null, Value::Null) => Ok(()),
(Value::Bool(a1), Value::Bool(b1)) => {
if a1 == b1 {
Ok(())
} else {
Err(TestCaseError::fail(format!(
"Bools not equal: {a1} vs {b1}"
)))
}
}
(Value::Number(a1), Value::Number(b1)) => {
// Use relative tolerance for float comparison
if (a1 - b1).abs() > 1e-10 * a1.abs().max(b1.abs()).max(1.0) {
return Err(TestCaseError::fail(format!(
"Numbers not equal: {a1} vs {b1}"
)));
}
Ok(())
}
(Value::String(a1), Value::String(b1)) => {
if a1 == b1 {
Ok(())
} else {
Err(TestCaseError::fail(format!(
"Strings not equal: {a1} vs {b1}"
)))
}
}
(Value::Array(a1), Value::Array(b1)) => {
if a1.len() != b1.len() {
return Err(TestCaseError::fail("Array lengths differ"));
}
for (ae, be) in a1.iter().zip(b1.iter()) {
assert_values_equal(ae, be)?;
}
Ok(())
}
(Value::Object(a1), Value::Object(b1)) => {
if a1.len() != b1.len() {
return Err(TestCaseError::fail("Object lengths differ"));
}
for (key, a_val) in a1.iter() {
let b_val = match b1.get(key) {
Some(value) => value,
None => {
return Err(TestCaseError::fail(format!(
"Key '{key}' missing in second object"
)));
}
};
assert_values_equal(a_val, b_val)?;
}
Ok(())
}
_ => Err(TestCaseError::fail(format!(
"Value types differ: {a:?} vs {b:?}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ensure_eq<T: PartialEq + std::fmt::Debug>(left: T, right: T) -> Result<(), TestCaseError> {
if left == right {
Ok(())
} else {
Err(TestCaseError::fail(format!(
"assertion failed: left={left:?} right={right:?}"
)))
}
}
#[test]
fn test_serialize_null() -> Result<(), TestCaseError> {
ensure_eq(serialize_value(&Value::Null), "null".to_string())?;
Ok(())
}
#[test]
fn test_serialize_bool() -> Result<(), TestCaseError> {
ensure_eq(serialize_value(&Value::Bool(true)), "true".to_string())?;
ensure_eq(serialize_value(&Value::Bool(false)), "false".to_string())?;
Ok(())
}
#[test]
fn test_serialize_number() -> Result<(), TestCaseError> {
let pi = std::f64::consts::PI;
ensure_eq(serialize_value(&Value::Number(42.0)), "42".to_string())?;
ensure_eq(serialize_value(&Value::Number(pi)), format!("{pi}"))?;
ensure_eq(serialize_value(&Value::Number(-123.0)), "-123".to_string())?;
Ok(())
}
#[test]
fn test_serialize_string() -> Result<(), TestCaseError> {
ensure_eq(
serialize_value(&Value::String("hello".to_string())),
"\"hello\"".to_string(),
)?;
ensure_eq(
serialize_value(&Value::String("hello world".to_string())),
"\"hello world\"".to_string(),
)?;
Ok(())
}
#[test]
fn test_serialize_string_escaping() -> Result<(), TestCaseError> {
ensure_eq(
serialize_value(&Value::String("hello\nworld".to_string())),
"\"hello\\nworld\"".to_string(),
)?;
ensure_eq(
serialize_value(&Value::String("hello\"world\"".to_string())),
"\"hello\\\"world\\\"\"".to_string(),
)?;
ensure_eq(
serialize_value(&Value::String("hello\\world".to_string())),
"\"hello\\\\world\"".to_string(),
)?;
Ok(())
}
#[test]
fn test_serialize_array() -> Result<(), TestCaseError> {
let arr = Value::Array(vec![Value::Null, Value::Bool(true), Value::Number(42.0)].into());
ensure_eq(serialize_value(&arr), "[null,true,42]".to_string())?;
Ok(())
}
#[test]
fn test_serialize_object() -> Result<(), TestCaseError> {
use zparse::Object;
let mut obj = Object::new();
obj.insert("name", Value::String("test".to_string()));
obj.insert("value", Value::Number(123.0));
ensure_eq(
serialize_value(&Value::Object(obj)),
"{\"name\":\"test\",\"value\":123}".to_string(),
)?;
Ok(())
}
#[test]
fn test_simple_roundtrip() -> Result<(), TestCaseError> {
let json = r#"{"name": "test", "value": 123}"#;
let parsed = parse_or_fail(json)?;
let serialized = serialize_value(&parsed);
let reparsed = parse_or_fail(&serialized)?;
assert_values_equal(&parsed, &reparsed)?;
Ok(())
}
#[test]
fn test_roundtrip_nested() -> Result<(), TestCaseError> {
let json = r#"{"outer": {"inner": [1, 2, 3], "flag": true}}"#;
let parsed = parse_or_fail(json)?;
let serialized = serialize_value(&parsed);
let reparsed = parse_or_fail(&serialized)?;
assert_values_equal(&parsed, &reparsed)?;
Ok(())
}
}