sheeter-engine 1.3.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 sheeter_engine::CanonicalValue;
use std::fs;
use tempfile::tempdir;

fn run(dir: &std::path::Path, args: &[&str]) -> Result<String, String> {
    let output = Command::cargo_bin("sheeter")
        .unwrap()
        .current_dir(dir)
        .args(args)
        .output()
        .unwrap();
    if !output.status.success() {
        return Err(String::from_utf8_lossy(&output.stderr).to_string());
    }
    Ok(String::from_utf8(output.stdout).unwrap())
}

#[test]
fn merge_two_inputs() {
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("a.csv"), "x\n1\n").unwrap();
    fs::write(dir.path().join("b.json"), r#"[{"y":2}]"#).unwrap();
    let out = dir.path().join("ss.xlsx");
    run(
        dir.path(),
        &["xlsx:merge", "a.csv", "b.json", "-o", out.to_str().unwrap()],
    )
    .unwrap();
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    assert_eq!(sheets.len(), 2);
    assert_eq!(sheets[0].0, "a");
    assert_eq!(sheets[1].0, "b");
}

#[test]
fn merge_requires_xlsx_output() {
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("a.csv"), "x\n1\n").unwrap();
    let err = run(dir.path(), &["xlsx:merge", "a.csv", "-o", "out.csv"]).unwrap_err();
    assert!(err.contains(".xlsx"));
}

#[test]
fn merge_appends_to_existing() {
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("a.csv"), "x\n1\n").unwrap();
    fs::write(dir.path().join("b.csv"), "y\n2\n").unwrap();
    let out = dir.path().join("ss.xlsx");
    run(
        dir.path(),
        &["xlsx:merge", "a.csv", "-o", out.to_str().unwrap()],
    )
    .unwrap();
    run(
        dir.path(),
        &["xlsx:merge", "b.csv", "-o", out.to_str().unwrap()],
    )
    .unwrap();
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    assert_eq!(sheets.len(), 2);
    assert_eq!(sheets[0].0, "a");
    assert_eq!(sheets[1].0, "b");
}

#[test]
fn merge_collision_appends_suffix() {
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("a.csv"), "x\n1\n").unwrap();
    let out = dir.path().join("ss.xlsx");
    run(
        dir.path(),
        &["xlsx:merge", "a.csv", "-o", out.to_str().unwrap()],
    )
    .unwrap();
    run(
        dir.path(),
        &["xlsx:merge", "a.csv", "-o", out.to_str().unwrap()],
    )
    .unwrap();
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    assert_eq!(sheets.len(), 2);
    assert_eq!(sheets[0].0, "a");
    assert_eq!(sheets[1].0, "a_2");
}

#[test]
fn merge_multisheet_xlsx_prefixes() {
    let dir = tempdir().unwrap();
    let s1 = CanonicalValue::from_csv("v\n1\n").unwrap();
    let s2 = CanonicalValue::from_csv("v\n2\n").unwrap();
    let bytes = CanonicalValue::to_xlsx_workbook(&[("Q1".to_string(), s1), ("Q2".to_string(), s2)])
        .unwrap();
    fs::write(dir.path().join("report.xlsx"), bytes).unwrap();
    let out = dir.path().join("ss.xlsx");
    run(
        dir.path(),
        &["xlsx:merge", "report.xlsx", "-o", out.to_str().unwrap()],
    )
    .unwrap();
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    assert_eq!(sheets.len(), 2);
    assert_eq!(sheets[0].0, "report_Q1");
    assert_eq!(sheets[1].0, "report_Q2");
}

#[test]
fn frag_splits_into_json_files() {
    let dir = tempdir().unwrap();
    let s1 = CanonicalValue::from_csv("a\n1\n").unwrap();
    let s2 = CanonicalValue::from_csv("b\n2\n").unwrap();
    let bytes = CanonicalValue::to_xlsx_workbook(&[
        ("Funcionarios".to_string(), s1),
        ("Clientes".to_string(), s2),
    ])
    .unwrap();
    fs::write(dir.path().join("book.xlsx"), bytes).unwrap();
    run(dir.path(), &["xlsx:frag", "book.xlsx"]).unwrap();
    assert!(dir.path().join("funcionarios.json").exists());
    assert!(dir.path().join("clientes.json").exists());
}

