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}