Skip to main content

zerodds_xrce/
object_store.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `ObjectStore` — pro-Client Object-Lifecycle (Spec §7.5).
5//!
6//! Pro `ProxyClient` haelt der Agent eine `BTreeMap<ObjectId, ObjectInstance>`,
7//! die alle aktuell im Client erzeugten Objekte (Participant, Topic,
8//! Publisher, Subscriber, DataWriter, DataReader, ...) enthaelt.
9//!
10//! ## CREATE-Semantik (Spec §7.5.1)
11//!
12//! Beim Empfang einer `CREATE`-Submessage entscheidet die Kombination
13//! der `creation_mode`-Flags `reuse` und `replace` ueber das Verhalten
14//! bei einer bereits existierenden ID:
15//!
16//! | reuse | replace | bei existierendem Objekt          |
17//! |-------|---------|-----------------------------------|
18//! | false | false   | Fehler (`STATUS_ERR_ALREADY_EXISTS`)|
19//! | true  | false   | Wiederverwenden (`STATUS_OK_MATCHED`)|
20//! | false | true    | Loeschen+Neu (`STATUS_OK`)         |
21//! | true  | true    | Wiederverwenden (Variante 2 dominiert)|
22//!
23//! `STATUS_*` werden von der Agent-Aufrufseite zurueckgemeldet — das
24//! Store liefert eine `CreateOutcome`, aus der der Caller den Status
25//! ableiten kann.
26
27extern crate alloc;
28use alloc::collections::BTreeMap;
29
30use crate::error::XrceError;
31use crate::object_id::ObjectId;
32use crate::object_kind::ObjectKind;
33use crate::object_repr::ObjectVariant;
34
35/// Creation-Mode-Flags (Spec §7.5.1, codiert als Submessage-Flags Bits 1+2).
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
37pub struct CreationMode {
38    /// Existierendes Objekt wiederverwenden.
39    pub reuse: bool,
40    /// Existierendes Objekt loeschen und neu anlegen.
41    pub replace: bool,
42}
43
44impl CreationMode {
45    /// Strict: weder reuse noch replace.
46    pub const STRICT: Self = Self {
47        reuse: false,
48        replace: false,
49    };
50    /// Reuse: bei Existenz wiederverwenden, sonst neu.
51    pub const REUSE: Self = Self {
52        reuse: true,
53        replace: false,
54    };
55    /// Replace: bei Existenz loeschen+neu.
56    pub const REPLACE: Self = Self {
57        reuse: false,
58        replace: true,
59    };
60}
61
62/// Eine im Store registrierte Object-Instance.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct ObjectInstance {
65    /// Object-Kind (4-bit, gespiegelt aus der ObjectId).
66    pub kind: ObjectKind,
67    /// Aktuelle Wire-Repraesentation der Object-Daten.
68    pub variant: ObjectVariant,
69    /// Versionszaehler — wird bei Replace inkrementiert.
70    pub version: u32,
71}
72
73impl ObjectInstance {
74    /// Konstruktor.
75    #[must_use]
76    pub fn new(kind: ObjectKind, variant: ObjectVariant) -> Self {
77        Self {
78            kind,
79            variant,
80            version: 0,
81        }
82    }
83}
84
85/// Resultat einer `create`-Operation.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum CreateOutcome {
88    /// Object wurde frisch angelegt.
89    Created,
90    /// Objekt existierte bereits, mit `reuse=true` wiederverwendet
91    /// (Inhalt ist gleich → `STATUS_OK_MATCHED`, oder Inhalt unterscheidet
92    /// sich → `STATUS_ERR_MISMATCH` — die Unterscheidung uebernimmt der Caller).
93    Reused {
94        /// `true`, falls der Inhalt mit dem bestehenden uebereinstimmt.
95        equal: bool,
96    },
97    /// Objekt existierte bereits, mit `replace=true` ersetzt.
98    Replaced,
99    /// Objekt existierte bereits, kein reuse/replace → Konflikt.
100    Conflict,
101}
102
103/// Pro-Client Object-Store.
104#[derive(Debug, Clone, Default)]
105pub struct ObjectStore {
106    objects: BTreeMap<ObjectId, ObjectInstance>,
107}
108
109impl ObjectStore {
110    /// Leerer Store.
111    #[must_use]
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Anzahl gehaltener Objekte.
117    #[must_use]
118    pub fn len(&self) -> usize {
119        self.objects.len()
120    }
121
122    /// Leer?
123    #[must_use]
124    pub fn is_empty(&self) -> bool {
125        self.objects.is_empty()
126    }
127
128    /// Existiert dieses Object?
129    #[must_use]
130    pub fn contains(&self, id: ObjectId) -> bool {
131        self.objects.contains_key(&id)
132    }
133
134    /// Lookup.
135    #[must_use]
136    pub fn get(&self, id: ObjectId) -> Option<&ObjectInstance> {
137        self.objects.get(&id)
138    }
139
140    /// Iteriert alle Objekte sortiert nach `ObjectId`.
141    pub fn iter(&self) -> impl Iterator<Item = (ObjectId, &ObjectInstance)> {
142        self.objects.iter().map(|(&id, inst)| (id, inst))
143    }
144
145    /// CREATE-Operation.
146    ///
147    /// Validiert dass der `kind` der `ObjectId` mit dem Argument
148    /// uebereinstimmt (Spec §7.2.1: Lower-4-Bits-Kind muss zur Variante
149    /// passen).
150    ///
151    /// # Errors
152    /// - `ValueOutOfRange`, wenn `id.kind() != kind` oder `id` invalid ist.
153    pub fn create(
154        &mut self,
155        id: ObjectId,
156        kind: ObjectKind,
157        variant: ObjectVariant,
158        mode: CreationMode,
159    ) -> Result<CreateOutcome, XrceError> {
160        if id.is_invalid() {
161            return Err(XrceError::ValueOutOfRange {
162                message: "cannot create OBJECTID_INVALID",
163            });
164        }
165        let id_kind = id.kind()?;
166        if id_kind != kind {
167            return Err(XrceError::ValueOutOfRange {
168                message: "ObjectId.kind() does not match argument kind",
169            });
170        }
171
172        let existing = self.objects.get(&id);
173        match (existing, mode.reuse, mode.replace) {
174            (None, _, _) => {
175                self.objects.insert(id, ObjectInstance::new(kind, variant));
176                Ok(CreateOutcome::Created)
177            }
178            (Some(prev), true, _) => {
179                let equal = prev.kind == kind && prev.variant == variant;
180                Ok(CreateOutcome::Reused { equal })
181            }
182            (Some(prev), false, true) => {
183                let next_version = prev.version.wrapping_add(1);
184                self.objects.insert(
185                    id,
186                    ObjectInstance {
187                        kind,
188                        variant,
189                        version: next_version,
190                    },
191                );
192                Ok(CreateOutcome::Replaced)
193            }
194            (Some(_), false, false) => Ok(CreateOutcome::Conflict),
195        }
196    }
197
198    /// DELETE-Operation. Liefert `true`, wenn die ID existiert hat.
199    pub fn delete(&mut self, id: ObjectId) -> bool {
200        self.objects.remove(&id).is_some()
201    }
202
203    /// Loescht alle Objekte (z.B. wenn der ProxyClient terminated).
204    pub fn clear(&mut self) {
205        self.objects.clear();
206    }
207
208    /// Filtert nach Kind.
209    pub fn iter_by_kind(
210        &self,
211        kind: ObjectKind,
212    ) -> impl Iterator<Item = (ObjectId, &ObjectInstance)> {
213        self.iter().filter(move |(_, inst)| inst.kind == kind)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    #![allow(clippy::expect_used, clippy::unwrap_used)]
220    use super::*;
221    use alloc::string::String;
222
223    fn participant_xml(name: &str) -> ObjectVariant {
224        ObjectVariant::ByXmlString(alloc::format!(
225            "<dds><domain_participant><name>{name}</name></domain_participant></dds>"
226        ))
227    }
228
229    #[test]
230    fn create_inserts_new_object() {
231        let mut store = ObjectStore::new();
232        let id = ObjectId::new(0x010, ObjectKind::Participant).unwrap();
233        let r = store
234            .create(
235                id,
236                ObjectKind::Participant,
237                participant_xml("p1"),
238                CreationMode::STRICT,
239            )
240            .unwrap();
241        assert_eq!(r, CreateOutcome::Created);
242        assert!(store.contains(id));
243        assert_eq!(store.len(), 1);
244    }
245
246    #[test]
247    fn create_strict_on_existing_returns_conflict() {
248        let mut store = ObjectStore::new();
249        let id = ObjectId::new(0x010, ObjectKind::Topic).unwrap();
250        store
251            .create(
252                id,
253                ObjectKind::Topic,
254                ObjectVariant::ByReference("foo".into()),
255                CreationMode::STRICT,
256            )
257            .unwrap();
258        let r = store
259            .create(
260                id,
261                ObjectKind::Topic,
262                ObjectVariant::ByReference("bar".into()),
263                CreationMode::STRICT,
264            )
265            .unwrap();
266        assert_eq!(r, CreateOutcome::Conflict);
267    }
268
269    #[test]
270    fn create_reuse_returns_equal_marker() {
271        let mut store = ObjectStore::new();
272        let id = ObjectId::new(0x011, ObjectKind::Topic).unwrap();
273        let first = ObjectVariant::ByReference("the_topic".into());
274        store
275            .create(id, ObjectKind::Topic, first.clone(), CreationMode::STRICT)
276            .unwrap();
277        // 1) Reuse mit gleichem Inhalt → equal=true
278        let r1 = store
279            .create(id, ObjectKind::Topic, first.clone(), CreationMode::REUSE)
280            .unwrap();
281        assert_eq!(r1, CreateOutcome::Reused { equal: true });
282        // 2) Reuse mit anderem Inhalt → equal=false
283        let r2 = store
284            .create(
285                id,
286                ObjectKind::Topic,
287                ObjectVariant::ByReference("other".into()),
288                CreationMode::REUSE,
289            )
290            .unwrap();
291        assert_eq!(r2, CreateOutcome::Reused { equal: false });
292        // Inhalt wurde NICHT ersetzt
293        assert_eq!(store.get(id).unwrap().variant, first);
294        assert_eq!(store.get(id).unwrap().version, 0);
295    }
296
297    #[test]
298    fn create_replace_increments_version() {
299        let mut store = ObjectStore::new();
300        let id = ObjectId::new(0x012, ObjectKind::DataWriter).unwrap();
301        store
302            .create(
303                id,
304                ObjectKind::DataWriter,
305                ObjectVariant::ByReference("wr1".into()),
306                CreationMode::STRICT,
307            )
308            .unwrap();
309        let r = store
310            .create(
311                id,
312                ObjectKind::DataWriter,
313                ObjectVariant::ByReference("wr2".into()),
314                CreationMode::REPLACE,
315            )
316            .unwrap();
317        assert_eq!(r, CreateOutcome::Replaced);
318        let inst = store.get(id).unwrap();
319        assert_eq!(inst.version, 1);
320        assert_eq!(inst.variant, ObjectVariant::ByReference("wr2".into()));
321    }
322
323    #[test]
324    fn delete_removes_object() {
325        let mut store = ObjectStore::new();
326        let id = ObjectId::new(0x020, ObjectKind::Subscriber).unwrap();
327        store
328            .create(
329                id,
330                ObjectKind::Subscriber,
331                ObjectVariant::ByReference("s".into()),
332                CreationMode::STRICT,
333            )
334            .unwrap();
335        assert!(store.delete(id));
336        assert!(!store.contains(id));
337        assert!(!store.delete(id));
338    }
339
340    #[test]
341    fn create_kind_mismatch_with_id_rejected() {
342        let mut store = ObjectStore::new();
343        // ObjectId mit Kind=Topic, aber wir geben DataWriter
344        let id = ObjectId::new(0x030, ObjectKind::Topic).unwrap();
345        let r = store.create(
346            id,
347            ObjectKind::DataWriter,
348            ObjectVariant::ByReference("x".into()),
349            CreationMode::STRICT,
350        );
351        assert!(r.is_err());
352    }
353
354    #[test]
355    fn create_invalid_object_id_rejected() {
356        let mut store = ObjectStore::new();
357        let r = store.create(
358            crate::object_id::OBJECTID_INVALID,
359            ObjectKind::Topic,
360            ObjectVariant::ByReference("x".into()),
361            CreationMode::STRICT,
362        );
363        assert!(r.is_err());
364    }
365
366    #[test]
367    fn iter_by_kind_filters() {
368        let mut store = ObjectStore::new();
369        let topic = ObjectId::new(0x100, ObjectKind::Topic).unwrap();
370        let dw = ObjectId::new(0x101, ObjectKind::DataWriter).unwrap();
371        let dr = ObjectId::new(0x102, ObjectKind::DataReader).unwrap();
372        store
373            .create(
374                topic,
375                ObjectKind::Topic,
376                ObjectVariant::ByReference("t".into()),
377                CreationMode::STRICT,
378            )
379            .unwrap();
380        store
381            .create(
382                dw,
383                ObjectKind::DataWriter,
384                ObjectVariant::ByReference("w".into()),
385                CreationMode::STRICT,
386            )
387            .unwrap();
388        store
389            .create(
390                dr,
391                ObjectKind::DataReader,
392                ObjectVariant::ByReference("r".into()),
393                CreationMode::STRICT,
394            )
395            .unwrap();
396        let topics: alloc::vec::Vec<_> = store.iter_by_kind(ObjectKind::Topic).collect();
397        assert_eq!(topics.len(), 1);
398        assert_eq!(topics[0].0, topic);
399    }
400
401    #[test]
402    fn clear_drops_everything() {
403        let mut store = ObjectStore::new();
404        for i in 0..10 {
405            let id = ObjectId::new(i, ObjectKind::Topic).unwrap();
406            store
407                .create(
408                    id,
409                    ObjectKind::Topic,
410                    ObjectVariant::ByReference(String::from("x")),
411                    CreationMode::STRICT,
412                )
413                .unwrap();
414        }
415        assert_eq!(store.len(), 10);
416        store.clear();
417        assert!(store.is_empty());
418    }
419
420    #[test]
421    fn replace_then_reuse_is_consistent() {
422        let mut store = ObjectStore::new();
423        let id = ObjectId::new(0x040, ObjectKind::Topic).unwrap();
424        let v1 = ObjectVariant::ByReference("A".into());
425        let v2 = ObjectVariant::ByReference("B".into());
426        store
427            .create(id, ObjectKind::Topic, v1.clone(), CreationMode::STRICT)
428            .unwrap();
429        store
430            .create(id, ObjectKind::Topic, v2.clone(), CreationMode::REPLACE)
431            .unwrap();
432        let r = store
433            .create(id, ObjectKind::Topic, v2.clone(), CreationMode::REUSE)
434            .unwrap();
435        assert_eq!(r, CreateOutcome::Reused { equal: true });
436        assert_eq!(store.get(id).unwrap().version, 1);
437    }
438
439    #[test]
440    fn iter_yields_sorted_ids() {
441        let mut store = ObjectStore::new();
442        for raw in [0x300u16, 0x100, 0x200] {
443            let id = ObjectId::new(raw, ObjectKind::Topic).unwrap();
444            store
445                .create(
446                    id,
447                    ObjectKind::Topic,
448                    ObjectVariant::ByReference(String::from("x")),
449                    CreationMode::STRICT,
450                )
451                .unwrap();
452        }
453        let ids: alloc::vec::Vec<ObjectId> = store.iter().map(|(id, _)| id).collect();
454        // BTreeMap sortiert nach ObjectId-raw
455        assert!(ids.windows(2).all(|w| w[0] <= w[1]));
456    }
457}