#[test]
fn frag_sanitizes_accents_and_specials() {
    let dir = tempdir().unwrap();
    let s = CanonicalValue::from_csv("a\n1\n").unwrap();
    let bytes = CanonicalValue::to_xlsx_workbook(&[("Relatório (2024)".to_string(), s)]).unwrap();
    fs::write(dir.path().join("book.xlsx"), bytes).unwrap();
    run(dir.path(), &["xlsx:frag", "book.xlsx"]).unwrap();
    assert!(dir.path().join("relatorio_2024.json").exists());
}

#[test]
fn frag_rejects_non_xlsx() {
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("a.csv"), "x\n1\n").unwrap();
    let err = run(dir.path(), &["xlsx:frag", "a.csv"]).unwrap_err();
    assert!(err.contains(".xlsx") || err.contains("xlsx"));
}

#[test]
fn frag_out_dir() {
    let dir = tempdir().unwrap();
    let s = CanonicalValue::from_csv("a\n1\n").unwrap();
    let bytes = CanonicalValue::to_xlsx_workbook(&[("Data".to_string(), s)]).unwrap();
    fs::write(dir.path().join("book.xlsx"), bytes).unwrap();
    let out = dir.path().join("sub");
    run(
        dir.path(),
        &["xlsx:frag", "book.xlsx", "--out", out.to_str().unwrap()],
    )
    .unwrap();
    assert!(out.join("data.json").exists());
}
#[test]
fn concat_two_csvs_adds_source_column() {
    use sheeter_engine::CanonicalValue;
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("jan.csv"), "v\n1\n2\n").unwrap();
    fs::write(dir.path().join("feb.csv"), "v\n3\n").unwrap();
    let out = dir.path().join("all.xlsx");
    let status = std::process::Command::new(env!("CARGO_BIN_EXE_sheeter"))
        .current_dir(dir.path())
        .args([
            "xlsx:concat",
            "jan.csv",
            "feb.csv",
            "-o",
            out.to_str().unwrap(),
        ])
        .status()
        .unwrap();
    assert!(status.success());
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    assert_eq!(sheets.len(), 1);
    assert_eq!(sheets[0].0, "data");
    let json = sheets[0].1.to_json().unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
    let arr = parsed.as_array().unwrap();
    assert_eq!(arr.len(), 3);
    assert_eq!(arr[0]["source"], "jan");
    assert_eq!(arr[2]["source"], "feb");
}

#[test]
fn concat_no_source_omits_column() {
    use sheeter_engine::CanonicalValue;
    let dir = tempdir().unwrap();
    fs::write(dir.path().join("a.csv"), "v\n1\n").unwrap();
    fs::write(dir.path().join("b.csv"), "v\n2\n").unwrap();
    let out = dir.path().join("all.xlsx");
    std::process::Command::new(env!("CARGO_BIN_EXE_sheeter"))
        .current_dir(dir.path())
        .args([
            "xlsx:concat",
            "--no-source",
            "a.csv",
            "b.csv",
            "-o",
            out.to_str().unwrap(),
        ])
        .status()
        .unwrap();
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    let json = sheets[0].1.to_json().unwrap();
    assert!(!json.contains("source"), "no source column: {json}");
}

#[test]
fn concat_multisheet_input_prefixes() {
    use sheeter_engine::CanonicalValue;
    let dir = tempdir().unwrap();
    let s1 = CanonicalValue::from_csv("v\n1\n").unwrap();
    let s2 = CanonicalValue::from_csv("v\n2\n").unwrap();
    let bytes = CanonicalValue::to_xlsx_workbook(&[("Q1".to_string(), s1), ("Q2".to_string(), s2)])
        .unwrap();
    fs::write(dir.path().join("report.xlsx"), bytes).unwrap();
    let out = dir.path().join("all.xlsx");
    std::process::Command::new(env!("CARGO_BIN_EXE_sheeter"))
        .current_dir(dir.path())
        .args(["xlsx:concat", "report.xlsx", "-o", out.to_str().unwrap()])
        .status()
        .unwrap();
    let sheets = CanonicalValue::from_xlsx_workbook(&fs::read(out).unwrap()).unwrap();
    let json = sheets[0].1.to_json().unwrap();
    assert!(json.contains("report_Q1"), "prefix: {json}");
    assert!(json.contains("report_Q2"), "prefix: {json}");
}