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 sheeter_engine::{convert_workbook, Format};
use std::fs;
use tempfile::tempdir;

fn write_xlsx(path: &std::path::Path, sheets: &[(&str, Vec<Vec<&str>>)]) {
    use rust_xlsxwriter::Workbook;
    let mut wb = Workbook::new();
    for (name, rows) in sheets {
        let ws = wb.add_worksheet();
        ws.set_name(*name).unwrap();
        for (r, row) in rows.iter().enumerate() {
            for (c, cell) in row.iter().enumerate() {
                ws.write_string(r as u32, c as u16, *cell).unwrap();
            }
        }
    }
    wb.save(path).unwrap();
}

#[test]
fn xlsx_single_sheet_to_csv() {
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    write_xlsx(&xlsx, &[("Data", vec![vec!["a", "b"], vec!["1", "2"]])]);
    let out = dir.path().join("out.csv");
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "csv",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    let csv = fs::read_to_string(out).unwrap();
    assert_eq!(csv, "a,b\n1,2\n");
}

#[test]
fn xlsx_multi_sheet_to_csv_n_outputs() {
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    write_xlsx(
        &xlsx,
        &[
            ("Alpha", vec![vec!["x"], vec!["1"]]),
            ("Beta", vec![vec!["y"], vec!["2"]]),
        ],
    );
    let out = dir.path().join("out.csv");
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "csv",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    assert_eq!(
        fs::read_to_string(dir.path().join("out.Alpha.csv")).unwrap(),
        "x\n1\n"
    );
    assert_eq!(
        fs::read_to_string(dir.path().join("out.Beta.csv")).unwrap(),
        "y\n2\n"
    );
}

#[test]
fn xlsx_multi_sheet_to_json_single_file() {
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    write_xlsx(
        &xlsx,
        &[
            ("A", vec![vec!["v"], vec!["1"]]),
            ("B", vec![vec!["v"], vec!["2"]]),
        ],
    );
    let out = dir.path().join("out.json");
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "json",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    let json = fs::read_to_string(out).unwrap();
    assert!(json.contains("\"A\""));
    assert!(json.contains("\"B\""));
}

#[test]
fn xlsx_sheet_flag_restricts_output() {
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    write_xlsx(
        &xlsx,
        &[
            ("Alpha", vec![vec!["x"], vec!["1"]]),
            ("Beta", vec![vec!["y"], vec!["2"]]),
        ],
    );
    let out = dir.path().join("out.csv");
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "csv",
            "--sheet",
            "Beta",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    assert_eq!(fs::read_to_string(out).unwrap(), "y\n2\n");
}

#[test]
fn xlsx_roundtrip_preserves_sheet_names() {
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    write_xlsx(
        &xlsx,
        &[
            ("First", vec![vec!["a"], vec!["1"]]),
            ("Second", vec![vec!["b"], vec!["2"]]),
        ],
    );
    let out = dir.path().join("out.xlsx");
    Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "xlsx",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    let bytes = fs::read(out).unwrap();
    let sheets = convert_workbook(Format::Xlsx, Format::Json, &bytes).unwrap();
    assert_eq!(sheets.len(), 1);
    let json = String::from_utf8(sheets[0].1.clone()).unwrap();
    assert!(json.contains("\"First\""));
    assert!(json.contains("\"Second\""));
}

#[test]
fn xlsx_sheet_names_with_spaces_preserved() {
    use tempfile::tempdir;
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    write_xlsx(
        &xlsx,
        &[
            ("Vendas 2024", vec![vec!["v"], vec!["1"]]),
            ("Vendas 2025", vec![vec!["v"], vec!["2"]]),
        ],
    );
    let out = dir.path().join("out.csv");
    assert_cmd::Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "csv",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    assert!(dir.path().join("out.Vendas 2024.csv").exists());
    assert!(dir.path().join("out.Vendas 2025.csv").exists());
}

#[test]
fn xlsx_integer_floats_become_ints_in_json() {
    use rust_xlsxwriter::Workbook;
    let dir = tempdir().unwrap();
    let xlsx = dir.path().join("in.xlsx");
    let mut wb = Workbook::new();
    let ws = wb.add_worksheet();
    ws.write_string(0, 0, "id").unwrap();
    ws.write_string(0, 1, "weight").unwrap();
    ws.write_number(1, 0, 1.0).unwrap();
    ws.write_number(1, 1, 6.9).unwrap();
    ws.write_number(2, 0, 2.0).unwrap();
    ws.write_number(2, 1, 13.5).unwrap();
    wb.save(&xlsx).unwrap();

    let out = dir.path().join("out.json");
    assert_cmd::Command::cargo_bin("sheeter")
        .unwrap()
        .args([
            "convert",
            "--from",
            "xlsx",
            "--to",
            "json",
            xlsx.to_str().unwrap(),
            out.to_str().unwrap(),
        ])
        .assert()
        .success();
    let json = fs::read_to_string(out).unwrap();
    assert!(
        json.contains(r#"{"id":1,"weight":6.9}"#),
        "int coercion: {json}"
    );
    assert!(
        json.contains(r#"{"id":2,"weight":13.5}"#),
        "int coercion: {json}"
    );
}