sheeter-engine 1.2.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());
}