sheeter-engine 1.1.0

Convert, inspect, and transform data files across ten formats with streaming where it matters and auto-detection where it can.
Documentation
use assert_cmd::Command;
use std::fs;
use tempfile::tempdir;

fn run(args: &[&str], input: &str, in_ext: &str, out_ext: &str) -> Result<String, String> {
    let dir = tempdir().unwrap();
    let inp = dir.path().join(format!("in.{in_ext}"));
    let out = dir.path().join(format!("out.{out_ext}"));
    fs::write(&inp, input).unwrap();
    let mut cmd = Command::cargo_bin("sheeter").unwrap();
    cmd.args(args);
    cmd.arg(inp.to_str().unwrap());
    cmd.arg(out.to_str().unwrap());
    let result = cmd.output().unwrap();
    if !result.status.success() {
        return Err(String::from_utf8_lossy(&result.stderr).to_string());
    }
    Ok(fs::read_to_string(out).unwrap())
}

#[test]
fn filter_on_buffered_json_to_yaml_actually_applies() {
    let json = r#"[{"name":"Alice","age":25},{"name":"Bob","age":40}]"#;
    let yaml = run(
        &[
            "convert", "--from", "json", "--to", "yaml", "--filter", "age > 30",
        ],
        json,
        "json",
        "yaml",
    )
    .unwrap();
    assert!(yaml.contains("Bob"), "Bob should survive: {yaml}");
    assert!(!yaml.contains("Alice"), "Alice should be filtered: {yaml}");
}

#[test]
fn limit_on_buffered_json_to_toml_actually_applies() {
    let json = r#"[{"a":1},{"a":2},{"a":3}]"#;
    let toml = run(
        &["convert", "--from", "json", "--to", "toml", "--limit", "2"],
        json,
        "json",
        "toml",
    )
    .unwrap();
    assert_eq!(toml.matches("a =").count(), 2, "only 2 rows: {toml}");
}

#[test]
fn select_on_buffered_json_to_xml_actually_applies() {
    let json = r#"[{"a":1,"b":2,"c":3}]"#;
    let xml = run(
        &[
            "convert", "--from", "json", "--to", "xml", "--select", "a,c",
        ],
        json,
        "json",
        "xml",
    )
    .unwrap();
    assert!(xml.contains("<a>"), "a should be there: {xml}");
    assert!(!xml.contains("<b>"), "b should be gone: {xml}");
    assert!(xml.contains("<c>"), "c should be there: {xml}");
}

#[test]
fn rename_on_buffered_json_to_csv_actually_applies() {
    let json = r#"[{"old_name":"Alice"}]"#;
    let csv = run(
        &[
            "convert",
            "--from",
            "json",
            "--to",
            "csv",
            "--rename",
            "old_name=new_name",
        ],
        json,
        "json",
        "csv",
    )
    .unwrap();
    assert!(csv.starts_with("new_name"), "header renamed: {csv}");
    assert!(!csv.contains("old_name"), "old name gone: {csv}");
}

#[test]
fn transforms_with_xlsx_target_error_explicitly() {
    // XLSX is binary and cannot round-trip through the hub, so the CLI
    // refuses transforms with XLSX output instead of silently ignoring them.
    let json = r#"[{"a":1},{"a":2},{"a":3}]"#;
    let dir = tempdir().unwrap();
    let inp = dir.path().join("in.json");
    let out = dir.path().join("out.xlsx");
    fs::write(&inp, json).unwrap();
    let output = Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "json",
            "--to",
            "xlsx",
            "--filter",
            "a >= 2",
            inp.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(!output.status.success(), "should fail with a clear error");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("cannot be applied") || stderr.contains("not applied"),
        "expected explicit error, got: {stderr}"
    );
    assert!(
        !out.exists(),
        "no partial output should be written on error"
    );
}