Skip to main content

omgbase_mutate/
changeset.rs

1//! Changesets (`spec/mutate/README.md` §4): the op shapes, their JSON wire
2//! form, and placeholder resolution (`$<n>.ids[<i>]` naming an earlier op's
3//! result). Applying a changeset needs a store (loading, commit) and lives in
4//! `omgbase-store`; the pure pieces are here.
5
6use std::collections::BTreeMap;
7
8use serde_json::{Map, Value, json};
9
10use crate::error::{ErrorCode, MutationError, Result};
11use crate::ops::{At, Expect, OpResult, Parent, To};
12
13/// One kernel op as a changeset carries it (§4 `Op`). String fields may be
14/// placeholders until [`resolve_op`] runs.
15#[derive(Clone, Debug, PartialEq)]
16pub enum Op {
17    Insert {
18        /// A doc id or path (§4 step 1); `None` infers from the placement.
19        doc: Option<String>,
20        to: To,
21        markdown: String,
22        /// §1.2: the DESTINATION parent's `parent_children_hash` (since 1.1).
23        expect: Option<Expect>,
24    },
25    Update {
26        block: String,
27        markdown: Option<String>,
28        attrs: Option<Map<String, Value>>,
29        expect: Option<Expect>,
30        trivia: Option<String>,
31        child_ids: Option<BTreeMap<String, String>>,
32    },
33    Move {
34        blocks: Vec<String>,
35        to: To,
36        /// §1.2: the DESTINATION parent's `parent_children_hash` (since 1.1).
37        expect: Option<Expect>,
38    },
39    Remove {
40        blocks: Vec<String>,
41        expect: Option<BTreeMap<String, Expect>>,
42    },
43    Split {
44        block: String,
45        /// Byte offsets (§2.5).
46        at: Vec<usize>,
47        expect: Option<Expect>,
48    },
49    Merge {
50        blocks: Vec<String>,
51        separator: Option<String>,
52        expect: Option<BTreeMap<String, Expect>>,
53    },
54}
55
56impl Op {
57    /// The op's name (`insert`, …).
58    #[must_use]
59    pub const fn name(&self) -> &'static str {
60        match self {
61            Op::Insert { .. } => "insert",
62            Op::Update { .. } => "update",
63            Op::Move { .. } => "move",
64            Op::Remove { .. } => "remove",
65            Op::Split { .. } => "split",
66            Op::Merge { .. } => "merge",
67        }
68    }
69}
70
71// ---- JSON ---------------------------------------------------------------------------
72
73fn expect_to_json(e: &Expect) -> Value {
74    let mut m = Map::new();
75    if let Some(h) = &e.content_hash {
76        m.insert("content_hash".to_owned(), json!(h));
77    }
78    if let Some(h) = &e.parent_children_hash {
79        m.insert("parent_children_hash".to_owned(), json!(h));
80    }
81    Value::Object(m)
82}
83
84fn expect_from_json(v: &Value) -> std::result::Result<Expect, String> {
85    let obj = v.as_object().ok_or("expect must be an object")?;
86    let s = |k: &str| -> std::result::Result<Option<String>, String> {
87        match obj.get(k) {
88            None | Some(Value::Null) => Ok(None),
89            Some(Value::String(s)) => Ok(Some(s.clone())),
90            Some(_) => Err(format!("expect.{k} must be a string")),
91        }
92    };
93    Ok(Expect {
94        content_hash: s("content_hash")?,
95        parent_children_hash: s("parent_children_hash")?,
96    })
97}
98
99fn expect_map_to_json(m: &BTreeMap<String, Expect>) -> Value {
100    Value::Object(
101        m.iter()
102            .map(|(k, e)| (k.clone(), expect_to_json(e)))
103            .collect(),
104    )
105}
106
107fn expect_map_from_json(v: &Value) -> std::result::Result<BTreeMap<String, Expect>, String> {
108    let obj = v
109        .as_object()
110        .ok_or("expect must be an object of block → expect")?;
111    obj.iter()
112        .map(|(k, e)| Ok((k.clone(), expect_from_json(e)?)))
113        .collect()
114}
115
116/// `To` as JSON: `{ parent, at }`.
117#[must_use]
118pub fn to_to_json(to: &To) -> Value {
119    let parent = match &to.parent {
120        Parent::Doc => json!({ "doc": true }),
121        Parent::Block(id) => json!(id),
122        Parent::Section { heading } => json!({ "heading": heading, "scope": "section" }),
123    };
124    let at = match &to.at {
125        At::Start => json!("start"),
126        At::End => json!("end"),
127        At::Before(id) => json!({ "before": id }),
128        At::After(id) => json!({ "after": id }),
129    };
130    json!({ "parent": parent, "at": at })
131}
132
133/// `To` from JSON (`parent: null` is the document, as the reference accepts).
134pub fn to_from_json(v: &Value) -> std::result::Result<To, String> {
135    let obj = v.as_object().ok_or("to must be an object")?;
136    let parent = match obj.get("parent") {
137        None | Some(Value::Null) => Parent::Doc,
138        Some(Value::String(id)) => Parent::Block(id.clone()),
139        Some(Value::Object(p)) => {
140            if let Some(h) = p.get("heading").and_then(Value::as_str) {
141                Parent::Section {
142                    heading: h.to_owned(),
143                }
144            } else if p.contains_key("doc") {
145                Parent::Doc
146            } else {
147                return Err("to.parent must be an id, { doc: true } or { heading, scope }".into());
148            }
149        }
150        Some(_) => return Err("to.parent must be an id or an object".into()),
151    };
152    let at = match obj.get("at") {
153        Some(Value::String(s)) if s == "start" => At::Start,
154        Some(Value::String(s)) if s == "end" => At::End,
155        Some(Value::Object(a)) => {
156            if let Some(b) = a.get("before").and_then(Value::as_str) {
157                At::Before(b.to_owned())
158            } else if let Some(b) = a.get("after").and_then(Value::as_str) {
159                At::After(b.to_owned())
160            } else {
161                return Err("to.at must be start, end, { before } or { after }".into());
162            }
163        }
164        _ => return Err("to.at must be start, end, { before } or { after }".into()),
165    };
166    Ok(To { parent, at })
167}
168
169fn strings(v: Option<&Value>, what: &str) -> std::result::Result<Vec<String>, String> {
170    v.and_then(Value::as_array)
171        .ok_or_else(|| format!("{what} must be an array of ids"))?
172        .iter()
173        .map(|x| {
174            x.as_str()
175                .map(str::to_owned)
176                .ok_or_else(|| format!("{what} must be an array of ids"))
177        })
178        .collect()
179}
180
181fn string(v: Option<&Value>, what: &str) -> std::result::Result<String, String> {
182    v.and_then(Value::as_str)
183        .map(str::to_owned)
184        .ok_or_else(|| format!("{what} must be a string"))
185}
186
187fn opt_string(v: Option<&Value>, what: &str) -> std::result::Result<Option<String>, String> {
188    match v {
189        None | Some(Value::Null) => Ok(None),
190        Some(Value::String(s)) => Ok(Some(s.clone())),
191        Some(_) => Err(format!("{what} must be a string")),
192    }
193}
194
195impl Op {
196    /// The wire form (§4 `Op`; `child_ids` as the spec spells it).
197    #[must_use]
198    pub fn to_json(&self) -> Value {
199        let mut m = Map::new();
200        m.insert("op".to_owned(), json!(self.name()));
201        match self {
202            Op::Insert {
203                doc,
204                to,
205                markdown,
206                expect,
207            } => {
208                if let Some(d) = doc {
209                    m.insert("doc".to_owned(), json!(d));
210                }
211                m.insert("to".to_owned(), to_to_json(to));
212                m.insert("markdown".to_owned(), json!(markdown));
213                if let Some(e) = expect {
214                    m.insert("expect".to_owned(), expect_to_json(e));
215                }
216            }
217            Op::Update {
218                block,
219                markdown,
220                attrs,
221                expect,
222                trivia,
223                child_ids,
224            } => {
225                m.insert("block".to_owned(), json!(block));
226                if let Some(md) = markdown {
227                    m.insert("markdown".to_owned(), json!(md));
228                }
229                if let Some(a) = attrs {
230                    m.insert("attrs".to_owned(), Value::Object(a.clone()));
231                }
232                if let Some(e) = expect {
233                    m.insert("expect".to_owned(), expect_to_json(e));
234                }
235                if let Some(t) = trivia {
236                    m.insert("trivia".to_owned(), json!(t));
237                }
238                if let Some(c) = child_ids {
239                    m.insert("child_ids".to_owned(), json!(c));
240                }
241            }
242            Op::Move { blocks, to, expect } => {
243                m.insert("blocks".to_owned(), json!(blocks));
244                m.insert("to".to_owned(), to_to_json(to));
245                if let Some(e) = expect {
246                    m.insert("expect".to_owned(), expect_to_json(e));
247                }
248            }
249            Op::Remove { blocks, expect } => {
250                m.insert("blocks".to_owned(), json!(blocks));
251                if let Some(e) = expect {
252                    m.insert("expect".to_owned(), expect_map_to_json(e));
253                }
254            }
255            Op::Split { block, at, expect } => {
256                m.insert("block".to_owned(), json!(block));
257                m.insert("at".to_owned(), json!(at));
258                if let Some(e) = expect {
259                    m.insert("expect".to_owned(), expect_to_json(e));
260                }
261            }
262            Op::Merge {
263                blocks,
264                separator,
265                expect,
266            } => {
267                m.insert("blocks".to_owned(), json!(blocks));
268                if let Some(s) = separator {
269                    m.insert("separator".to_owned(), json!(s));
270                }
271                if let Some(e) = expect {
272                    m.insert("expect".to_owned(), expect_map_to_json(e));
273                }
274            }
275        }
276        Value::Object(m)
277    }
278
279    /// Parse the wire form (`child_ids` or the reference's `childIds`).
280    pub fn from_json(v: &Value) -> std::result::Result<Op, String> {
281        let obj = v.as_object().ok_or("an op must be an object")?;
282        let kind = obj
283            .get("op")
284            .and_then(Value::as_str)
285            .ok_or("op.op missing")?;
286        let expect_one = |k: &str| -> std::result::Result<Option<Expect>, String> {
287            match obj.get(k) {
288                None | Some(Value::Null) => Ok(None),
289                Some(e) => Ok(Some(expect_from_json(e)?)),
290            }
291        };
292        let expect_many =
293            |k: &str| -> std::result::Result<Option<BTreeMap<String, Expect>>, String> {
294                match obj.get(k) {
295                    None | Some(Value::Null) => Ok(None),
296                    Some(e) => Ok(Some(expect_map_from_json(e)?)),
297                }
298            };
299        Ok(match kind {
300            "insert" => Op::Insert {
301                doc: opt_string(obj.get("doc"), "insert.doc")?,
302                to: to_from_json(obj.get("to").ok_or("insert.to missing")?)?,
303                markdown: string(obj.get("markdown"), "insert.markdown")?,
304                expect: expect_one("expect")?,
305            },
306            "update" => {
307                let attrs = match obj.get("attrs") {
308                    None | Some(Value::Null) => None,
309                    Some(Value::Object(a)) => Some(a.clone()),
310                    Some(_) => return Err("update.attrs must be an object".into()),
311                };
312                let child_ids = match obj.get("child_ids").or_else(|| obj.get("childIds")) {
313                    None | Some(Value::Null) => None,
314                    Some(Value::Object(c)) => Some(
315                        c.iter()
316                            .map(|(k, v)| {
317                                v.as_str()
318                                    .map(|s| (k.clone(), s.to_owned()))
319                                    .ok_or_else(|| "update.child_ids values must be ids".to_owned())
320                            })
321                            .collect::<std::result::Result<BTreeMap<_, _>, _>>()?,
322                    ),
323                    Some(_) => return Err("update.child_ids must be an object".into()),
324                };
325                Op::Update {
326                    block: string(obj.get("block"), "update.block")?,
327                    markdown: opt_string(obj.get("markdown"), "update.markdown")?,
328                    attrs,
329                    expect: expect_one("expect")?,
330                    trivia: opt_string(obj.get("trivia"), "update.trivia")?,
331                    child_ids,
332                }
333            }
334            "move" => Op::Move {
335                blocks: strings(obj.get("blocks"), "move.blocks")?,
336                to: to_from_json(obj.get("to").ok_or("move.to missing")?)?,
337                expect: expect_one("expect")?,
338            },
339            "remove" => Op::Remove {
340                blocks: strings(obj.get("blocks"), "remove.blocks")?,
341                expect: expect_many("expect")?,
342            },
343            "split" => Op::Split {
344                block: string(obj.get("block"), "split.block")?,
345                at: obj
346                    .get("at")
347                    .and_then(Value::as_array)
348                    .ok_or("split.at must be an array of offsets")?
349                    .iter()
350                    .map(|x| {
351                        x.as_u64()
352                            .and_then(|n| usize::try_from(n).ok())
353                            .ok_or_else(|| "split.at must be non-negative integers".to_owned())
354                    })
355                    .collect::<std::result::Result<Vec<_>, _>>()?,
356                expect: expect_one("expect")?,
357            },
358            "merge" => Op::Merge {
359                blocks: strings(obj.get("blocks"), "merge.blocks")?,
360                separator: opt_string(obj.get("separator"), "merge.separator")?,
361                expect: expect_many("expect")?,
362            },
363            other => return Err(format!("unknown op {other}")),
364        })
365    }
366}
367
368// ---- placeholders (§4 step 1) --------------------------------------------------------
369
370/// `$<n>.ids[<i>]` → `(n, i)`; `None` when `value` is not a placeholder.
371#[must_use]
372pub fn parse_placeholder(value: &str) -> Option<(usize, usize)> {
373    let rest = value.strip_prefix('$')?;
374    let (n, rest) = rest.split_once(".ids[")?;
375    let i = rest.strip_suffix(']')?;
376    if n.is_empty()
377        || i.is_empty()
378        || !n.bytes().all(|b| b.is_ascii_digit())
379        || !i.bytes().all(|b| b.is_ascii_digit())
380    {
381        return None;
382    }
383    Some((n.parse().ok()?, i.parse().ok()?))
384}
385
386/// Resolve a placeholder against earlier results; a non-placeholder passes
387/// through; an index out of range is `target_missing`.
388pub fn resolve_placeholder(value: &str, results: &[OpResult]) -> Result<String> {
389    let Some((op_idx, id_idx)) = parse_placeholder(value) else {
390        return Ok(value.to_owned());
391    };
392    results
393        .get(op_idx)
394        .and_then(|r| r.ids.get(id_idx))
395        .filter(|id| !id.is_empty())
396        .cloned()
397        .ok_or_else(|| {
398            MutationError::new(
399                ErrorCode::TargetMissing,
400                format!("placeholder {value} did not resolve"),
401            )
402        })
403}
404
405/// Resolve the placeholders a `To` may carry (`parent`, `before`/`after`).
406pub fn resolve_to(to: &To, results: &[OpResult]) -> Result<To> {
407    let parent = match &to.parent {
408        Parent::Block(id) => Parent::Block(resolve_placeholder(id, results)?),
409        other => other.clone(),
410    };
411    let at = match &to.at {
412        At::Before(id) => At::Before(resolve_placeholder(id, results)?),
413        At::After(id) => At::After(resolve_placeholder(id, results)?),
414        other => other.clone(),
415    };
416    Ok(To { parent, at })
417}
418
419/// Resolve every placeholder-bearing field of an op (`block`, `blocks[]`,
420/// `to`), as `apply` does before dispatching it.
421pub fn resolve_op(op: &Op, results: &[OpResult]) -> Result<Op> {
422    let ids = |list: &[String]| -> Result<Vec<String>> {
423        list.iter()
424            .map(|b| resolve_placeholder(b, results))
425            .collect()
426    };
427    Ok(match op {
428        Op::Insert {
429            doc,
430            to,
431            markdown,
432            expect,
433        } => Op::Insert {
434            doc: doc.clone(),
435            to: resolve_to(to, results)?,
436            markdown: markdown.clone(),
437            expect: expect.clone(),
438        },
439        Op::Update {
440            block,
441            markdown,
442            attrs,
443            expect,
444            trivia,
445            child_ids,
446        } => Op::Update {
447            block: resolve_placeholder(block, results)?,
448            markdown: markdown.clone(),
449            attrs: attrs.clone(),
450            expect: expect.clone(),
451            trivia: trivia.clone(),
452            child_ids: child_ids.clone(),
453        },
454        Op::Move { blocks, to, expect } => Op::Move {
455            blocks: ids(blocks)?,
456            to: resolve_to(to, results)?,
457            expect: expect.clone(),
458        },
459        Op::Remove { blocks, expect } => Op::Remove {
460            blocks: ids(blocks)?,
461            expect: expect.clone(),
462        },
463        Op::Split { block, at, expect } => Op::Split {
464            block: resolve_placeholder(block, results)?,
465            at: at.clone(),
466            expect: expect.clone(),
467        },
468        Op::Merge {
469            blocks,
470            separator,
471            expect,
472        } => Op::Merge {
473            blocks: ids(blocks)?,
474            separator: separator.clone(),
475            expect: expect.clone(),
476        },
477    })
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn placeholders_parse_and_resolve() {
486        assert_eq!(parse_placeholder("$0.ids[1]"), Some((0, 1)));
487        assert_eq!(parse_placeholder("$12.ids[0]"), Some((12, 0)));
488        assert_eq!(parse_placeholder("b_x"), None);
489        assert_eq!(parse_placeholder("$a.ids[0]"), None);
490        assert_eq!(parse_placeholder("$0.ids[]"), None);
491        let results = vec![OpResult::ids(vec!["b_1".into(), "b_2".into()])];
492        assert_eq!(resolve_placeholder("$0.ids[1]", &results).unwrap(), "b_2");
493        assert_eq!(resolve_placeholder("b_9", &results).unwrap(), "b_9");
494        let e = resolve_placeholder("$0.ids[2]", &results).unwrap_err();
495        assert_eq!(e.code, ErrorCode::TargetMissing);
496        let e = resolve_placeholder("$1.ids[0]", &results).unwrap_err();
497        assert_eq!(e.code, ErrorCode::TargetMissing);
498        let to = To {
499            parent: Parent::Block("$0.ids[0]".into()),
500            at: At::After("$0.ids[1]".into()),
501        };
502        assert_eq!(
503            resolve_to(&to, &results).unwrap(),
504            To {
505                parent: Parent::Block("b_1".into()),
506                at: At::After("b_2".into()),
507            }
508        );
509        let op = Op::Move {
510            blocks: vec!["$0.ids[0]".into()],
511            to: To {
512                parent: Parent::Doc,
513                at: At::Start,
514            },
515            expect: None,
516        };
517        assert!(
518            matches!(resolve_op(&op, &results).unwrap(), Op::Move { blocks, .. } if blocks == ["b_1"])
519        );
520    }
521
522    #[test]
523    fn ops_round_trip_through_json() {
524        let ops = json!([
525            { "op": "insert", "doc": "a.md", "to": { "parent": { "doc": true }, "at": "end" }, "markdown": "x", "expect": { "parent_children_hash": "cd" } },
526            { "op": "insert", "to": { "parent": { "heading": "b_0", "scope": "section" }, "at": { "after": "b_1" } }, "markdown": "x" },
527            { "op": "update", "block": "b_1", "markdown": "y", "attrs": { "checked": true }, "expect": { "content_hash": "ab" }, "trivia": "\n", "child_ids": { "/0": "b_2" } },
528            { "op": "move", "blocks": ["b_1"], "to": { "parent": "b_0", "at": { "before": "b_2" } }, "expect": { "parent_children_hash": "cd" } },
529            { "op": "remove", "blocks": ["b_1"], "expect": { "b_1": { "content_hash": "ab" } } },
530            { "op": "split", "block": "b_1", "at": [3, 7], "expect": { "content_hash": "ab" } },
531            { "op": "merge", "blocks": ["b_1", "b_2"], "separator": "\n" },
532        ]);
533        for v in ops.as_array().unwrap() {
534            let op = Op::from_json(v).unwrap();
535            assert_eq!(op.to_json(), *v);
536        }
537        // The reference's `childIds` spelling and `parent: null` are accepted.
538        let op =
539            Op::from_json(&json!({ "op": "update", "block": "b", "childIds": { "/0": "b_2" } }))
540                .unwrap();
541        assert!(matches!(op, Op::Update { child_ids: Some(c), .. } if c["/0"] == "b_2"));
542        let to = to_from_json(&json!({ "parent": null, "at": "start" })).unwrap();
543        assert_eq!(to.parent, Parent::Doc);
544        assert!(Op::from_json(&json!({ "op": "nope" })).is_err());
545        assert!(Op::from_json(&json!({ "op": "split", "block": "b", "at": [-1] })).is_err());
546    }
547}