Skip to main content

open_gpui_canvas/
relations.rs

1use crate::{BindingId, CanvasRecordId};
2use indexmap::IndexSet;
3use serde::{Deserialize, Serialize};
4
5#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum CanvasRecordRelationKind {
8    Parent,
9    Group,
10    Binding,
11}
12
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
14pub enum CanvasRecordRelationKey<'a> {
15    Parent {
16        child: &'a CanvasRecordId,
17        parent: &'a CanvasRecordId,
18    },
19    Group {
20        group: &'a CanvasRecordId,
21        member: &'a CanvasRecordId,
22    },
23    Binding {
24        id: &'a BindingId,
25    },
26}
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
29pub struct CanvasRecordParentRelation {
30    pub child: CanvasRecordId,
31    pub parent: CanvasRecordId,
32}
33
34impl CanvasRecordParentRelation {
35    pub fn new(child: impl Into<CanvasRecordId>, parent: impl Into<CanvasRecordId>) -> Self {
36        Self {
37            child: child.into(),
38            parent: parent.into(),
39        }
40    }
41}
42
43#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
44pub struct CanvasRecordGroupRelation {
45    pub group: CanvasRecordId,
46    pub member: CanvasRecordId,
47}
48
49impl CanvasRecordGroupRelation {
50    pub fn new(group: impl Into<CanvasRecordId>, member: impl Into<CanvasRecordId>) -> Self {
51        Self {
52            group: group.into(),
53            member: member.into(),
54        }
55    }
56}
57
58#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
59pub struct CanvasRecordBindingRelation {
60    pub id: BindingId,
61    #[serde(default = "default_binding_kind")]
62    pub kind: String,
63    pub source: CanvasRecordId,
64    pub target: CanvasRecordId,
65}
66
67impl CanvasRecordBindingRelation {
68    pub fn new(
69        id: impl Into<BindingId>,
70        source: impl Into<CanvasRecordId>,
71        target: impl Into<CanvasRecordId>,
72    ) -> Self {
73        Self {
74            id: id.into(),
75            kind: default_binding_kind(),
76            source: source.into(),
77            target: target.into(),
78        }
79    }
80
81    pub fn with_kind(mut self, kind: impl Into<String>) -> Self {
82        self.kind = kind.into();
83        self
84    }
85}
86
87fn default_binding_kind() -> String {
88    "binding".to_string()
89}
90
91#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
92pub enum CanvasRecordRelation {
93    Parent(CanvasRecordParentRelation),
94    Group(CanvasRecordGroupRelation),
95    Binding(CanvasRecordBindingRelation),
96}
97
98impl CanvasRecordRelation {
99    pub fn kind(&self) -> CanvasRecordRelationKind {
100        match self {
101            Self::Parent(_) => CanvasRecordRelationKind::Parent,
102            Self::Group(_) => CanvasRecordRelationKind::Group,
103            Self::Binding(_) => CanvasRecordRelationKind::Binding,
104        }
105    }
106
107    pub fn relation_key(&self) -> CanvasRecordRelationKey<'_> {
108        match self {
109            Self::Parent(relation) => CanvasRecordRelationKey::Parent {
110                child: &relation.child,
111                parent: &relation.parent,
112            },
113            Self::Group(relation) => CanvasRecordRelationKey::Group {
114                group: &relation.group,
115                member: &relation.member,
116            },
117            Self::Binding(relation) => CanvasRecordRelationKey::Binding { id: &relation.id },
118        }
119    }
120}
121
122impl From<CanvasRecordParentRelation> for CanvasRecordRelation {
123    fn from(value: CanvasRecordParentRelation) -> Self {
124        Self::Parent(value)
125    }
126}
127
128impl From<CanvasRecordGroupRelation> for CanvasRecordRelation {
129    fn from(value: CanvasRecordGroupRelation) -> Self {
130        Self::Group(value)
131    }
132}
133
134impl From<CanvasRecordBindingRelation> for CanvasRecordRelation {
135    fn from(value: CanvasRecordBindingRelation) -> Self {
136        Self::Binding(value)
137    }
138}
139
140#[derive(Clone, Debug, Default, Serialize, Deserialize)]
141pub struct CanvasRecordRelations {
142    #[serde(default)]
143    parents: Vec<CanvasRecordParentRelation>,
144    #[serde(default)]
145    groups: Vec<CanvasRecordGroupRelation>,
146    #[serde(default)]
147    bindings: Vec<CanvasRecordBindingRelation>,
148}
149
150impl PartialEq for CanvasRecordRelations {
151    fn eq(&self, other: &Self) -> bool {
152        self.parents.len() == other.parents.len()
153            && self.groups.len() == other.groups.len()
154            && self.bindings.len() == other.bindings.len()
155            && self.parents.iter().all(|relation| {
156                other.contains_relation(&CanvasRecordRelation::Parent(relation.clone()))
157            })
158            && self.groups.iter().all(|relation| {
159                other.contains_relation(&CanvasRecordRelation::Group(relation.clone()))
160            })
161            && self.bindings.iter().all(|relation| {
162                other.contains_relation(&CanvasRecordRelation::Binding(relation.clone()))
163            })
164    }
165}
166
167impl Eq for CanvasRecordRelations {}
168
169impl CanvasRecordRelations {
170    pub fn builder() -> CanvasRecordRelationsBuilder {
171        CanvasRecordRelationsBuilder::new()
172    }
173
174    pub fn is_empty(&self) -> bool {
175        self.parents.is_empty() && self.groups.is_empty() && self.bindings.is_empty()
176    }
177
178    pub fn parents(&self) -> impl Iterator<Item = &CanvasRecordParentRelation> {
179        self.parents.iter()
180    }
181
182    pub fn groups(&self) -> impl Iterator<Item = &CanvasRecordGroupRelation> {
183        self.groups.iter()
184    }
185
186    pub fn bindings(&self) -> impl Iterator<Item = &CanvasRecordBindingRelation> {
187        self.bindings.iter()
188    }
189
190    pub fn binding(&self, id: &BindingId) -> Option<&CanvasRecordBindingRelation> {
191        self.bindings.iter().find(|relation| &relation.id == id)
192    }
193
194    pub fn parent_of(&self, child: &CanvasRecordId) -> Option<&CanvasRecordId> {
195        self.parents
196            .iter()
197            .find(|relation| &relation.child == child)
198            .map(|relation| &relation.parent)
199    }
200
201    pub fn children_of(&self, parent: &CanvasRecordId) -> impl Iterator<Item = &CanvasRecordId> {
202        self.parents
203            .iter()
204            .filter(move |relation| &relation.parent == parent)
205            .map(|relation| &relation.child)
206    }
207
208    pub fn members_of(&self, group: &CanvasRecordId) -> impl Iterator<Item = &CanvasRecordId> {
209        self.groups
210            .iter()
211            .filter(move |relation| &relation.group == group)
212            .map(|relation| &relation.member)
213    }
214
215    pub fn groups_for(&self, member: &CanvasRecordId) -> impl Iterator<Item = &CanvasRecordId> {
216        self.groups
217            .iter()
218            .filter(move |relation| &relation.member == member)
219            .map(|relation| &relation.group)
220    }
221
222    pub fn bindings_for(
223        &self,
224        record: &CanvasRecordId,
225    ) -> impl Iterator<Item = &CanvasRecordBindingRelation> {
226        self.bindings
227            .iter()
228            .filter(move |relation| &relation.source == record || &relation.target == record)
229    }
230
231    pub(crate) fn collect_related_records(
232        &self,
233        seeds: impl IntoIterator<Item = CanvasRecordId>,
234        mut can_include: impl FnMut(&CanvasRecordId) -> bool,
235    ) -> IndexSet<CanvasRecordId> {
236        let mut records = IndexSet::new();
237        let mut pending = Vec::new();
238
239        for record_id in seeds {
240            Self::insert_related_record(record_id, &mut records, &mut pending, &mut can_include);
241        }
242
243        while let Some(record_id) = pending.pop() {
244            for child in self.children_of(&record_id).cloned() {
245                Self::insert_related_record(child, &mut records, &mut pending, &mut can_include);
246            }
247            for member in self.members_of(&record_id).cloned() {
248                Self::insert_related_record(member, &mut records, &mut pending, &mut can_include);
249            }
250        }
251
252        records
253    }
254
255    pub(crate) fn collect_descendant_records(
256        &self,
257        seeds: impl IntoIterator<Item = CanvasRecordId>,
258        mut can_traverse: impl FnMut(&CanvasRecordId) -> bool,
259    ) -> IndexSet<CanvasRecordId> {
260        let mut records = IndexSet::new();
261        let mut pending = seeds
262            .into_iter()
263            .filter(|record_id| can_traverse(record_id))
264            .collect::<Vec<_>>();
265
266        while let Some(record_id) = pending.pop() {
267            for child in self.children_of(&record_id).cloned() {
268                Self::insert_related_record(child, &mut records, &mut pending, &mut can_traverse);
269            }
270            for member in self.members_of(&record_id).cloned() {
271                Self::insert_related_record(member, &mut records, &mut pending, &mut can_traverse);
272            }
273        }
274
275        records
276    }
277
278    pub fn contains_relation(&self, relation: &CanvasRecordRelation) -> bool {
279        match relation {
280            CanvasRecordRelation::Parent(relation) => {
281                self.parent_of(&relation.child) == Some(&relation.parent)
282            }
283            CanvasRecordRelation::Group(relation) => self.contains_group_relation(relation),
284            CanvasRecordRelation::Binding(relation) => self.binding(&relation.id) == Some(relation),
285        }
286    }
287
288    pub(crate) fn subset_for_records(
289        &self,
290        mut contains_record: impl FnMut(&CanvasRecordId) -> bool,
291    ) -> CanvasRecordRelations {
292        let mut relations = CanvasRecordRelationsBuilder::new();
293
294        for relation in self.parents() {
295            if contains_record(&relation.child) && contains_record(&relation.parent) {
296                relations.add_parent(relation.child.clone(), relation.parent.clone());
297            }
298        }
299
300        for relation in self.groups() {
301            if contains_record(&relation.group) && contains_record(&relation.member) {
302                relations.add_group_member(relation.group.clone(), relation.member.clone());
303            }
304        }
305
306        for relation in self.bindings() {
307            if contains_record(&relation.source) && contains_record(&relation.target) {
308                relations.add_binding(relation.clone());
309            }
310        }
311
312        relations.build()
313    }
314
315    pub(crate) fn set_parent(
316        &mut self,
317        child: CanvasRecordId,
318        parent: CanvasRecordId,
319    ) -> Option<CanvasRecordId> {
320        if let Some(relation) = self
321            .parents
322            .iter_mut()
323            .find(|relation| relation.child == child)
324        {
325            if relation.parent == parent {
326                return Some(parent);
327            }
328
329            let previous = std::mem::replace(&mut relation.parent, parent);
330            return Some(previous);
331        }
332
333        self.parents
334            .push(CanvasRecordParentRelation { child, parent });
335        None
336    }
337
338    pub(crate) fn clear_parent(&mut self, child: &CanvasRecordId) -> Option<CanvasRecordId> {
339        let index = self
340            .parents
341            .iter()
342            .position(|relation| &relation.child == child)?;
343        Some(self.parents.remove(index).parent)
344    }
345
346    pub(crate) fn add_to_group(&mut self, group: CanvasRecordId, member: CanvasRecordId) -> bool {
347        if self
348            .groups
349            .iter()
350            .any(|relation| relation.group == group && relation.member == member)
351        {
352            return false;
353        }
354
355        self.groups
356            .push(CanvasRecordGroupRelation { group, member });
357        true
358    }
359
360    pub(crate) fn remove_from_group(
361        &mut self,
362        group: &CanvasRecordId,
363        member: &CanvasRecordId,
364    ) -> bool {
365        let Some(index) = self
366            .groups
367            .iter()
368            .position(|relation| &relation.group == group && &relation.member == member)
369        else {
370            return false;
371        };
372        self.groups.remove(index);
373        true
374    }
375
376    pub(crate) fn set_binding(
377        &mut self,
378        binding: CanvasRecordBindingRelation,
379    ) -> Option<CanvasRecordBindingRelation> {
380        if let Some(relation) = self
381            .bindings
382            .iter_mut()
383            .find(|relation| relation.id == binding.id)
384        {
385            if *relation == binding {
386                return Some(binding);
387            }
388
389            let previous = std::mem::replace(relation, binding);
390            return Some(previous);
391        }
392
393        self.bindings.push(binding);
394        None
395    }
396
397    pub(crate) fn remove_binding(&mut self, id: &BindingId) -> Option<CanvasRecordBindingRelation> {
398        let index = self
399            .bindings
400            .iter()
401            .position(|relation| &relation.id == id)?;
402        Some(self.bindings.remove(index))
403    }
404
405    pub(crate) fn prune_missing_records(&mut self, existing: &IndexSet<CanvasRecordId>) -> bool {
406        let parent_count = self.parents.len();
407        self.parents.retain(|relation| {
408            existing.contains(&relation.child) && existing.contains(&relation.parent)
409        });
410
411        let group_count = self.groups.len();
412        self.groups.retain(|relation| {
413            existing.contains(&relation.group) && existing.contains(&relation.member)
414        });
415
416        let binding_count = self.bindings.len();
417        self.bindings.retain(|relation| {
418            existing.contains(&relation.source) && existing.contains(&relation.target)
419        });
420
421        self.parents.len() != parent_count
422            || self.groups.len() != group_count
423            || self.bindings.len() != binding_count
424    }
425
426    fn contains_group_relation(&self, relation: &CanvasRecordGroupRelation) -> bool {
427        self.groups
428            .iter()
429            .any(|existing| existing.group == relation.group && existing.member == relation.member)
430    }
431
432    fn insert_related_record<F>(
433        record_id: CanvasRecordId,
434        records: &mut IndexSet<CanvasRecordId>,
435        pending: &mut Vec<CanvasRecordId>,
436        can_include: &mut F,
437    ) where
438        F: FnMut(&CanvasRecordId) -> bool,
439    {
440        if !can_include(&record_id) {
441            return;
442        }
443        if records.insert(record_id.clone()) {
444            pending.push(record_id);
445        }
446    }
447}
448
449#[derive(Clone, Debug, Default, PartialEq)]
450pub struct CanvasRecordRelationsBuilder {
451    relations: CanvasRecordRelations,
452}
453
454impl CanvasRecordRelationsBuilder {
455    pub fn new() -> Self {
456        Self::default()
457    }
458
459    pub fn add_parent(
460        &mut self,
461        child: impl Into<CanvasRecordId>,
462        parent: impl Into<CanvasRecordId>,
463    ) -> &mut Self {
464        self.relations.set_parent(child.into(), parent.into());
465        self
466    }
467
468    pub fn add_group_member(
469        &mut self,
470        group: impl Into<CanvasRecordId>,
471        member: impl Into<CanvasRecordId>,
472    ) -> &mut Self {
473        self.relations.add_to_group(group.into(), member.into());
474        self
475    }
476
477    pub fn add_relation(&mut self, relation: impl Into<CanvasRecordRelation>) -> &mut Self {
478        match relation.into() {
479            CanvasRecordRelation::Parent(relation) => {
480                self.add_parent(relation.child, relation.parent);
481            }
482            CanvasRecordRelation::Group(relation) => {
483                self.add_group_member(relation.group, relation.member);
484            }
485            CanvasRecordRelation::Binding(relation) => {
486                self.add_binding(relation);
487            }
488        }
489        self
490    }
491
492    pub fn add_binding(&mut self, binding: CanvasRecordBindingRelation) -> &mut Self {
493        self.relations.set_binding(binding);
494        self
495    }
496
497    pub fn build(self) -> CanvasRecordRelations {
498        self.relations
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::{BindingId, NodeId, ShapeId};
506    use indexmap::IndexSet;
507
508    #[test]
509    fn relation_defaults_are_empty() {
510        let relations = CanvasRecordRelations::default();
511
512        assert!(relations.is_empty());
513        assert_eq!(relations.parents().count(), 0);
514        assert_eq!(relations.groups().count(), 0);
515        assert_eq!(relations.bindings().count(), 0);
516    }
517
518    #[test]
519    fn collect_related_records_expands_descendants_only() {
520        let mut relations = CanvasRecordRelations::default();
521        let frame = CanvasRecordId::Shape(ShapeId::from("frame"));
522        let group = CanvasRecordId::Shape(ShapeId::from("group"));
523        let leaf = CanvasRecordId::Node(NodeId::from("leaf"));
524        let outside = CanvasRecordId::Node(NodeId::from("outside"));
525
526        relations.set_parent(group.clone(), frame.clone());
527        relations.add_to_group(group.clone(), leaf.clone());
528        relations.add_to_group(frame.clone(), outside.clone());
529
530        let records = relations.collect_related_records([frame.clone()], |_| true);
531
532        assert_eq!(records.len(), 4);
533        assert!(records.contains(&frame));
534        assert!(records.contains(&group));
535        assert!(records.contains(&leaf));
536        assert!(records.contains(&outside));
537    }
538
539    #[test]
540    fn parent_and_child_lookup_use_record_ids() {
541        let mut relations = CanvasRecordRelations::default();
542        let child = CanvasRecordId::Node(NodeId::from("child"));
543        let parent = CanvasRecordId::Shape(ShapeId::from("frame"));
544
545        assert_eq!(relations.set_parent(child.clone(), parent.clone()), None);
546
547        assert_eq!(relations.parent_of(&child), Some(&parent));
548        assert_eq!(
549            relations.children_of(&parent).cloned().collect::<Vec<_>>(),
550            vec![child]
551        );
552    }
553
554    #[test]
555    fn group_memberships_are_deduplicated() {
556        let mut relations = CanvasRecordRelations::default();
557        let group = CanvasRecordId::Shape(ShapeId::from("group"));
558        let member = CanvasRecordId::Node(NodeId::from("member"));
559
560        assert!(relations.add_to_group(group.clone(), member.clone()));
561        assert!(!relations.add_to_group(group.clone(), member.clone()));
562
563        assert_eq!(
564            relations.members_of(&group).cloned().collect::<Vec<_>>(),
565            vec![member.clone()]
566        );
567        assert_eq!(
568            relations.groups_for(&member).cloned().collect::<Vec<_>>(),
569            vec![group]
570        );
571    }
572
573    #[test]
574    fn relation_records_report_kind_and_identity() {
575        let child = CanvasRecordId::Node(NodeId::from("child"));
576        let parent = CanvasRecordId::Shape(ShapeId::from("frame"));
577        let parent_relation = CanvasRecordRelation::from(CanvasRecordParentRelation::new(
578            child.clone(),
579            parent.clone(),
580        ));
581        let group_relation = CanvasRecordRelation::from(CanvasRecordGroupRelation::new(
582            parent.clone(),
583            child.clone(),
584        ));
585        let binding_relation = CanvasRecordRelation::from(CanvasRecordBindingRelation::new(
586            "binding",
587            child.clone(),
588            parent.clone(),
589        ));
590
591        assert_eq!(parent_relation.kind(), CanvasRecordRelationKind::Parent);
592        assert_eq!(
593            parent_relation.relation_key(),
594            CanvasRecordRelationKey::Parent {
595                child: &child,
596                parent: &parent,
597            }
598        );
599        assert_eq!(group_relation.kind(), CanvasRecordRelationKind::Group);
600        assert_eq!(
601            group_relation.relation_key(),
602            CanvasRecordRelationKey::Group {
603                group: &parent,
604                member: &child,
605            }
606        );
607        assert_eq!(binding_relation.kind(), CanvasRecordRelationKind::Binding);
608        assert_eq!(
609            binding_relation.relation_key(),
610            CanvasRecordRelationKey::Binding {
611                id: &BindingId::from("binding"),
612            }
613        );
614    }
615
616    #[test]
617    fn relations_builder_constructs_structural_relation_sets() {
618        let child = CanvasRecordId::Node(NodeId::from("child"));
619        let parent = CanvasRecordId::Shape(ShapeId::from("frame"));
620        let mut builder = CanvasRecordRelations::builder();
621        builder
622            .add_parent(child.clone(), parent.clone())
623            .add_group_member(parent.clone(), child.clone());
624
625        let relations = builder.build();
626
627        assert_eq!(relations.parent_of(&child), Some(&parent));
628        assert!(relations.contains_relation(&CanvasRecordRelation::Group(
629            CanvasRecordGroupRelation::new(parent, child)
630        )));
631    }
632
633    #[test]
634    fn subset_for_records_filters_relation_facts_by_scope() {
635        let child = CanvasRecordId::Node(NodeId::from("child"));
636        let group = CanvasRecordId::Shape(ShapeId::from("group"));
637        let outsider = CanvasRecordId::Node(NodeId::from("outsider"));
638        let binding = CanvasRecordBindingRelation::new("binding", child.clone(), group.clone());
639        let mut relations = CanvasRecordRelations::default();
640        relations.set_parent(child.clone(), group.clone());
641        relations.add_to_group(group.clone(), child.clone());
642        relations.add_to_group(group.clone(), outsider.clone());
643        relations.set_binding(binding.clone());
644
645        let scope: IndexSet<CanvasRecordId> = IndexSet::from_iter([child.clone(), group.clone()]);
646        let subset = relations.subset_for_records(|record_id| scope.contains(record_id));
647
648        assert_eq!(subset.parent_of(&child), Some(&group));
649        assert_eq!(
650            subset.members_of(&group).cloned().collect::<Vec<_>>(),
651            vec![child.clone()]
652        );
653        assert_eq!(subset.binding(&BindingId::from("binding")), Some(&binding));
654        assert!(!subset.contains_relation(&CanvasRecordRelation::Group(
655            CanvasRecordGroupRelation::new(group, outsider)
656        )));
657    }
658
659    #[test]
660    fn relations_builder_accepts_unified_relation_records() {
661        let child = CanvasRecordId::Node(NodeId::from("child"));
662        let parent = CanvasRecordId::Shape(ShapeId::from("frame"));
663        let mut builder = CanvasRecordRelationsBuilder::new();
664        builder.add_relation(CanvasRecordParentRelation::new(
665            child.clone(),
666            parent.clone(),
667        ));
668        builder.add_relation(CanvasRecordGroupRelation::new(
669            parent.clone(),
670            child.clone(),
671        ));
672        builder.add_relation(CanvasRecordBindingRelation::new(
673            "binding",
674            child.clone(),
675            parent.clone(),
676        ));
677
678        let relations = builder.build();
679
680        assert!(relations.contains_relation(&CanvasRecordRelation::Parent(
681            CanvasRecordParentRelation::new(child.clone(), parent.clone())
682        )));
683        assert!(relations.contains_relation(&CanvasRecordRelation::Group(
684            CanvasRecordGroupRelation::new(parent.clone(), child.clone())
685        )));
686        assert!(relations.contains_relation(&CanvasRecordRelation::Binding(
687            CanvasRecordBindingRelation::new("binding", child, parent)
688        )));
689    }
690
691    #[test]
692    fn binding_relations_are_keyed_by_binding_id() {
693        let source = CanvasRecordId::Node(NodeId::from("source"));
694        let target = CanvasRecordId::Shape(ShapeId::from("target"));
695        let replacement_target = CanvasRecordId::Shape(ShapeId::from("replacement-target"));
696        let mut relations = CanvasRecordRelations::default();
697
698        assert_eq!(
699            relations.set_binding(CanvasRecordBindingRelation::new(
700                "binding",
701                source.clone(),
702                target.clone(),
703            )),
704            None
705        );
706        assert_eq!(
707            relations.set_binding(CanvasRecordBindingRelation::new(
708                "binding",
709                source.clone(),
710                replacement_target.clone(),
711            )),
712            Some(CanvasRecordBindingRelation::new(
713                "binding",
714                source.clone(),
715                target
716            ))
717        );
718
719        assert_eq!(
720            relations.bindings_for(&source).cloned().collect::<Vec<_>>(),
721            vec![CanvasRecordBindingRelation::new(
722                "binding",
723                source,
724                replacement_target
725            )]
726        );
727        assert_eq!(
728            relations.remove_binding(&BindingId::from("binding")),
729            Some(CanvasRecordBindingRelation::new(
730                "binding",
731                CanvasRecordId::Node(NodeId::from("source")),
732                CanvasRecordId::Shape(ShapeId::from("replacement-target")),
733            ))
734        );
735        assert!(relations.is_empty());
736    }
737
738    #[test]
739    fn relation_equality_uses_semantic_ordering() {
740        let group_a = CanvasRecordId::Shape(ShapeId::from("group-a"));
741        let group_b = CanvasRecordId::Shape(ShapeId::from("group-b"));
742        let member_a = CanvasRecordId::Node(NodeId::from("member-a"));
743        let member_b = CanvasRecordId::Node(NodeId::from("member-b"));
744
745        let mut left = CanvasRecordRelations::default();
746        left.set_parent(member_a.clone(), group_a.clone());
747        left.set_parent(member_b.clone(), group_b.clone());
748        left.add_to_group(group_a.clone(), member_a.clone());
749        left.add_to_group(group_b.clone(), member_b.clone());
750        left.set_binding(CanvasRecordBindingRelation::new(
751            "binding-a",
752            member_a.clone(),
753            group_a.clone(),
754        ));
755        left.set_binding(CanvasRecordBindingRelation::new(
756            "binding-b",
757            member_b.clone(),
758            group_b.clone(),
759        ));
760
761        let mut right = CanvasRecordRelations::default();
762        right.set_parent(member_b.clone(), group_b.clone());
763        right.set_parent(member_a.clone(), group_a.clone());
764        right.add_to_group(group_b.clone(), member_b.clone());
765        right.add_to_group(group_a.clone(), member_a.clone());
766        right.set_binding(CanvasRecordBindingRelation::new(
767            "binding-b",
768            member_b,
769            group_b,
770        ));
771        right.set_binding(CanvasRecordBindingRelation::new(
772            "binding-a",
773            member_a,
774            group_a,
775        ));
776
777        assert_eq!(left, right);
778    }
779
780    #[test]
781    fn prune_missing_records_removes_dangling_relations() {
782        let mut relations = CanvasRecordRelations::default();
783        let group = CanvasRecordId::Shape(ShapeId::from("group"));
784        let member = CanvasRecordId::Node(NodeId::from("member"));
785        let missing = CanvasRecordId::Node(NodeId::from("missing"));
786        relations.set_parent(member.clone(), group.clone());
787        relations.add_to_group(group.clone(), member.clone());
788        relations.add_to_group(group.clone(), missing.clone());
789        relations.set_binding(CanvasRecordBindingRelation::new(
790            "binding",
791            member.clone(),
792            missing,
793        ));
794
795        let existing = IndexSet::from_iter([group.clone(), member.clone()]);
796
797        assert!(relations.prune_missing_records(&existing));
798        assert_eq!(relations.parent_of(&member), Some(&group));
799        assert_eq!(
800            relations.members_of(&group).cloned().collect::<Vec<_>>(),
801            vec![member]
802        );
803        assert!(relations.bindings().next().is_none());
804    }
805}