WIP: a legacy JSON workbook is refused by name (improvise-tt22)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! `.improv` document lifecycle — save, load, gzip, legacy JSON — over a
|
||||
//! `.improv` document lifecycle — save, load, gzip — over a
|
||||
//! [`Storage`] backend. The text format is in `format`, `parse` and `export`.
|
||||
|
||||
mod export;
|
||||
@@ -96,11 +96,16 @@ pub fn load_from(storage: &dyn Storage, path: &Path) -> Result<Workbook> {
|
||||
};
|
||||
let text =
|
||||
String::from_utf8(bytes).with_context(|| format!("{} is not UTF-8", path.display()))?;
|
||||
// `.improv` text is the only native format; a JSON workbook is named, not
|
||||
// handed to the grammar to fail on its first byte (improvise-tt22).
|
||||
if text.trim_start().starts_with('{') {
|
||||
serde_json::from_str(&text).context("Failed to deserialize workbook")
|
||||
} else {
|
||||
parse_md(&text)
|
||||
anyhow::bail!(
|
||||
"{} is a legacy JSON workbook, which is no longer supported: \
|
||||
only .improv text can be opened",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
parse_md(&text)
|
||||
}
|
||||
|
||||
pub fn autosave_path(path: &Path) -> std::path::PathBuf {
|
||||
@@ -194,37 +199,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// improvise-tt22: `.improv` text is the only native format, so a legacy
|
||||
/// JSON workbook is refused by name. Evidence: `context/test-evidence/formula-and-io.md`.
|
||||
#[test]
|
||||
fn load_dispatcher_detects_legacy_json_by_brace() {
|
||||
// The load() function routes to JSON deserializer when text starts with '{'
|
||||
let m = two_cat_model();
|
||||
let json = serde_json::to_string_pretty(&m).unwrap();
|
||||
assert!(json.trim_start().starts_with('{'), "sanity check");
|
||||
// Deserialise via the JSON path
|
||||
let m2: Workbook = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(m2.model.name, "Budget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_legacy_json_workbook_loads_its_numbers_bit_for_bit() {
|
||||
fn a_legacy_json_workbook_is_refused_by_name() {
|
||||
use crate::storage::{MemoryFs, Storage};
|
||||
const LOSSY: f64 = 0.248_157_142_857_142_85;
|
||||
let key = coord(&[("Type", "Food"), ("Month", "Jan")]);
|
||||
let mut wb = two_cat_model();
|
||||
wb.model.set_cell(key.clone(), CellValue::Number(LOSSY));
|
||||
let json = serde_json::to_string(&wb).unwrap();
|
||||
assert!(
|
||||
json.contains("0.24815714285714285"),
|
||||
"the file holds the exact text"
|
||||
);
|
||||
let fs = MemoryFs::new();
|
||||
let path = std::path::Path::new("legacy.improv");
|
||||
fs.write(path, json.as_bytes()).unwrap();
|
||||
let loaded = super::load_from(&fs, path).unwrap();
|
||||
let Some(CellValue::Number(n)) = loaded.model.get_cell(&key) else {
|
||||
panic!("the cell must load as a number");
|
||||
};
|
||||
assert_eq!(n.to_bits(), LOSSY.to_bits(), "loaded {n:?}, not {LOSSY:?}");
|
||||
// The shape the removed loader read: `Workbook`'s serde form.
|
||||
let legacy = r#"
|
||||
{"model": {"name": "Budget", "categories": {}, "data": [], "formulas": []},
|
||||
"views": {}, "active_view": "Default"}"#;
|
||||
for name in ["legacy.improv", "legacy.improv.gz"] {
|
||||
let fs = MemoryFs::new();
|
||||
let path = std::path::Path::new(name);
|
||||
let bytes = if name.ends_with(".gz") {
|
||||
use std::io::Write;
|
||||
let mut enc =
|
||||
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
enc.write_all(legacy.as_bytes()).unwrap();
|
||||
enc.finish().unwrap()
|
||||
} else {
|
||||
legacy.as_bytes().to_vec()
|
||||
};
|
||||
fs.write(path, &bytes).unwrap();
|
||||
let err = format!("{:#}", super::load_from(&fs, path).unwrap_err());
|
||||
assert!(
|
||||
err.contains("legacy JSON") && err.contains("no longer supported"),
|
||||
"{name}: expected the format named and refused, got {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── save/load roundtrip via file ────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user