mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 03:56:50 -06:00
Commands: an invalid shape id is an error instead of the selection
A shape id that was present but not a valid id (negative, not an integer, larger than u32) was treated as absent, so commands such as text.set and text.edit silently edited the selected shape instead. id_param now tells three cases apart: absent (the selection fallback is unchanged), valid, and invalid (an error naming the command). arrange, edit (select), inspect, review, table, text and the animation painter pick this up.
This commit is contained in:
@@ -305,7 +305,7 @@ fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn painter_pick(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let id = id_param(p, "id")
|
||||
let id = id_param(p, "id", "animation.painter")?
|
||||
.or_else(|| st.selection.shapes.first().copied())
|
||||
.ok_or_else(|| bad("animation.painter", "select an animated shape first"))?;
|
||||
let sl = st.current_slide().ok_or_else(|| bad("animation.painter", "no slide"))?;
|
||||
|
||||
@@ -86,7 +86,7 @@ fn order(s: &mut Session, p: &Value, how: i32) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn reorder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("arrange.reorder", "missing `id`"))?;
|
||||
let id = id_param(p, "id", "arrange.reorder")?.ok_or_else(|| bad("arrange.reorder", "missing `id`"))?;
|
||||
let to = usize_param(p, "index").ok_or_else(|| bad("arrange.reorder", "missing `index`"))?;
|
||||
s.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("arrange.reorder", "no slide"))?;
|
||||
|
||||
@@ -353,7 +353,10 @@ fn deselect(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = ids_param(p, "ids").or_else(|| id_param(p, "id").map(|i| vec![i])).unwrap_or_default();
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(v) => v,
|
||||
None => id_param(p, "id", "edit.select")?.map(|i| vec![i]).unwrap_or_default(),
|
||||
};
|
||||
let add = bool_or(p, "add", false);
|
||||
let toggle = bool_or(p, "toggle", false);
|
||||
let valid: Vec<ShapeId> = ids.into_iter().filter(|i| s.active().is_some_and(|d| d.shape(*i).is_some())).collect();
|
||||
|
||||
@@ -136,7 +136,8 @@ fn slide(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let id = id_param(p, "id").or_else(|| st.selection.shapes.first().copied()).ok_or_else(|| bad("shape.inspect", "missing `id`"))?;
|
||||
let id =
|
||||
id_param(p, "id", "shape.inspect")?.or_else(|| st.selection.shapes.first().copied()).ok_or_else(|| bad("shape.inspect", "missing `id`"))?;
|
||||
let sh = st.shape(id).ok_or_else(|| bad("shape.inspect", format!("no shape {id}")))?;
|
||||
let x = xfrm_of(&st.doc, &st.selection, sh);
|
||||
Ok(json!({"shape": serde_json::to_value(sh).unwrap_or_default(), "box": x}))
|
||||
|
||||
@@ -171,8 +171,17 @@ pub(crate) fn str_param<'a>(p: &'a Value, key: &str) -> Option<&'a str> {
|
||||
pub(crate) fn usize_param(p: &Value, key: &str) -> Option<usize> {
|
||||
p.get(key).and_then(Value::as_u64).and_then(|v| usize::try_from(v).ok())
|
||||
}
|
||||
pub(crate) fn id_param(p: &Value, key: &str) -> Option<ShapeId> {
|
||||
p.get(key).and_then(Value::as_u64).and_then(|v| u32::try_from(v).ok()).map(ShapeId)
|
||||
/// A shape id param: `Ok(None)` when absent (callers may fall back to the selection), `Err` when
|
||||
/// present but not a valid id (negative, non-integer, beyond `u32`), so a bad id never edits the selection.
|
||||
pub(crate) fn id_param(p: &Value, key: &str, cmd: &str) -> Result<Option<ShapeId>> {
|
||||
match p.get(key) {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.and_then(|v| u32::try_from(v).ok())
|
||||
.map(|v| Some(ShapeId(v)))
|
||||
.ok_or_else(|| bad(cmd, format!("`{key}` is not a valid shape id: {v}"))),
|
||||
}
|
||||
}
|
||||
pub(crate) fn ids_param(p: &Value, key: &str) -> Option<Vec<ShapeId>> {
|
||||
p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).filter_map(|v| u32::try_from(v).ok()).map(ShapeId).collect())
|
||||
@@ -196,7 +205,7 @@ pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<ShapeId>> {
|
||||
{
|
||||
return Ok(ids);
|
||||
}
|
||||
if let Some(id) = id_param(p, "id") {
|
||||
if let Some(id) = id_param(p, "id", "shape")? {
|
||||
return Ok(vec![id]);
|
||||
}
|
||||
Ok(s.doc()?.selection.shapes.clone())
|
||||
|
||||
@@ -60,7 +60,7 @@ fn now() -> String {
|
||||
fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let text = str_param(p, "text").ok_or_else(|| bad("comment.add", "missing `text`"))?.to_string();
|
||||
let author = s.prefs.author.clone();
|
||||
let shape = id_param(p, "id").or_else(|| s.active().and_then(|d| d.selection.shapes.first().copied()));
|
||||
let shape = id_param(p, "id", "comment.add")?.or_else(|| s.active().and_then(|d| d.selection.shapes.first().copied()));
|
||||
let (x, y) = (f64_or(p, "x", 10.0), f64_or(p, "y", 10.0));
|
||||
s.edit(|doc, sel| {
|
||||
let sl = Arc::make_mut(doc.slides.get_mut(sel.slide).ok_or_else(|| bad("comment.add", "no slide"))?);
|
||||
|
||||
@@ -78,7 +78,7 @@ fn at(s: &Session, p: &Value) -> (usize, usize) {
|
||||
}
|
||||
|
||||
fn with_table(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut Table) -> Result<()>) -> Result<Value> {
|
||||
let id = match id_param(p, "id") {
|
||||
let id = match id_param(p, "id", cmd)? {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
let st = s.doc()?;
|
||||
@@ -253,7 +253,7 @@ fn row_height(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn select_cells(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = match id_param(p, "id") {
|
||||
let id = match id_param(p, "id", "table.selectCells")? {
|
||||
Some(i) => i,
|
||||
None => s.doc()?.selection.shapes.first().copied().ok_or_else(|| bad("table.selectCells", "select a table"))?,
|
||||
};
|
||||
|
||||
@@ -158,7 +158,7 @@ fn edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})?;
|
||||
return ok();
|
||||
}
|
||||
let id = match id_param(p, "id") {
|
||||
let id = match id_param(p, "id", "text.edit")? {
|
||||
Some(i) => i,
|
||||
None => s.doc()?.selection.shapes.first().copied().ok_or_else(|| bad("text.edit", "select a shape with text"))?,
|
||||
};
|
||||
@@ -491,7 +491,7 @@ fn select_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let text = str_param(p, "text").ok_or_else(|| bad("text.set", "missing `text`"))?.to_string();
|
||||
let id = match id_param(p, "id") {
|
||||
let id = match id_param(p, "id", "text.set")? {
|
||||
Some(i) => i,
|
||||
None => s.doc()?.selection.shapes.first().copied().ok_or_else(|| bad("text.set", "missing `id` (or select a shape)"))?,
|
||||
};
|
||||
@@ -531,7 +531,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = s.doc()?;
|
||||
let id = match id_param(p, "id") {
|
||||
let id = match id_param(p, "id", "text.get")? {
|
||||
Some(i) => i,
|
||||
None => match &st.selection.text {
|
||||
Some(t) => t.shape,
|
||||
|
||||
@@ -79,6 +79,47 @@ fn agent_builds_a_deck() {
|
||||
assert_eq!(n2, n1 + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_text_unknown_id_is_error() {
|
||||
let mut s = Server::new(Box::new(Headless::new()));
|
||||
tool(&mut s, "new_presentation", json!({"theme": "Harbor"}));
|
||||
let title_id = tool(&mut s, "inspect_slide", json!({}))["shapes"][0]["id"].as_u64().unwrap();
|
||||
tool(&mut s, "select", json!({"ids": [title_id]}));
|
||||
// -1 and 2^32 are not shape ids; they must not fall back to the selection.
|
||||
for bad in [json!(999999), json!(-1), json!(4294967296u64)] {
|
||||
let r = call(&mut s, 1, "tools/call", json!({"name": "set_text", "arguments": {"id": bad, "text": "x"}}));
|
||||
assert_eq!(r["result"]["isError"], true, "{bad}: {r}");
|
||||
}
|
||||
// A real id, and a just-created shape, still work; omitting id still uses the selection.
|
||||
tool(&mut s, "set_text", json!({"id": title_id, "text": "Real"}));
|
||||
let sid = tool(&mut s, "add_shape", json!({"preset": "rect", "x": 10, "y": 10, "w": 50, "h": 50}))["id"].as_u64().unwrap();
|
||||
tool(&mut s, "set_text", json!({"id": sid, "text": "Fresh"}));
|
||||
tool(&mut s, "select", json!({"ids": [title_id]}));
|
||||
tool(&mut s, "set_text", json!({"text": "Selected"}));
|
||||
let sl = tool(&mut s, "inspect_slide", json!({}));
|
||||
let text_of = |id: u64| sl["shapes"].as_array().unwrap().iter().find(|x| x["id"].as_u64() == Some(id)).unwrap()["text"].clone();
|
||||
assert_eq!(text_of(sid), "Fresh");
|
||||
assert_eq!(text_of(title_id), "Selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_id_never_falls_back_to_selection() {
|
||||
let mut s = Server::new(Box::new(Headless::new()));
|
||||
tool(&mut s, "new_presentation", json!({"theme": "Harbor"}));
|
||||
let title_id = tool(&mut s, "inspect_slide", json!({}))["shapes"][0]["id"].as_u64().unwrap();
|
||||
tool(&mut s, "select", json!({"ids": [title_id]}));
|
||||
for cmd in ["text.edit", "text.get", "shape.inspect", "table.selectCells", "arrange.reorder"] {
|
||||
for bad in [json!(-1), json!(1.5), json!("7"), json!(4294967296u64)] {
|
||||
let r = call(&mut s, 1, "tools/call", json!({"name": "run_command", "arguments": {"command": cmd, "params": {"id": bad, "index": 0}}}));
|
||||
assert_eq!(r["result"]["isError"], true, "{cmd} {bad}: {r}");
|
||||
}
|
||||
}
|
||||
// Absent id still means the selection, and a real id still works.
|
||||
tool(&mut s, "run_command", json!({"command": "text.get", "params": {}}));
|
||||
tool(&mut s, "run_command", json!({"command": "text.get", "params": {"id": title_id}}));
|
||||
tool(&mut s, "run_command", json!({"command": "shape.inspect", "params": {"id": title_id}}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_are_reported_not_panics() {
|
||||
let mut s = Server::new(Box::new(Headless::new()));
|
||||
|
||||
Reference in New Issue
Block a user