Skip to main content

sim_lib_doc_core/
edit.rs

1//! Open reversible edit contract for document domains.
2
3use sim_kernel::{Cx, Value};
4
5use crate::{Doc, DocId, OfficeError};
6
7/// Reversible edit carried by a document domain.
8#[derive(Clone, Debug, PartialEq)]
9pub struct Edit {
10    /// Document the edit targets.
11    pub doc: DocId,
12    /// Domain namespace that understands the operation payload.
13    pub domain: String,
14    /// Domain-owned operation payload.
15    pub op: Value,
16    /// Domain-owned inverse payload.
17    pub inverse: Value,
18}
19
20impl Edit {
21    /// Builds an open reversible edit.
22    #[must_use]
23    pub fn new(doc: DocId, domain: impl Into<String>, op: Value, inverse: Value) -> Self {
24        Self {
25            doc,
26            domain: domain.into(),
27            op,
28            inverse,
29        }
30    }
31
32    /// Returns the edit that reverses this edit.
33    #[must_use]
34    pub fn inverted(&self) -> Self {
35        Self {
36            doc: self.doc.clone(),
37            domain: self.domain.clone(),
38            op: self.inverse.clone(),
39            inverse: self.op.clone(),
40        }
41    }
42}
43
44/// Domain implementation for applying and inverting open edit payloads.
45pub trait DomainEdit {
46    /// Stable domain namespace for this edit implementation.
47    fn domain(&self) -> &'static str;
48    /// Applies a domain operation to a document.
49    fn apply(&self, cx: &mut Cx, doc: &mut Doc, op: &Value) -> Result<(), OfficeError>;
50    /// Produces the domain operation that reverses `op`.
51    fn invert(&self, op: &Value) -> Value;
52}
53
54/// Returns the inverse edit.
55#[must_use]
56pub fn invert(edit: &Edit) -> Edit {
57    edit.inverted()
58}
59
60#[cfg(test)]
61mod tests {
62    use std::sync::Arc;
63
64    use sim_kernel::{DefaultFactory, NoopEvalPolicy};
65
66    use crate::DocKind;
67
68    use super::*;
69
70    struct BodySwapEdit {
71        inverse: Value,
72    }
73
74    impl DomainEdit for BodySwapEdit {
75        fn domain(&self) -> &'static str {
76            "office/body-swap"
77        }
78
79        fn apply(&self, _cx: &mut Cx, doc: &mut Doc, op: &Value) -> Result<(), OfficeError> {
80            doc.body = op.clone();
81            Ok(())
82        }
83
84        fn invert(&self, _op: &Value) -> Value {
85            self.inverse.clone()
86        }
87    }
88
89    #[test]
90    fn edit_round_trips_through_double_invert() {
91        let cx = Cx::new(
92            Arc::new(NoopEvalPolicy),
93            Arc::new(DefaultFactory),
94            sim_kernel::HandleSeed::new(0xc37b_b833_0649_c0de),
95        );
96        let op = cx.factory().string("set title".to_owned()).unwrap();
97        let inverse = cx.factory().string("restore title".to_owned()).unwrap();
98        let edit = Edit::new(DocId::new("doc-1"), "office/body", op, inverse);
99
100        assert_eq!(invert(&invert(&edit)), edit);
101    }
102
103    #[test]
104    fn domain_edit_round_trips_without_core_enum_variant() {
105        let mut cx = Cx::new(
106            Arc::new(NoopEvalPolicy),
107            Arc::new(DefaultFactory),
108            sim_kernel::HandleSeed::new(0x251d_1f52_8144_2a42),
109        );
110        let old_body = cx.factory().string("old".to_owned()).unwrap();
111        let new_body = cx.factory().string("new".to_owned()).unwrap();
112        let domain = BodySwapEdit {
113            inverse: old_body.clone(),
114        };
115        let mut doc = Doc::new(
116            DocKind::new("report"),
117            DocId::new("doc-1"),
118            old_body.clone(),
119            vec![],
120        );
121        let edit = Edit::new(
122            doc.id.clone(),
123            domain.domain(),
124            new_body.clone(),
125            domain.invert(&new_body),
126        );
127
128        domain.apply(&mut cx, &mut doc, &edit.op).unwrap();
129        assert_eq!(doc.body, new_body);
130        domain.apply(&mut cx, &mut doc, &edit.inverse).unwrap();
131        assert_eq!(doc.body, old_body);
132        assert_eq!(invert(&invert(&edit)), edit);
133    }
134}