#![cfg(feature = "json")]
use formualizer_common::LiteralValue;
use formualizer_workbook::backends::JsonAdapter;
use formualizer_workbook::traits::SpreadsheetReader;
use formualizer_workbook::{LoadStrategy, Workbook, WorkbookConfig};
fn load(json: &str) -> Result<Workbook, String> {
let adapter = <JsonAdapter as SpreadsheetReader>::open_bytes(json.as_bytes().to_vec())
.map_err(|e| e.to_string())?;
Workbook::from_reader(
adapter,
LoadStrategy::EagerAll,
WorkbookConfig::interactive(),
)
.map_err(|e| e.to_string())
}
const WORKED_EXAMPLE: &str = r#"{
"sheets": {
"S": {
"cells": [
{ "row": 1, "col": 1, "value": { "type": "Text", "value": "Nama" } },
{ "row": 1, "col": 2, "value": { "type": "Text", "value": "Nilai" } },
{ "row": 2, "col": 1, "value": { "type": "Text", "value": "Ani" } },
{ "row": 2, "col": 2, "value": { "type": "Number", "value": 10 } },
{ "row": 3, "col": 1, "value": { "type": "Text", "value": "Budi" } },
{ "row": 3, "col": 2, "value": { "type": "Number", "value": 20 } },
{ "row": 4, "col": 1, "value": { "type": "Text", "value": "Cici" } },
{ "row": 4, "col": 2, "value": { "type": "Number", "value": 30 } }
],
"tables": [{
"name": "Table1",
"range": [1, 1, 4, 2],
"headers": ["Nama", "Nilai"],
"totals_row": false
}]
}
}
}"#;
#[test]
fn the_documented_worked_example_loads_and_evaluates() {
let mut wb = load(WORKED_EXAMPLE).expect("documented example must load");
wb.set_formula("S", 10, 4, "=SUM(Table1[Nilai])").unwrap();
wb.evaluate_all().unwrap();
assert_eq!(wb.get_value("S", 10, 4), Some(LiteralValue::Number(60.0)));
}
#[test]
fn header_row_defaults_to_true_and_totals_row_is_required() {
let wb = load(WORKED_EXAMPLE).unwrap();
let table = wb.table_metadata("Table1").expect("table");
assert!(table.header_row, "header_row should default to true");
assert!(!table.totals_row);
let without_totals = WORKED_EXAMPLE.replace(",\n \"totals_row\": false", "");
let Err(error) = load(&without_totals) else {
panic!("totals_row is required")
};
assert!(
error.contains("totals_row"),
"error should name the missing field, got: {error}"
);
}
#[test]
fn a_tables_key_outside_a_sheet_is_ignored() {
let misplaced = r#"{
"sheets": { "S": { "cells": [] } },
"tables": [{ "name": "Table1", "range": [1,1,4,2], "headers": ["a"], "totals_row": false }]
}"#;
let wb = load(misplaced).expect("unknown top-level keys are ignored");
assert!(
wb.tables().is_empty(),
"a top-level `tables` key must not define anything"
);
}
#[test]
fn cells_must_be_a_sequence_and_values_must_be_tagged() {
let cells_as_map = r#"{ "sheets": { "S": { "cells": {} } } }"#;
let Err(error) = load(cells_as_map) else {
panic!("cells must be a sequence")
};
assert!(error.contains("sequence"), "got: {error}");
let untagged_value =
r#"{ "sheets": { "S": { "cells": [{ "row": 1, "col": 1, "value": 5 }] } } }"#;
assert!(
load(untagged_value).is_err(),
"a bare scalar value must be rejected"
);
}
#[test]
fn documented_cell_value_tags_are_accepted() {
let json = r##"{ "sheets": { "S": { "cells": [
{ "row": 1, "col": 1, "value": { "type": "Int", "value": 42 } },
{ "row": 2, "col": 1, "value": { "type": "Number", "value": 3.5 } },
{ "row": 3, "col": 1, "value": { "type": "Text", "value": "x" } },
{ "row": 4, "col": 1, "value": { "type": "Boolean", "value": true } },
{ "row": 5, "col": 1, "value": { "type": "Empty" } },
{ "row": 6, "col": 1, "value": { "type": "Date", "value": "2026-07-25" } },
{ "row": 7, "col": 1, "value": { "type": "DateTime", "value": "2026-07-25T09:30:00" } },
{ "row": 8, "col": 1, "value": { "type": "Time", "value": "09:30:00" } },
{ "row": 9, "col": 1, "value": { "type": "Duration", "value": 3600 } },
{ "row": 10, "col": 1, "value": { "type": "Error", "value": "#DIV/0!" } }
] } } }"##;
let wb = load(json).expect("every documented tag must be accepted");
assert_eq!(wb.get_value("S", 1, 1), Some(LiteralValue::Number(42.0)));
assert_eq!(
wb.get_value("S", 3, 1),
Some(LiteralValue::Text("x".into()))
);
assert_eq!(wb.get_value("S", 4, 1), Some(LiteralValue::Boolean(true)));
}