lino-objects-codec 0.8.0

A library to encode/decode objects to/from links notation
Documentation
#![cfg(feature = "serde_json")]

use lino_objects_codec::json::{JsonOptions, from_json_with_options, json_to_lino, lino_to_json};
use lino_objects_codec::{LinoValue, decode, encode, encode_compact, encode_line};
use proptest::prelude::*;
use serde_json::Value;

fn assert_roundtrip(json: &Value) {
    let expected = serde_json::to_string(&json).unwrap();
    let value = LinoValue::from(json);
    assert_eq!(
        serde_json::to_string(&Value::try_from(&value).unwrap()).unwrap(),
        expected
    );
    for text in [encode(&value), encode_line(&value), encode_compact(&value)] {
        let result = Value::try_from(decode(&text).unwrap()).unwrap();
        assert_eq!(serde_json::to_string(&result).unwrap(), expected, "{text}");
    }
    assert_eq!(
        serde_json::to_string(&lino_to_json(&json_to_lino(json)).unwrap()).unwrap(),
        expected
    );
}

#[test]
fn key_order_number_text_and_empty_values_survive() {
    let value: Value = serde_json::from_str(r#"{"z":1.2300e+45,"a":18446744073709551616,"negative":-0.0,"null":null,"array":[],"object":{},"text":"","false":false}"#).unwrap();
    assert_roundtrip(&value);
    for number in [
        "-0",
        "1.0000",
        "1e400",
        "-999999999999999999999999999999999999",
        "0.0000000000000000000000000000001",
    ] {
        assert_roundtrip(&serde_json::from_str(number).unwrap());
    }
}

#[test]
fn stripping_is_explicit_and_recursive() {
    let value: Value = serde_json::from_str(
        r#"{"null":null,"array":[null,{},[],false,0,""],"nested":{"empty":{}},"object":{}}"#,
    )
    .unwrap();
    assert_roundtrip(&value);
    let stripped = from_json_with_options(value, JsonOptions { strip_empty: true });
    assert_eq!(
        Value::try_from(stripped).unwrap().to_string(),
        r#"{"array":[false,0,""]}"#
    );
    assert_eq!(
        from_json_with_options(serde_json::json!([]), JsonOptions { strip_empty: true }),
        LinoValue::Null
    );
}

#[test]
fn conversion_rejects_non_json_floats_and_duplicate_keys() {
    for number in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        assert!(Value::try_from(LinoValue::Float(number)).is_err());
    }
    assert!(Value::try_from(LinoValue::object([("a", 1), ("a", 2)])).is_err());
    assert_eq!(
        Value::try_from(LinoValue::Int(42)).unwrap(),
        serde_json::json!(42)
    );
}

fn arbitrary_json() -> impl Strategy<Value = Value> {
    let scalar = prop_oneof![
        Just(Value::Null),
        any::<bool>().prop_map(Value::Bool),
        any::<i64>().prop_map(|v| serde_json::from_str::<Value>(&v.to_string()).unwrap()),
        (any::<i64>(), 0u16..500)
            .prop_map(|(n, e)| serde_json::from_str::<Value>(&format!("{n}.000e+{e}")).unwrap()),
        prop::collection::vec(any::<char>(), 0..48)
            .prop_map(|v| Value::String(v.into_iter().collect())),
    ];
    scalar.prop_recursive(4, 64, 8, |inner| {
        prop_oneof![
            prop::collection::vec(inner.clone(), 0..8).prop_map(Value::Array),
            prop::collection::vec((".{0,12}", inner), 0..8)
                .prop_map(|pairs| Value::Object(pairs.into_iter().collect())),
        ]
    })
}

proptest! {
    #![proptest_config(ProptestConfig { cases: 512, .. ProptestConfig::default() })]
    #[test]
    fn arbitrary_json_roundtrips(json in arbitrary_json()) { assert_roundtrip(&json); }
}