Skip to main content

open_gpui_canvas/
changes.rs

1use crate::{
2    CanvasEdge, CanvasNode, CanvasRecordId, CanvasRecordRelation, CanvasShape, CanvasTransaction,
3    CanvasValue, DocumentCommand,
4};
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
9pub enum CanvasRecord {
10    Node(CanvasNode),
11    Edge(CanvasEdge),
12    Shape(CanvasShape),
13}
14
15impl CanvasRecord {
16    pub fn id(&self) -> CanvasRecordId {
17        match self {
18            Self::Node(node) => CanvasRecordId::Node(node.id.clone()),
19            Self::Edge(edge) => CanvasRecordId::Edge(edge.id.clone()),
20            Self::Shape(shape) => CanvasRecordId::Shape(shape.id.clone()),
21        }
22    }
23}
24
25impl From<CanvasNode> for CanvasRecord {
26    fn from(value: CanvasNode) -> Self {
27        Self::Node(value)
28    }
29}
30
31impl From<CanvasEdge> for CanvasRecord {
32    fn from(value: CanvasEdge) -> Self {
33        Self::Edge(value)
34    }
35}
36
37impl From<CanvasShape> for CanvasRecord {
38    fn from(value: CanvasShape) -> Self {
39        Self::Shape(value)
40    }
41}
42
43#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
44pub enum CanvasRecordChange {
45    Upsert(CanvasRecord),
46    Delete(CanvasRecordId),
47}
48
49impl CanvasRecordChange {
50    pub fn id(&self) -> CanvasRecordId {
51        match self {
52            Self::Upsert(record) => record.id(),
53            Self::Delete(id) => id.clone(),
54        }
55    }
56}
57
58#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
59pub enum CanvasRelationChange {
60    Upsert(CanvasRecordRelation),
61    Delete(CanvasRecordRelation),
62}
63
64impl CanvasRelationChange {
65    pub fn relation(&self) -> &CanvasRecordRelation {
66        match self {
67            Self::Upsert(relation) | Self::Delete(relation) => relation,
68        }
69    }
70}
71
72#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
73#[serde(transparent)]
74pub struct CanvasChangeOrigin(String);
75
76impl CanvasChangeOrigin {
77    pub fn new(origin: impl Into<String>) -> Self {
78        Self(origin.into())
79    }
80
81    pub fn as_str(&self) -> &str {
82        &self.0
83    }
84}
85
86impl From<&str> for CanvasChangeOrigin {
87    fn from(value: &str) -> Self {
88        Self::new(value)
89    }
90}
91
92impl From<String> for CanvasChangeOrigin {
93    fn from(value: String) -> Self {
94        Self::new(value)
95    }
96}
97
98impl fmt::Display for CanvasChangeOrigin {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.write_str(self.as_str())
101    }
102}
103
104#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
105pub struct CanvasRecordOperation {
106    pub transaction_sequence: u64,
107    pub operation_index: u64,
108    pub change: CanvasRecordChange,
109}
110
111impl CanvasRecordOperation {
112    pub fn new(
113        transaction_sequence: u64,
114        operation_index: u64,
115        change: CanvasRecordChange,
116    ) -> Self {
117        Self {
118            transaction_sequence,
119            operation_index,
120            change,
121        }
122    }
123
124    pub fn id(&self) -> CanvasRecordId {
125        self.change.id()
126    }
127}
128
129#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
130pub struct CanvasRecordOperationBatch {
131    pub transaction_sequence: u64,
132    #[serde(default)]
133    pub origin: Option<CanvasChangeOrigin>,
134    #[serde(default)]
135    pub transaction_metadata: CanvasValue,
136    #[serde(default)]
137    pub operations: Vec<CanvasRecordOperation>,
138}
139
140impl CanvasRecordOperationBatch {
141    pub fn new(transaction_sequence: u64, transaction: &CanvasTransaction) -> Self {
142        Self::from_record_changes(
143            transaction_sequence,
144            transaction.metadata.clone(),
145            transaction.record_changes(),
146        )
147    }
148
149    pub fn from_record_changes(
150        transaction_sequence: u64,
151        transaction_metadata: CanvasValue,
152        changes: impl IntoIterator<Item = CanvasRecordChange>,
153    ) -> Self {
154        Self {
155            transaction_sequence,
156            origin: None,
157            transaction_metadata,
158            operations: changes
159                .into_iter()
160                .enumerate()
161                .map(|(index, change)| {
162                    CanvasRecordOperation::new(transaction_sequence, index as u64, change)
163                })
164                .collect(),
165        }
166    }
167
168    pub fn with_origin(mut self, origin: impl Into<CanvasChangeOrigin>) -> Self {
169        self.origin = Some(origin.into());
170        self
171    }
172
173    pub fn is_empty(&self) -> bool {
174        self.operations.is_empty()
175    }
176
177    pub fn changes(&self) -> impl Iterator<Item = &CanvasRecordChange> {
178        self.operations.iter().map(|operation| &operation.change)
179    }
180}
181
182#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
183pub struct CanvasRelationOperation {
184    pub transaction_sequence: u64,
185    pub operation_index: u64,
186    pub change: CanvasRelationChange,
187}
188
189impl CanvasRelationOperation {
190    pub fn new(
191        transaction_sequence: u64,
192        operation_index: u64,
193        change: CanvasRelationChange,
194    ) -> Self {
195        Self {
196            transaction_sequence,
197            operation_index,
198            change,
199        }
200    }
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
204pub struct CanvasRelationOperationBatch {
205    pub transaction_sequence: u64,
206    #[serde(default)]
207    pub origin: Option<CanvasChangeOrigin>,
208    #[serde(default)]
209    pub transaction_metadata: CanvasValue,
210    #[serde(default)]
211    pub operations: Vec<CanvasRelationOperation>,
212}
213
214impl CanvasRelationOperationBatch {
215    pub fn from_relation_changes(
216        transaction_sequence: u64,
217        transaction_metadata: CanvasValue,
218        changes: impl IntoIterator<Item = CanvasRelationChange>,
219    ) -> Self {
220        Self {
221            transaction_sequence,
222            origin: None,
223            transaction_metadata,
224            operations: changes
225                .into_iter()
226                .enumerate()
227                .map(|(index, change)| {
228                    CanvasRelationOperation::new(transaction_sequence, index as u64, change)
229                })
230                .collect(),
231        }
232    }
233
234    pub fn with_origin(mut self, origin: impl Into<CanvasChangeOrigin>) -> Self {
235        self.origin = Some(origin.into());
236        self
237    }
238
239    pub fn is_empty(&self) -> bool {
240        self.operations.is_empty()
241    }
242
243    pub fn changes(&self) -> impl Iterator<Item = &CanvasRelationChange> {
244        self.operations.iter().map(|operation| &operation.change)
245    }
246}
247
248impl DocumentCommand {
249    pub fn record_id(&self) -> Option<CanvasRecordId> {
250        Some(match self {
251            Self::InsertNode(node) | Self::UpdateNode(node) => {
252                CanvasRecordId::Node(node.id.clone())
253            }
254            Self::RemoveNode(id) => CanvasRecordId::Node(id.clone()),
255            Self::InsertEdge(edge) | Self::UpdateEdge(edge) => {
256                CanvasRecordId::Edge(edge.id.clone())
257            }
258            Self::RemoveEdge(id) => CanvasRecordId::Edge(id.clone()),
259            Self::InsertShape(shape) | Self::UpdateShape(shape) => {
260                CanvasRecordId::Shape(shape.id.clone())
261            }
262            Self::RemoveShape(id) => CanvasRecordId::Shape(id.clone()),
263            Self::SetRecordParent { child, .. } | Self::ClearRecordParent { child } => {
264                child.clone()
265            }
266            Self::AddRecordToGroup { group, .. } | Self::RemoveRecordFromGroup { group, .. } => {
267                group.clone()
268            }
269            Self::SetRecordBinding(binding) => binding.source.clone(),
270            Self::RemoveRecordBinding { .. } => return None,
271        })
272    }
273
274    pub fn record_change(&self) -> Option<CanvasRecordChange> {
275        match self {
276            Self::InsertNode(node) | Self::UpdateNode(node) => {
277                Some(CanvasRecordChange::Upsert(CanvasRecord::Node(node.clone())))
278            }
279            Self::RemoveNode(id) => {
280                Some(CanvasRecordChange::Delete(CanvasRecordId::Node(id.clone())))
281            }
282            Self::InsertEdge(edge) | Self::UpdateEdge(edge) => {
283                Some(CanvasRecordChange::Upsert(CanvasRecord::Edge(edge.clone())))
284            }
285            Self::RemoveEdge(id) => {
286                Some(CanvasRecordChange::Delete(CanvasRecordId::Edge(id.clone())))
287            }
288            Self::InsertShape(shape) | Self::UpdateShape(shape) => Some(
289                CanvasRecordChange::Upsert(CanvasRecord::Shape(shape.clone())),
290            ),
291            Self::RemoveShape(id) => Some(CanvasRecordChange::Delete(CanvasRecordId::Shape(
292                id.clone(),
293            ))),
294            Self::SetRecordParent { .. }
295            | Self::ClearRecordParent { .. }
296            | Self::AddRecordToGroup { .. }
297            | Self::RemoveRecordFromGroup { .. }
298            | Self::SetRecordBinding(_)
299            | Self::RemoveRecordBinding { .. } => None,
300        }
301    }
302}
303
304impl CanvasTransaction {
305    pub fn record_changes(&self) -> impl Iterator<Item = CanvasRecordChange> + '_ {
306        self.commands
307            .iter()
308            .filter_map(DocumentCommand::record_change)
309    }
310
311    pub fn record_ids(&self) -> impl Iterator<Item = CanvasRecordId> + '_ {
312        self.commands.iter().filter_map(DocumentCommand::record_id)
313    }
314
315    pub fn record_operations(
316        &self,
317        transaction_sequence: u64,
318    ) -> impl Iterator<Item = CanvasRecordOperation> + '_ {
319        self.record_changes()
320            .enumerate()
321            .map(move |(index, change)| {
322                CanvasRecordOperation::new(transaction_sequence, index as u64, change)
323            })
324    }
325
326    pub fn record_operation_batch(&self, transaction_sequence: u64) -> CanvasRecordOperationBatch {
327        CanvasRecordOperationBatch::new(transaction_sequence, self)
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::{
335        BindingId, CanvasEdge, CanvasEndpoint, CanvasRecordBindingRelation,
336        CanvasRecordGroupRelation, CanvasRecordParentRelation, EdgeId, NodeId, ShapeId,
337    };
338    use open_gpui::{Bounds, point, px, size};
339
340    #[test]
341    fn document_commands_expose_record_ids() {
342        assert_eq!(
343            DocumentCommand::InsertNode(CanvasNode::new(
344                "node",
345                point(px(0.0), px(0.0)),
346                size(px(10.0), px(10.0)),
347            ))
348            .record_id(),
349            Some(CanvasRecordId::Node(NodeId::from("node")))
350        );
351        assert_eq!(
352            DocumentCommand::RemoveEdge(EdgeId::from("edge")).record_id(),
353            Some(CanvasRecordId::Edge(EdgeId::from("edge")))
354        );
355        assert_eq!(
356            DocumentCommand::RemoveShape(ShapeId::from("shape")).record_id(),
357            Some(CanvasRecordId::Shape(ShapeId::from("shape")))
358        );
359        assert_eq!(
360            DocumentCommand::SetRecordBinding(CanvasRecordBindingRelation::new(
361                "binding",
362                CanvasRecordId::Node(NodeId::from("source")),
363                CanvasRecordId::Shape(ShapeId::from("target")),
364            ))
365            .record_id(),
366            Some(CanvasRecordId::Node(NodeId::from("source")))
367        );
368        assert_eq!(
369            DocumentCommand::RemoveRecordBinding {
370                id: BindingId::from("binding")
371            }
372            .record_id(),
373            None
374        );
375    }
376
377    #[test]
378    fn document_commands_expose_record_changes() {
379        let node = CanvasNode::new("node", point(px(0.0), px(0.0)), size(px(10.0), px(10.0)));
380        let edge = CanvasEdge::new(
381            "edge",
382            CanvasEndpoint::new("a", None::<&str>),
383            CanvasEndpoint::new("b", None::<&str>),
384        );
385        let shape = CanvasShape::new(
386            "shape",
387            Bounds::new(point(px(0.0), px(0.0)), size(px(10.0), px(10.0))),
388        );
389
390        assert_eq!(
391            DocumentCommand::UpdateNode(node.clone()).record_change(),
392            Some(CanvasRecordChange::Upsert(CanvasRecord::Node(node)))
393        );
394        assert_eq!(
395            DocumentCommand::InsertEdge(edge.clone()).record_change(),
396            Some(CanvasRecordChange::Upsert(CanvasRecord::Edge(edge)))
397        );
398        assert_eq!(
399            DocumentCommand::UpdateShape(shape.clone()).record_change(),
400            Some(CanvasRecordChange::Upsert(CanvasRecord::Shape(shape)))
401        );
402        assert_eq!(
403            DocumentCommand::RemoveNode(NodeId::from("node")).record_change(),
404            Some(CanvasRecordChange::Delete(CanvasRecordId::Node(
405                NodeId::from("node")
406            )))
407        );
408        assert_eq!(
409            DocumentCommand::RemoveRecordBinding {
410                id: BindingId::from("binding")
411            }
412            .record_change(),
413            None
414        );
415    }
416
417    #[test]
418    fn transactions_preserve_record_change_order() {
419        let transaction = CanvasTransaction::new([
420            DocumentCommand::InsertNode(CanvasNode::new(
421                "a",
422                point(px(0.0), px(0.0)),
423                size(px(10.0), px(10.0)),
424            )),
425            DocumentCommand::RemoveShape(ShapeId::from("old-shape")),
426        ]);
427
428        assert_eq!(
429            transaction.record_ids().collect::<Vec<_>>(),
430            vec![
431                CanvasRecordId::Node(NodeId::from("a")),
432                CanvasRecordId::Shape(ShapeId::from("old-shape")),
433            ]
434        );
435        assert_eq!(
436            transaction.record_changes().collect::<Vec<_>>(),
437            vec![
438                CanvasRecordChange::Upsert(CanvasRecord::Node(CanvasNode::new(
439                    "a",
440                    point(px(0.0), px(0.0)),
441                    size(px(10.0), px(10.0)),
442                ))),
443                CanvasRecordChange::Delete(CanvasRecordId::Shape(ShapeId::from("old-shape"))),
444            ]
445        );
446    }
447
448    #[test]
449    fn transactions_expose_record_operation_batches() {
450        let mut transaction = CanvasTransaction::new([
451            DocumentCommand::InsertNode(CanvasNode::new(
452                "a",
453                point(px(0.0), px(0.0)),
454                size(px(10.0), px(10.0)),
455            )),
456            DocumentCommand::RemoveShape(ShapeId::from("old-shape")),
457        ]);
458        transaction
459            .metadata
460            .insert("reason".into(), serde_json::json!("sync"));
461
462        let batch = transaction
463            .record_operation_batch(42)
464            .with_origin("client-a");
465
466        assert_eq!(batch.transaction_sequence, 42);
467        assert_eq!(
468            batch.origin.as_ref().map(CanvasChangeOrigin::as_str),
469            Some("client-a")
470        );
471        assert_eq!(
472            batch.transaction_metadata.get("reason"),
473            Some(&serde_json::json!("sync"))
474        );
475        assert_eq!(
476            batch
477                .operations
478                .iter()
479                .map(|operation| (operation.transaction_sequence, operation.operation_index))
480                .collect::<Vec<_>>(),
481            vec![(42, 0), (42, 1)]
482        );
483        assert_eq!(
484            batch.changes().cloned().collect::<Vec<_>>(),
485            transaction.record_changes().collect::<Vec<_>>()
486        );
487        assert!(!batch.is_empty());
488    }
489
490    #[test]
491    fn empty_transaction_operation_batch_is_empty() {
492        let batch = CanvasTransaction::default().record_operation_batch(7);
493
494        assert_eq!(batch.transaction_sequence, 7);
495        assert!(batch.is_empty());
496        assert!(batch.transaction_metadata.is_empty());
497    }
498
499    #[test]
500    fn relation_operation_batches_preserve_order_and_metadata() {
501        let mut metadata = CanvasValue::new();
502        metadata.insert("reason".into(), serde_json::json!("sync"));
503        let changes = vec![
504            CanvasRelationChange::Upsert(CanvasRecordRelation::Parent(
505                CanvasRecordParentRelation::new(NodeId::from("child"), ShapeId::from("frame")),
506            )),
507            CanvasRelationChange::Upsert(CanvasRecordRelation::Group(
508                CanvasRecordGroupRelation::new(ShapeId::from("frame"), NodeId::from("child")),
509            )),
510        ];
511
512        let batch = CanvasRelationOperationBatch::from_relation_changes(
513            42,
514            metadata.clone(),
515            changes.clone(),
516        )
517        .with_origin("client-a");
518
519        assert_eq!(batch.transaction_sequence, 42);
520        assert_eq!(
521            batch.origin.as_ref().map(CanvasChangeOrigin::as_str),
522            Some("client-a")
523        );
524        assert_eq!(batch.transaction_metadata, metadata);
525        assert_eq!(
526            batch
527                .operations
528                .iter()
529                .map(|operation| (operation.transaction_sequence, operation.operation_index))
530                .collect::<Vec<_>>(),
531            vec![(42, 0), (42, 1)]
532        );
533        assert_eq!(batch.changes().cloned().collect::<Vec<_>>(), changes);
534    }
535}