diff --git a/crates/engine/src/cmd/extra.rs b/crates/engine/src/cmd/extra.rs index a08f08013..5a23b8dd7 100644 --- a/crates/engine/src/cmd/extra.rs +++ b/crates/engine/src/cmd/extra.rs @@ -701,6 +701,16 @@ fn stash_get(wb: &Workbook, key: &str) -> Json { .unwrap_or(json!({})) } +/// Like `stash_get`, for the name-keyed lists (views, scripts): a stored value that is not a JSON object is treated as empty. +fn stash_map(wb: &Workbook, key: &str) -> Json { + let v = stash_get(wb, key); + if v.is_object() { + v + } else { + json!({}) + } +} + fn stash_set(wb: &mut Workbook, key: &str, v: &Json) { let text = v.to_string(); let formula = format!("\"{}\"", text.replace('"', "\"\"")); @@ -723,7 +733,7 @@ pub(crate) fn keep_watch_list(from: &Workbook, to: &mut std::sync::Arc fn custom_views(s: &mut Session, p: &Json) -> Result { let d = s.doc()?; - let mut views = stash_get(&d.wb, VIEWS_NAME); + let mut views = stash_map(&d.wb, VIEWS_NAME); if let Some(name) = str_param(p, "save") { let sh = d.wb.active().ok_or(EngineError::NoDocument)?; views[name] = json!({"sheet": d.wb.active_sheet, "selection": d.selection.a1(), "zoom": sh.zoom, "freeze": sh.freeze}); @@ -938,7 +948,7 @@ fn new_script(s: &mut Session, p: &Json) -> Result { return Err(bad("automate.newScript", "`commands` must be an array")); } edit(s, |cx| { - let mut all = stash_get(&cx.wb, SCRIPTS_NAME); + let mut all = stash_map(&cx.wb, SCRIPTS_NAME); all[name.as_str()] = cmds.clone(); stash_set(&mut cx.wb, SCRIPTS_NAME, &all); Ok(json!({"name": name})) @@ -977,7 +987,7 @@ fn run_script(s: &mut Session, p: &Json) -> Result { fn delete_script(s: &mut Session, p: &Json) -> Result { let name = str_param(p, "name").ok_or_else(|| bad("automate.deleteScript", "missing `name`"))?.to_string(); edit(s, |cx| { - let mut all = stash_get(&cx.wb, SCRIPTS_NAME); + let mut all = stash_map(&cx.wb, SCRIPTS_NAME); if let Some(o) = all.as_object_mut() { o.remove(&name); } diff --git a/crates/engine/tests/stored_list_shape.rs b/crates/engine/tests/stored_list_shape.rs new file mode 100644 index 000000000..0d38cd5f9 --- /dev/null +++ b/crates/engine/tests/stored_list_shape.rs @@ -0,0 +1,48 @@ +use gridcraft_engine::Session; +use serde_json::json; + +const NOT_OBJECTS: [&str; 4] = ["\"[1,2]\"", "\"7\"", "\"true\"", "\"\"\"hi\"\"\""]; + +fn with_name(name: &str, refers_to: &str) -> Session { + let mut s = Session::new(); + s.new_workbook(); + s.execute("formulas.defineName", json!({"name": name, "refersTo": refers_to})).unwrap(); + s +} + +#[test] +fn saving_a_view_works_when_stored_list_is_not_an_object() { + for text in NOT_OBJECTS { + let mut s = with_name("_sc_custom_views", text); + let out = s.execute("view.customViews", json!({"save": "v"})).unwrap(); + assert!(out.get("v").is_some(), "{text}: {out}"); + } +} + +#[test] +fn saving_a_view_keeps_the_keys_of_a_stored_object() { + let mut s = with_name("_sc_custom_views", "\"{\"\"a\"\":1}\""); + let out = s.execute("view.customViews", json!({"save": "v"})).unwrap(); + assert!(out.get("a").is_some() && out.get("v").is_some(), "{out}"); +} + +#[test] +fn creating_a_script_works_when_stored_list_is_not_an_object() { + for text in NOT_OBJECTS { + let mut s = with_name("_sc_scripts", text); + s.execute("automate.newScript", json!({"name": "s1", "commands": []})).unwrap(); + let list = s.execute("automate.allScripts", json!({})).unwrap(); + assert!(list.to_string().contains("s1"), "{text}: {list}"); + } +} + +#[test] +fn stopping_a_recording_works_when_stored_list_is_not_an_object() { + for text in NOT_OBJECTS { + let mut s = with_name("_sc_scripts", text); + s.execute("automate.recordActions", json!({})).unwrap(); + s.execute("automate.recordActions", json!({"stop": true, "name": "s1"})).unwrap(); + let list = s.execute("automate.allScripts", json!({})).unwrap(); + assert!(list.to_string().contains("s1"), "{text}: {list}"); + } +}