miniexcel 0.1.0

Experimental Rust XLSX reader and writer for MiniExcel
Documentation
mod common;

use miniexcel::{
    FormulaCalculationStatus, HeaderMode, MiniExcel, RagExportOptions, RagValue, ReadOptions,
};

#[test]
fn path_and_byte_exports_have_identical_chunks_and_manifests() {
    let path = common::fixture("TestIssue157.xlsx");
    let read_options = ReadOptions::new().with_header_mode(HeaderMode::FirstRow);
    let export_options =
        RagExportOptions::new().with_chunk_rows(2).with_source_name("formula-workbook.xlsx");

    let mut export = MiniExcel::export_rag(&path, &read_options, &export_options)
        .expect("create path RAG export");
    let path_chunks =
        export.by_ref().collect::<miniexcel::Result<Vec<_>>>().expect("stream path chunks");
    let path_manifest = export.manifest().clone();

    let bytes = std::fs::read(path).expect("read workbook bytes");
    let mut byte_chunks = Vec::new();
    let byte_manifest =
        MiniExcel::visit_rag_chunks_from_bytes(&bytes, &read_options, &export_options, |chunk| {
            byte_chunks.push(chunk.clone());
            Ok(())
        })
        .expect("stream byte chunks");

    assert_eq!(byte_chunks, path_chunks);
    assert_eq!(byte_manifest, path_manifest);
    assert_eq!(path_manifest.emitted_rows(), 5);
    assert_eq!(path_manifest.emitted_chunks(), 3);
    assert_eq!(path_manifest.source_sha256().len(), 64);
    assert!(!path_manifest.truncated());
    assert!(path_chunks.iter().all(|chunk| chunk.header().is_some()));
    assert_eq!(path_chunks[0].data_range(), "A2:E3");

    let formula_cell = path_chunks[0]
        .rows()
        .iter()
        .flat_map(|row| row.cells())
        .find(|cell| cell.address() == "D2")
        .expect("formula cell D2");
    assert_eq!(formula_cell.formula(), Some("FALSE()"));
    assert_eq!(formula_cell.value(), &RagValue::Bool(false));

    let json = serde_json::to_value(&path_chunks[0]).expect("serialize RAG chunk");
    assert_eq!(json["rows"][0]["cells"][3]["calculationStatus"], "cachedOnly");
    assert_eq!(
        serde_json::from_value::<FormulaCalculationStatus>(
            json["rows"][0]["cells"][3]["calculationStatus"].clone(),
        )
        .expect("deserialize status"),
        FormulaCalculationStatus::CachedOnly
    );
}

#[test]
fn max_rows_truncates_without_overfilling_a_chunk() {
    let path = common::fixture("TestTypeMapping.xlsx");
    let read_options = ReadOptions::new().with_header_mode(HeaderMode::FirstRow);
    let export_options =
        RagExportOptions::new().with_chunk_rows(25).with_max_rows(3).with_source_name("types.xlsx");

    let mut export = MiniExcel::export_rag(&path, &read_options, &export_options)
        .expect("create truncated path export");
    let chunks = export
        .by_ref()
        .collect::<miniexcel::Result<Vec<_>>>()
        .expect("stream truncated path export");
    assert_eq!(chunks.len(), 1);
    assert_eq!(chunks[0].rows().len(), 3);
    assert!(export.manifest().truncated());

    let bytes = std::fs::read(path).expect("read workbook bytes");
    let mut byte_chunks = Vec::new();
    let manifest =
        MiniExcel::visit_rag_chunks_from_bytes(&bytes, &read_options, &export_options, |chunk| {
            byte_chunks.push(chunk.clone());
            Ok(())
        })
        .expect("stream truncated byte export");
    assert_eq!(byte_chunks, chunks);
    assert_eq!(manifest.emitted_rows(), 3);
    assert!(manifest.truncated());
}

#[test]
fn exact_row_limit_is_not_reported_as_truncated() {
    let path = common::fixture("TestIssue157.xlsx");
    let read_options = ReadOptions::new().with_header_mode(HeaderMode::FirstRow);
    let export_options = RagExportOptions::new().with_chunk_rows(2).with_max_rows(5);

    let mut export = MiniExcel::export_rag(&path, &read_options, &export_options)
        .expect("create exact-limit export");
    export.by_ref().collect::<miniexcel::Result<Vec<_>>>().expect("consume exact-limit export");
    assert_eq!(export.manifest().emitted_rows(), 5);
    assert!(!export.manifest().truncated());

    let bytes = std::fs::read(path).expect("read workbook bytes");
    let manifest =
        MiniExcel::visit_rag_chunks_from_bytes(&bytes, &read_options, &export_options, |_| Ok(()))
            .expect("consume exact-limit byte export");
    assert_eq!(manifest.emitted_rows(), 5);
    assert!(!manifest.truncated());
}

#[test]
fn hidden_sheets_require_explicit_opt_in() {
    let path = common::fixture("TestMultiSheetWithHiddenSheet.xlsx");
    let read_options = ReadOptions::new().with_sheet_name("HiddenSheet4");
    let error = match MiniExcel::export_rag(&path, &read_options, &RagExportOptions::new()) {
        Ok(_) => panic!("hidden sheet should be rejected"),
        Err(error) => error,
    };
    assert!(error.to_string().contains("requires explicit opt-in"));

    MiniExcel::export_rag(
        path,
        &read_options,
        &RagExportOptions::new().with_allow_hidden_sheets(true),
    )
    .expect("explicitly allow hidden sheet");
}