tru 0.2.3

TOON reference implementation in Rust (JSON <-> TOON)
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use serde::Deserialize;

use toon::encode::encode;
use toon::options::{EncodeOptions, KeyFoldingMode};

#[derive(Debug, Deserialize)]
struct FixtureFile {
    tests: Vec<FixtureCase>,
}

#[derive(Debug, Deserialize)]
struct FixtureCase {
    name: String,
    input: serde_json::Value,
    expected: String,
    options: Option<serde_json::Value>,
}

#[test]
fn encode_spec_fixtures() {
    let root = fixture_root();
    let encode_dir = root.join("encode");
    let files = load_fixture_files(&encode_dir);

    assert!(!files.is_empty(), "no encode fixtures found");

    for file in files {
        for case in file.tests {
            run_case(&case);
        }
    }
}

fn run_case(case: &FixtureCase) {
    let options = parse_encode_options(case.options.as_ref());
    let output = encode(case.input.clone(), options);

    assert_eq!(
        output, case.expected,
        "fixture '{}' failed (expected {:?}, got {:?})",
        case.name, case.expected, output
    );
}

fn parse_encode_options(options: Option<&serde_json::Value>) -> Option<EncodeOptions> {
    let options = options?;

    let indent = options
        .get("indent")
        .and_then(serde_json::Value::as_u64)
        .and_then(|value| usize::try_from(value).ok());

    let delimiter = options
        .get("delimiter")
        .and_then(serde_json::Value::as_str)
        .and_then(|value| value.chars().next());

    let key_folding = options
        .get("keyFolding")
        .and_then(serde_json::Value::as_str)
        .and_then(|value| match value {
            "safe" => Some(KeyFoldingMode::Safe),
            "off" => Some(KeyFoldingMode::Off),
            _ => None,
        });

    let flatten_depth = options
        .get("flattenDepth")
        .and_then(serde_json::Value::as_u64)
        .and_then(|value| usize::try_from(value).ok());

    Some(EncodeOptions {
        indent,
        delimiter,
        key_folding,
        flatten_depth,
        replacer: None,
    })
}

fn fixture_root() -> PathBuf {
    if let Ok(path) = std::env::var("TOON_SPEC_FIXTURES") {
        return PathBuf::from(path);
    }

    PathBuf::from("tests/fixtures/spec")
}

fn load_fixture_files(dir: &Path) -> Vec<FixtureFile> {
    if !dir.exists() {
        return Vec::new();
    }

    let mut fixtures = Vec::new();
    let Ok(entries) = fs::read_dir(dir) else {
        return fixtures;
    };

    for entry in entries.flatten() {
        let path = entry.path();
        if !path.is_file() {
            continue;
        }
        if path.extension().and_then(|s| s.to_str()) != Some("json") {
            continue;
        }

        let Ok(contents) = fs::read_to_string(&path) else {
            continue;
        };

        let parsed: FixtureFile = match serde_json::from_str(&contents) {
            Ok(parsed) => parsed,
            Err(_) => continue,
        };

        fixtures.push(parsed);
    }

    fixtures
}