use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};
use icu_casemap::CaseMapperBorrowed;
use crate::canonical;
use crate::casefold::simple_fold;
use crate::engine::EngineFlavor;
use crate::error::WorkbookError;
use crate::limits;
use crate::named_range::NamedRange;
use crate::strict_json;
use crate::validate;
use crate::worksheet::Worksheet;
pub const SCHEMA_VERSION: &str = "1";
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Workbook {
engine: EngineFlavor,
names: Vec<NamedRange>,
sheets: Vec<Worksheet>,
#[serde(deserialize_with = "de_version")]
version: String,
}
impl Workbook {
pub fn new(engine: EngineFlavor) -> Self {
Self {
engine,
names: Vec::new(),
sheets: Vec::new(),
version: SCHEMA_VERSION.to_owned(),
}
}
pub fn engine(&self) -> EngineFlavor {
self.engine
}
pub fn version(&self) -> &str {
&self.version
}
pub fn sheets(&self) -> &[Worksheet] {
&self.sheets
}
pub fn sheets_mut(&mut self) -> &mut Vec<Worksheet> {
&mut self.sheets
}
pub fn names(&self) -> &[NamedRange] {
&self.names
}
pub fn names_mut(&mut self) -> &mut Vec<NamedRange> {
&mut self.names
}
pub fn sheet(&self, name: &str) -> Option<&Worksheet> {
self.sheet_index(name).map(|i| &self.sheets[i])
}
pub fn sheet_mut(&mut self, name: &str) -> Option<&mut Worksheet> {
match self.sheet_index(name) {
Some(i) => Some(&mut self.sheets[i]),
None => None,
}
}
pub fn sheet_index(&self, name: &str) -> Option<usize> {
let folder = CaseMapperBorrowed::new();
let target = simple_fold(&folder, name);
self.sheets
.iter()
.position(|s| simple_fold(&folder, s.name()) == target)
}
pub fn add_sheet(&mut self, sheet: Worksheet) -> Result<usize, WorkbookError> {
let pos = self.sheets.len();
self.insert_sheet(pos, sheet)?;
Ok(pos)
}
pub fn insert_sheet(&mut self, index: usize, sheet: Worksheet) -> Result<(), WorkbookError> {
if self.sheets.len() >= limits::MAX_SHEETS {
return Err(WorkbookError::SheetManagement(format!(
"cannot add sheet: workbook already has {} sheets, the limit (scope ADR Decision 5)",
limits::MAX_SHEETS
)));
}
if index > self.sheets.len() {
return Err(WorkbookError::SheetManagement(format!(
"cannot insert sheet at position {index}: only {} tab slots exist",
self.sheets.len() + 1
)));
}
validate_sheet_name(sheet.name())?;
if let Some(existing) = self.sheet(sheet.name()) {
return Err(WorkbookError::SheetManagement(format!(
"cannot add sheet {:?}: it collides with the existing sheet {:?} under simple \
case folding (schema spec §2)",
sheet.name(),
existing.name()
)));
}
self.sheets.insert(index, sheet);
Ok(())
}
pub fn remove_sheet(&mut self, name: &str) -> Option<Worksheet> {
self.sheet_index(name).map(|i| self.sheets.remove(i))
}
pub fn rename_sheet(&mut self, from: &str, to: &str) -> Result<(), WorkbookError> {
let idx = self.sheet_index(from).ok_or_else(|| {
WorkbookError::SheetManagement(format!("cannot rename: no sheet named {from:?}"))
})?;
validate_sheet_name(to)?;
if let Some(other) = self.sheet_index(to) {
if other != idx {
return Err(WorkbookError::SheetManagement(format!(
"cannot rename sheet to {to:?}: it collides with another sheet under simple \
case folding (schema spec §2)"
)));
}
}
self.sheets[idx].set_name(to);
Ok(())
}
pub fn move_sheet(&mut self, from: usize, to: usize) -> Result<(), WorkbookError> {
let len = self.sheets.len();
if from >= len || to >= len {
return Err(WorkbookError::SheetManagement(format!(
"cannot move sheet from {from} to {to}: valid tab positions are 0..{len}"
)));
}
let sheet = self.sheets.remove(from);
self.sheets.insert(to, sheet);
Ok(())
}
pub fn from_json(bytes: &[u8]) -> Result<Self, WorkbookError> {
if bytes.len() > limits::MAX_SERIALIZED_BYTES {
return Err(WorkbookError::Validation(format!(
"input is {} bytes, exceeding the {}-byte limit (scope ADR Decision 5)",
bytes.len(),
limits::MAX_SERIALIZED_BYTES
)));
}
let tree = strict_json::parse_no_dup_keys(bytes).map_err(WorkbookError::Validation)?;
validate::validate_document(&tree).map_err(WorkbookError::Validation)?;
serde_json::from_value(tree).map_err(|e| WorkbookError::Validation(e.to_string()))
}
pub fn to_json(&self) -> Result<String, WorkbookError> {
let mut tree =
serde_json::to_value(self).map_err(|e| WorkbookError::Validation(e.to_string()))?;
sort_names_by_name(&mut tree);
let canonical = canonical::to_canonical_string(&tree).map_err(WorkbookError::Validation)?;
if canonical.len() > limits::MAX_SERIALIZED_BYTES {
return Err(WorkbookError::Validation(format!(
"canonical workbook is {} bytes, exceeding the {}-byte limit (scope ADR Decision 5)",
canonical.len(),
limits::MAX_SERIALIZED_BYTES
)));
}
Ok(canonical)
}
}
fn validate_sheet_name(name: &str) -> Result<(), WorkbookError> {
let len = name.chars().count();
if len == 0 {
return Err(WorkbookError::SheetManagement(
"a worksheet name must be non-empty (schema spec §3)".to_owned(),
));
}
if len > limits::MAX_SHEET_NAME_LEN {
return Err(WorkbookError::SheetManagement(format!(
"worksheet name {name:?} has {len} scalar values, exceeding the limit of {} \
(schema spec §3)",
limits::MAX_SHEET_NAME_LEN
)));
}
Ok(())
}
fn sort_names_by_name(tree: &mut serde_json::Value) {
if let Some(names) = tree.get_mut("names").and_then(|v| v.as_array_mut()) {
names.sort_by(|a, b| {
let an = a.get("name").and_then(|v| v.as_str()).unwrap_or("");
let bn = b.get("name").and_then(|v| v.as_str()).unwrap_or("");
an.encode_utf16().cmp(bn.encode_utf16())
});
}
}
fn de_version<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
let version = String::deserialize(deserializer)?;
if version != SCHEMA_VERSION {
return Err(D::Error::custom(format!(
"unsupported schema version {version:?}: this version of \
truecalc-workbook reads version \"1\" (schema spec §10); \
upgrade truecalc to load this workbook"
)));
}
Ok(version)
}