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