tru 0.2.3

TOON reference implementation in Rust (JSON <-> TOON)
Documentation
use toon::cli::conversion::{decode_to_json_chunks, encode_to_toon_lines};
use toon::options::{DecodeOptions, EncodeOptions, ExpandPathsMode, KeyFoldingMode};

#[test]
fn encode_to_toon_lines_matches_encode() {
    let input = r#"{"name":"Ada","id":1}"#;
    let expected = toon::encode::encode(
        toon::JsonValue::from(serde_json::from_str::<serde_json::Value>(input).unwrap()),
        None,
    );
    let actual = encode_to_toon_lines(input, None).unwrap().join("\n");
    assert_eq!(actual, expected);
}

#[test]
fn encode_to_toon_lines_rejects_invalid_json() {
    let input = r#"{"name": }"#;
    let err = encode_to_toon_lines(input, None).unwrap_err();
    assert!(err.to_string().contains("Failed to parse JSON"));
}

#[test]
fn decode_to_json_chunks_streams_without_expand_paths() {
    let input = "items[2]: a,b";
    let options = DecodeOptions {
        indent: Some(2),
        strict: Some(true),
        expand_paths: Some(ExpandPathsMode::Off),
    };

    let output = decode_to_json_chunks(input, Some(options))
        .unwrap()
        .concat();
    let expected = serde_json::to_string_pretty(&serde_json::json!({"items": ["a", "b"]})).unwrap();
    assert_eq!(output, expected);
}

#[test]
fn decode_to_json_chunks_expands_paths_when_enabled() {
    let input = "a.b: 1";
    let options = DecodeOptions {
        indent: Some(0),
        strict: Some(true),
        expand_paths: Some(ExpandPathsMode::Safe),
    };

    let output = decode_to_json_chunks(input, Some(options))
        .unwrap()
        .concat();
    let expected = serde_json::json!({"a": {"b": 1}});
    let actual: serde_json::Value = serde_json::from_str(&output).unwrap();
    assert_json_eq(&actual, &expected);
}

#[test]
fn encode_to_toon_lines_respects_options() {
    let input = r#"{"data":{"meta":{"items":["x","y"]}}}"#;
    let options = EncodeOptions {
        indent: Some(2),
        delimiter: Some(','),
        key_folding: Some(KeyFoldingMode::Safe),
        flatten_depth: Some(usize::MAX),
        replacer: None,
    };

    let output = encode_to_toon_lines(input, Some(options))
        .unwrap()
        .join("\n");
    assert_eq!(output, "data.meta.items[2]: x,y");
}

fn assert_json_eq(actual: &serde_json::Value, expected: &serde_json::Value) {
    if json_eq(actual, expected) {
        return;
    }
    panic!("expected {expected:?}, got {actual:?}");
}

fn json_eq(left: &serde_json::Value, right: &serde_json::Value) -> bool {
    match (left, right) {
        (serde_json::Value::Null, serde_json::Value::Null) => true,
        (serde_json::Value::Bool(a), serde_json::Value::Bool(b)) => a == b,
        (serde_json::Value::String(a), serde_json::Value::String(b)) => a == b,
        (serde_json::Value::Number(a), serde_json::Value::Number(b)) => {
            match (a.as_f64(), b.as_f64()) {
                (Some(a), Some(b)) => (a - b).abs() <= 1e-12,
                _ => false,
            }
        }
        (serde_json::Value::Array(a), serde_json::Value::Array(b)) => {
            if a.len() != b.len() {
                return false;
            }
            a.iter().zip(b.iter()).all(|(a, b)| json_eq(a, b))
        }
        (serde_json::Value::Object(a), serde_json::Value::Object(b)) => {
            if a.len() != b.len() {
                return false;
            }
            a.iter()
                .all(|(key, value)| b.get(key).is_some_and(|other| json_eq(value, other)))
        }
        _ => false,
    }
}