use crate::error::Error;
use crate::model::{Cell, CellRef, CellValue, Sheet, SheetVisibility, Workbook};
use serde::ser::{SerializeSeq, SerializeStruct};
use serde::{Serialize, Serializer};
use std::io::Write;
pub fn to_json_writer<W: Write>(workbook: &Workbook, writer: W) -> Result<(), Error> {
let json_workbook = JsonWorkbook { workbook };
serde_json::to_writer(writer, &json_workbook).map_err(|source| Error::JsonSerialize {
source: Box::new(source),
})
}
pub fn to_json_string(workbook: &Workbook) -> Result<String, Error> {
let mut buf = Vec::new();
to_json_writer(workbook, &mut buf)?;
String::from_utf8(buf).map_err(|source| Error::JsonSerialize {
source: Box::new(source),
})
}
struct JsonWorkbook<'a> {
workbook: &'a Workbook,
}
impl Serialize for JsonWorkbook<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("Workbook", 1)?;
state.serialize_field(
"sheets",
&SheetSeq {
workbook: self.workbook,
},
)?;
state.end()
}
}
struct SheetSeq<'a> {
workbook: &'a Workbook,
}
impl Serialize for SheetSeq<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let sheets = self.workbook.sheets();
let mut seq = serializer.serialize_seq(Some(sheets.len()))?;
for sheet in sheets {
seq.serialize_element(&JsonSheet { sheet })?;
}
seq.end()
}
}
struct JsonSheet<'a> {
sheet: &'a Sheet,
}
impl Serialize for JsonSheet<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("Sheet", 5)?;
state.serialize_field("name", &self.sheet.name)?;
state.serialize_field("visibility", visibility_tag(self.sheet.visibility))?;
state.serialize_field("maxRow", &self.sheet.max_row)?;
state.serialize_field("maxCol", &self.sheet.max_col)?;
state.serialize_field("cells", &CellSeq { sheet: self.sheet })?;
state.end()
}
}
struct CellSeq<'a> {
sheet: &'a Sheet,
}
impl Serialize for CellSeq<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(None)?;
for (cell_ref, cell) in self.sheet.iter_cells() {
seq.serialize_element(&cell_to_json(self.sheet, cell_ref, cell))?;
}
seq.end()
}
}
#[derive(Debug, Serialize)]
struct JsonCell {
row: u32,
col: u32,
value: JsonCellValue,
#[serde(rename = "rowSpan", skip_serializing_if = "is_one")]
row_span: u32,
#[serde(rename = "colSpan", skip_serializing_if = "is_one")]
col_span: u32,
}
fn is_one(n: &u32) -> bool {
*n == 1
}
#[derive(Debug, Serialize, PartialEq)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
enum JsonCellValue {
Number(f64),
#[allow(dead_code)]
DateTime(String),
Text(std::sync::Arc<str>),
Boolean(bool),
Error(String),
Empty,
}
fn cell_to_json(sheet: &Sheet, cell_ref: CellRef, cell: &Cell) -> JsonCell {
let (row_span, col_span) = sheet
.merged_region_at(cell_ref)
.map(|r| (r.row_span(), r.col_span()))
.unwrap_or((1, 1));
JsonCell {
row: cell_ref.row,
col: cell_ref.col,
value: cell_value_to_json(cell.value.as_ref()),
row_span,
col_span,
}
}
fn cell_value_to_json(value: Option<&CellValue>) -> JsonCellValue {
match value {
None => JsonCellValue::Empty,
Some(CellValue::Number(n)) if n.is_finite() => JsonCellValue::Number(*n),
Some(CellValue::Number(_)) => JsonCellValue::Empty,
Some(CellValue::DateTime(_)) => JsonCellValue::Empty,
Some(CellValue::Text(s)) => JsonCellValue::Text(s.clone()),
Some(CellValue::Boolean(b)) => JsonCellValue::Boolean(*b),
Some(CellValue::Error(e)) => JsonCellValue::Error(e.clone()),
}
}
fn visibility_tag(v: SheetVisibility) -> &'static str {
match v {
SheetVisibility::Visible => "visible",
SheetVisibility::Hidden => "hidden",
SheetVisibility::VeryHidden => "veryHidden",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{DateTimeValue, ResolvedStyle};
use std::sync::Arc;
fn sheet_with_one_cell(name: &str, value: Option<CellValue>) -> Sheet {
let mut sheet = Sheet::new(name.to_string(), SheetVisibility::Visible);
sheet.insert_cell(CellRef { row: 1, col: 1 }, Cell { value, style: None });
sheet
}
#[test]
fn single_numeric_cell_round_trips_through_json_string() {
let sheet = sheet_with_one_cell("Sheet1", Some(CellValue::Number(42.0)));
let workbook = Workbook::new(vec![sheet]);
let json = to_json_string(&workbook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed,
serde_json::json!({
"sheets": [{
"name": "Sheet1",
"visibility": "visible",
"maxRow": 1,
"maxCol": 1,
"cells": [
{"row": 1, "col": 1, "value": {"type": "number", "value": 42.0}}
]
}]
})
);
}
#[test]
fn merged_cell_reports_span_and_excludes_virtual_coordinates() {
let mut sheet = Sheet::new("Sheet1".into(), SheetVisibility::Visible);
sheet.insert_cell(
CellRef { row: 1, col: 1 },
Cell {
value: Some(CellValue::Text(Arc::from("merged"))),
style: None,
},
);
sheet.insert_merge(crate::model::MergedRegion {
start: CellRef { row: 1, col: 1 },
end: CellRef { row: 2, col: 3 },
});
let workbook = Workbook::new(vec![sheet]);
let json = to_json_string(&workbook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let cells = parsed["sheets"][0]["cells"].as_array().unwrap();
assert_eq!(cells.len(), 1, "virtual coordinates must not appear");
assert_eq!(cells[0]["rowSpan"], 2);
assert_eq!(cells[0]["colSpan"], 3);
}
#[test]
fn unmerged_cell_omits_span_fields() {
let sheet = sheet_with_one_cell("Sheet1", Some(CellValue::Number(1.0)));
let workbook = Workbook::new(vec![sheet]);
let json = to_json_string(&workbook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let cell = &parsed["sheets"][0]["cells"][0];
assert!(cell.get("rowSpan").is_none());
assert!(cell.get("colSpan").is_none());
}
#[test]
fn each_cell_value_variant_serializes_with_expected_tag() {
assert_eq!(
cell_value_to_json(Some(&CellValue::Number(1.5))),
JsonCellValue::Number(1.5)
);
assert_eq!(
cell_value_to_json(Some(&CellValue::Text(Arc::from("hi")))),
JsonCellValue::Text(Arc::from("hi"))
);
assert_eq!(
cell_value_to_json(Some(&CellValue::Boolean(true))),
JsonCellValue::Boolean(true)
);
assert_eq!(
cell_value_to_json(Some(&CellValue::Error("#DIV/0!".into()))),
JsonCellValue::Error("#DIV/0!".into())
);
}
#[test]
fn formatting_only_cell_serializes_as_empty() {
let style = Arc::new(ResolvedStyle {
is_date_time: false,
});
let mut sheet = Sheet::new("Sheet1".into(), SheetVisibility::Visible);
sheet.insert_cell(
CellRef { row: 1, col: 1 },
Cell {
value: None,
style: Some(style),
},
);
let workbook = Workbook::new(vec![sheet]);
let json = to_json_string(&workbook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["sheets"][0]["cells"][0]["value"]["type"], "empty");
}
#[test]
fn non_finite_numbers_fall_back_to_empty_without_erroring() {
for n in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let sheet = sheet_with_one_cell("Sheet1", Some(CellValue::Number(n)));
let workbook = Workbook::new(vec![sheet]);
let json = to_json_string(&workbook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed["sheets"][0]["cells"][0]["value"]["type"], "empty",
"n = {n}"
);
}
}
#[test]
fn date_time_value_currently_serializes_as_empty() {
assert_eq!(
cell_value_to_json(Some(&CellValue::DateTime(DateTimeValue))),
JsonCellValue::Empty
);
let json = serde_json::to_string(&JsonCellValue::DateTime("2024-01-01".into())).unwrap();
assert_eq!(json, r#"{"type":"dateTime","value":"2024-01-01"}"#);
}
#[test]
fn hidden_and_very_hidden_sheets_report_visibility() {
let workbook = Workbook::new(vec![
Sheet::new("Vis".into(), SheetVisibility::Visible),
Sheet::new("Hid".into(), SheetVisibility::Hidden),
Sheet::new("VHid".into(), SheetVisibility::VeryHidden),
]);
let json = to_json_string(&workbook).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let visibilities: Vec<&str> = parsed["sheets"]
.as_array()
.unwrap()
.iter()
.map(|s| s["visibility"].as_str().unwrap())
.collect();
assert_eq!(visibilities, vec!["visible", "hidden", "veryHidden"]);
}
#[test]
fn zero_sheet_workbook_serializes_as_empty_array() {
let workbook = Workbook::new(vec![]);
assert_eq!(to_json_string(&workbook).unwrap(), r#"{"sheets":[]}"#);
}
#[test]
fn failing_writer_propagates_as_json_serialize_error() {
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("write failed"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let workbook = Workbook::new(vec![sheet_with_one_cell(
"Sheet1",
Some(CellValue::Number(1.0)),
)]);
let err = to_json_writer(&workbook, FailingWriter).unwrap_err();
assert!(matches!(err, Error::JsonSerialize { .. }));
}
}