Skip to main content

zerodds_dlrl/
relationship.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Relationship resolver — DDS 1.4 §B.5.
5//!
6//! Fully implemented.
7
8use alloc::collections::BTreeMap;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use crate::object_cache::ObjectId;
13
14/// Relationship kind. Spec §B.5.1.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum RelationshipKind {
17    /// `Reference` — referential binding (the source can be created
18    /// freely).
19    Reference,
20    /// `Composition` — compositional binding (lifecycle coupled).
21    Composition,
22}
23
24/// Direction. Spec §B.5.2.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum Direction {
27    /// `Mono` — one-directional.
28    Mono,
29    /// `Bi` — bidirectional.
30    Bi,
31}
32
33/// Cascade mode on update/delete. Spec §B.5.4.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum CascadeMode {
36    /// `None` — no cascade.
37    None,
38    /// `Update` — updates cascade.
39    Update,
40    /// `Delete` — deletes cascade.
41    Delete,
42    /// `All` — all cascades.
43    All,
44}
45
46/// A concrete relationship definition.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Relationship {
49    /// Freely chosen name (e.g. `"orders"`).
50    pub name: String,
51    /// Source.
52    pub source: ObjectId,
53    /// Target.
54    pub target: ObjectId,
55    /// Kind.
56    pub kind: RelationshipKind,
57    /// Direction.
58    pub direction: Direction,
59    /// Cascade mode.
60    pub cascade: CascadeMode,
61}
62
63/// Resolver — index over relationships per source object.
64#[derive(Debug, Default, Clone, PartialEq, Eq)]
65pub struct RelationshipResolver {
66    by_source: BTreeMap<ObjectId, Vec<Relationship>>,
67}
68
69impl RelationshipResolver {
70    /// Constructor.
71    #[must_use]
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Add a relationship.
77    pub fn add(&mut self, rel: Relationship) {
78        let source = rel.source.clone();
79        self.by_source.entry(source).or_default().push(rel.clone());
80        if rel.direction == Direction::Bi {
81            let mut reverse = rel;
82            core::mem::swap(&mut reverse.source, &mut reverse.target);
83            self.by_source
84                .entry(reverse.source.clone())
85                .or_default()
86                .push(reverse);
87        }
88    }
89
90    /// List of relationships originating from a source.
91    #[must_use]
92    pub fn outgoing(&self, source: &ObjectId) -> Vec<&Relationship> {
93        self.by_source
94            .get(source)
95            .map_or_else(Vec::new, |v| v.iter().collect())
96    }
97
98    /// Number of stored relationships (including inverse ones for `Bi`).
99    #[must_use]
100    pub fn len(&self) -> usize {
101        self.by_source.values().map(Vec::len).sum()
102    }
103
104    /// `true` if there are no relationships.
105    #[must_use]
106    pub fn is_empty(&self) -> bool {
107        self.by_source.is_empty()
108    }
109
110    /// Spec §B.5.4 — cascading targets for a source ID when the source
111    /// object is deleted.
112    #[must_use]
113    pub fn cascade_targets_for_delete(&self, source: &ObjectId) -> Vec<ObjectId> {
114        self.by_source.get(source).map_or_else(Vec::new, |rels| {
115            rels.iter()
116                .filter(|r| matches!(r.cascade, CascadeMode::Delete | CascadeMode::All))
117                .map(|r| r.target.clone())
118                .collect()
119        })
120    }
121
122    /// Spec §B.5.4 — cascading targets for an update.
123    #[must_use]
124    pub fn cascade_targets_for_update(&self, source: &ObjectId) -> Vec<ObjectId> {
125        self.by_source.get(source).map_or_else(Vec::new, |rels| {
126            rels.iter()
127                .filter(|r| matches!(r.cascade, CascadeMode::Update | CascadeMode::All))
128                .map(|r| r.target.clone())
129                .collect()
130        })
131    }
132}
133
134#[cfg(test)]
135#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
136mod tests {
137    use super::*;
138
139    fn oid(t: &str, k: &[u8]) -> ObjectId {
140        ObjectId::new(t.into(), k.to_vec())
141    }
142
143    fn rel(name: &str, src: ObjectId, tgt: ObjectId) -> Relationship {
144        Relationship {
145            name: name.into(),
146            source: src,
147            target: tgt,
148            kind: RelationshipKind::Reference,
149            direction: Direction::Mono,
150            cascade: CascadeMode::None,
151        }
152    }
153
154    #[test]
155    fn mono_adds_one_entry() {
156        let mut r = RelationshipResolver::new();
157        r.add(rel("a", oid("T", b"1"), oid("T", b"2")));
158        assert_eq!(r.len(), 1);
159        assert_eq!(r.outgoing(&oid("T", b"1")).len(), 1);
160        assert_eq!(r.outgoing(&oid("T", b"2")).len(), 0);
161    }
162
163    #[test]
164    fn bi_adds_inverse() {
165        let mut r = RelationshipResolver::new();
166        let mut x = rel("a", oid("T", b"1"), oid("T", b"2"));
167        x.direction = Direction::Bi;
168        r.add(x);
169        assert_eq!(r.len(), 2);
170        assert_eq!(r.outgoing(&oid("T", b"1")).len(), 1);
171        assert_eq!(r.outgoing(&oid("T", b"2")).len(), 1);
172    }
173
174    #[test]
175    fn cascade_delete_targets_only_marked_relations() {
176        let mut r = RelationshipResolver::new();
177        let mut a = rel("a", oid("T", b"1"), oid("T", b"2"));
178        a.cascade = CascadeMode::Delete;
179        let b = rel("b", oid("T", b"1"), oid("T", b"3"));
180        r.add(a);
181        r.add(b);
182        let targets = r.cascade_targets_for_delete(&oid("T", b"1"));
183        assert_eq!(targets, alloc::vec![oid("T", b"2")]);
184    }
185
186    #[test]
187    fn cascade_update_targets_only_marked_relations() {
188        let mut r = RelationshipResolver::new();
189        let mut a = rel("a", oid("T", b"1"), oid("T", b"2"));
190        a.cascade = CascadeMode::Update;
191        r.add(a);
192        let mut b = rel("b", oid("T", b"1"), oid("T", b"3"));
193        b.cascade = CascadeMode::All;
194        r.add(b);
195        let targets = r.cascade_targets_for_update(&oid("T", b"1"));
196        assert_eq!(targets.len(), 2);
197    }
198
199    #[test]
200    fn relationship_kind_distinct() {
201        assert_ne!(RelationshipKind::Reference, RelationshipKind::Composition);
202    }
203
204    #[test]
205    fn cascade_modes_distinct() {
206        assert_ne!(CascadeMode::None, CascadeMode::All);
207        assert_ne!(CascadeMode::Update, CascadeMode::Delete);
208    }
209
210    #[test]
211    fn empty_resolver_returns_empty_lists() {
212        let r = RelationshipResolver::new();
213        assert!(r.is_empty());
214        assert!(r.cascade_targets_for_delete(&oid("T", b"x")).is_empty());
215        assert!(r.cascade_targets_for_update(&oid("T", b"x")).is_empty());
216    }
217}