1use 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#[derive(Clone, Debug, PartialEq)]
16pub enum Op {
17 Insert {
18 doc: Option<String>,
20 to: To,
21 markdown: String,
22 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 expect: Option<Expect>,
38 },
39 Remove {
40 blocks: Vec<String>,
41 expect: Option<BTreeMap<String, Expect>>,
42 },
43 Split {
44 block: String,
45 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 #[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
71fn 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#[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
133pub 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 #[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 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#[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
386pub 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
405pub 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
419pub 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 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}