Skip to main content

zerodds_dlrl/
metamodel.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! DLRL 1.2 metamodel + mapping + entity hierarchy + exception types.
5//!
6//! Spec source: `dds-1.2.pdf` §8 DLRL. We provide the UML metamodel
7//! class hierarchy + mapping information + the 17 entity classes
8//! + 8 exception types + default mapping convention as a stub layer.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13// ===========================================================================
14// §8.1.3.2.1 ObjectRoot — inheritance base
15// ===========================================================================
16
17/// Spec §8.1.3.2.1 — `ObjectRoot` as the common abstract base for all
18/// DLRL objects.
19pub trait ObjectRoot: Send + Sync {
20    /// Spec §8.1.3.1 — object identity (oid).
21    fn oid(&self) -> u64;
22    /// Repository ID of the object type.
23    fn repository_id(&self) -> &str;
24    /// Spec §8.1.3.1 — `is_modified()`.
25    fn is_modified(&self) -> bool;
26    /// Spec §8.1.3.1 — `is_deleted()`.
27    fn is_deleted(&self) -> bool;
28}
29
30// ===========================================================================
31// §8.1.3.3 + §8.1.4.4 Metamodel with mapping information
32// ===========================================================================
33
34/// Spec §8.1.3.3 — `Class` metamodel element.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ClassMetamodel {
37    /// Class name.
38    pub name: String,
39    /// Repository ID.
40    pub repository_id: String,
41    /// Spec §8.1.4.4 — DCPS topic mapping (default = `<Class>`).
42    pub main_topic: String,
43    /// Spec §8.1.4.4 — field name for oid (default = `oid`).
44    pub oid_field: String,
45    /// Spec §8.1.4.4 — field name for class (default = `class`).
46    pub class_field: String,
47    /// Spec §8.1.4.4 — `full_oid_required` flag.
48    pub full_oid_required: bool,
49    /// Spec §8.1.4.4 — `final` flag (no subclass).
50    pub final_class: bool,
51}
52
53/// Spec §8.1.3.3 — `MultiAttribute` element (sequence/list).
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct MultiAttributeMetamodel {
56    /// Attribute name.
57    pub name: String,
58    /// Topic mapping (default = `<Class>.<attribute>`).
59    pub topic: String,
60    /// Spec §8.1.4.4 — `target_field` name.
61    pub target_field: String,
62    /// Spec §8.1.4.4 — `index_field` for multi-attribute.
63    pub index_field: String,
64    /// Spec §8.1.4.4 — `key_fields` (list).
65    pub key_fields: Vec<String>,
66}
67
68/// Spec §8.1.3.3 — `MonoRelation` element (1:1 relationship).
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct MonoRelationMetamodel {
71    /// Relation name.
72    pub name: String,
73    /// Topic mapping.
74    pub topic: String,
75    /// Target fields (list).
76    pub target_fields: Vec<String>,
77    /// Key fields (list).
78    pub key_fields: Vec<String>,
79    /// `full_oid_required` flag.
80    pub full_oid_required: bool,
81    /// `is_composition` flag.
82    pub is_composition: bool,
83}
84
85/// Spec §8.1.4.3 — default mapping convention.
86pub mod default_mapping {
87    /// Default topic name = `<Class>`.
88    #[must_use]
89    pub fn topic_name(class: &str) -> alloc::string::String {
90        class.into()
91    }
92
93    /// Default oid field name.
94    pub const DEFAULT_OID_FIELD: &str = "oid";
95
96    /// Default class field name.
97    pub const DEFAULT_CLASS_FIELD: &str = "class";
98
99    /// Default index field name for multi-attribute.
100    pub const DEFAULT_INDEX_FIELD: &str = "index";
101
102    /// Default multi-topic form = `<Class>.<attribute>`.
103    #[must_use]
104    pub fn multi_topic(class: &str, attribute: &str) -> alloc::string::String {
105        alloc::format!("{class}.{attribute}")
106    }
107}
108
109// ===========================================================================
110// §8.1.6.2 — 17 DLRL entity classes
111// ===========================================================================
112
113/// Spec §8.1.6.2 — DLRL entity class list.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum DlrlEntityKind {
116    /// `CacheFactory`.
117    CacheFactory,
118    /// `CacheBase`.
119    CacheBase,
120    /// `Cache`.
121    Cache,
122    /// `CacheAccess`.
123    CacheAccess,
124    /// `Contract`.
125    Contract,
126    /// `Selection`.
127    Selection,
128    /// `SelectionCriterion`.
129    SelectionCriterion,
130    /// `FilterCriterion`.
131    FilterCriterion,
132    /// `QueryCriterion`.
133    QueryCriterion,
134    /// `SelectionListener`.
135    SelectionListener,
136    /// `ObjectRoot`.
137    ObjectRoot,
138    /// `ObjectHome`.
139    ObjectHome,
140    /// `Collection`.
141    Collection,
142    /// `List`.
143    List,
144    /// `Set`.
145    Set,
146    /// `StrMap`.
147    StrMap,
148    /// `IntMap`.
149    IntMap,
150}
151
152impl DlrlEntityKind {
153    /// Spec §8.1.6.2 — spec class name.
154    #[must_use]
155    pub const fn spec_name(self) -> &'static str {
156        match self {
157            Self::CacheFactory => "CacheFactory",
158            Self::CacheBase => "CacheBase",
159            Self::Cache => "Cache",
160            Self::CacheAccess => "CacheAccess",
161            Self::Contract => "Contract",
162            Self::Selection => "Selection",
163            Self::SelectionCriterion => "SelectionCriterion",
164            Self::FilterCriterion => "FilterCriterion",
165            Self::QueryCriterion => "QueryCriterion",
166            Self::SelectionListener => "SelectionListener",
167            Self::ObjectRoot => "ObjectRoot",
168            Self::ObjectHome => "ObjectHome",
169            Self::Collection => "Collection",
170            Self::List => "List",
171            Self::Set => "Set",
172            Self::StrMap => "StrMap",
173            Self::IntMap => "IntMap",
174        }
175    }
176
177    /// List of all 17 entity kinds.
178    #[must_use]
179    pub fn all() -> [Self; 17] {
180        [
181            Self::CacheFactory,
182            Self::CacheBase,
183            Self::Cache,
184            Self::CacheAccess,
185            Self::Contract,
186            Self::Selection,
187            Self::SelectionCriterion,
188            Self::FilterCriterion,
189            Self::QueryCriterion,
190            Self::SelectionListener,
191            Self::ObjectRoot,
192            Self::ObjectHome,
193            Self::Collection,
194            Self::List,
195            Self::Set,
196            Self::StrMap,
197            Self::IntMap,
198        ]
199    }
200}
201
202// ===========================================================================
203// §8.1.6.2 — 8 exception types
204// ===========================================================================
205
206/// Spec §8.1.6.2 — DLRL exception hierarchy.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum DlrlException {
209    /// `DCPSError` — generic error from the DCPS layer.
210    DcpsError {
211        /// Reason string.
212        reason: String,
213    },
214    /// `BadHomeDefinition` — class/home mismatch.
215    BadHomeDefinition {
216        /// Reason string.
217        reason: String,
218    },
219    /// `NotFound` — object/selection not in the cache.
220    NotFound,
221    /// `AlreadyExisting` — an object with that key already exists.
222    AlreadyExisting,
223    /// `AlreadyDeleted` — operation on an already-deleted object.
224    AlreadyDeleted,
225    /// `PreconditionNotMet` — constraint violation (e.g. the
226    /// is_modified tracking requirement).
227    PreconditionNotMet {
228        /// Constraint description.
229        constraint: String,
230    },
231    /// `NoSuchElement` — iterator/collection out of range.
232    NoSuchElement,
233    /// `SQLError` — Annex B query-filter parse error.
234    SqlError {
235        /// Reason string.
236        reason: String,
237    },
238}
239
240impl DlrlException {
241    /// Spec exception ID (repository-ID form).
242    #[must_use]
243    pub const fn repository_id(&self) -> &'static str {
244        match self {
245            Self::DcpsError { .. } => "IDL:omg.org/DLRL/DCPSError:1.0",
246            Self::BadHomeDefinition { .. } => "IDL:omg.org/DLRL/BadHomeDefinition:1.0",
247            Self::NotFound => "IDL:omg.org/DLRL/NotFound:1.0",
248            Self::AlreadyExisting => "IDL:omg.org/DLRL/AlreadyExisting:1.0",
249            Self::AlreadyDeleted => "IDL:omg.org/DLRL/AlreadyDeleted:1.0",
250            Self::PreconditionNotMet { .. } => "IDL:omg.org/DLRL/PreconditionNotMet:1.0",
251            Self::NoSuchElement => "IDL:omg.org/DLRL/NoSuchElement:1.0",
252            Self::SqlError { .. } => "IDL:omg.org/DLRL/SQLError:1.0",
253        }
254    }
255}
256
257// ===========================================================================
258// §8.1.5 Cache-DCPS lifecycle (attachment + creation + QoS)
259// ===========================================================================
260
261/// Spec §8.1.5.1 — cache attachment state.
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum CacheAttachmentState {
264    /// Cache not bound to a DCPS pub/sub.
265    Detached,
266    /// Cache bound to a subscriber (read-only).
267    AttachedSubscriber,
268    /// Cache bound to a publisher (write-only).
269    AttachedPublisher,
270    /// Cache bound to both (read-write).
271    AttachedBoth,
272}
273
274/// Spec §8.1.5 — cache mode (Spec §8.1.6.1.2).
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276pub enum CacheMode {
277    /// `Transparent` — cache loads automatically, no caller pull.
278    Transparent,
279    /// `OnDemand` — caller loads explicitly via `refresh`.
280    OnDemand,
281}
282
283// ===========================================================================
284// Annex B — query/filter with SQL-92 subset
285// ===========================================================================
286
287/// Annex B — `QueryCriterion` expression.
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct QueryExpression {
290    /// SQL-92 subset expression (e.g. `"price > 100 AND symbol = 'AAPL'"`).
291    pub expr: String,
292    /// Bind parameters.
293    pub params: Vec<String>,
294}
295
296impl QueryExpression {
297    /// Constructor.
298    #[must_use]
299    pub fn new(expr: impl Into<String>) -> Self {
300        Self {
301            expr: expr.into(),
302            params: Vec::new(),
303        }
304    }
305
306    /// Adds a bind parameter.
307    pub fn add_param(&mut self, p: impl Into<String>) {
308        self.params.push(p.into());
309    }
310}
311
312#[cfg(test)]
313#[allow(clippy::expect_used)]
314mod tests {
315    use super::*;
316    use alloc::string::ToString;
317
318    // §8.1.3.2.1 ObjectRoot
319    struct DummyObject;
320    impl ObjectRoot for DummyObject {
321        fn oid(&self) -> u64 {
322            42
323        }
324        fn repository_id(&self) -> &str {
325            "IDL:demo/Dummy:1.0"
326        }
327        fn is_modified(&self) -> bool {
328            false
329        }
330        fn is_deleted(&self) -> bool {
331            false
332        }
333    }
334
335    #[test]
336    fn object_root_trait_callable() {
337        let o = DummyObject;
338        assert_eq!(o.oid(), 42);
339        assert_eq!(o.repository_id(), "IDL:demo/Dummy:1.0");
340        assert!(!o.is_modified());
341        assert!(!o.is_deleted());
342    }
343
344    // §8.1.3.3 / §8.1.4.4 Metamodel
345    #[test]
346    fn class_metamodel_default_fields() {
347        let c = ClassMetamodel {
348            name: "Trade".into(),
349            repository_id: "IDL:demo/Trade:1.0".into(),
350            main_topic: default_mapping::topic_name("Trade"),
351            oid_field: default_mapping::DEFAULT_OID_FIELD.into(),
352            class_field: default_mapping::DEFAULT_CLASS_FIELD.into(),
353            full_oid_required: false,
354            final_class: false,
355        };
356        assert_eq!(c.main_topic, "Trade");
357        assert_eq!(c.oid_field, "oid");
358        assert_eq!(c.class_field, "class");
359    }
360
361    #[test]
362    fn default_mapping_topic_name_uses_class() {
363        assert_eq!(default_mapping::topic_name("Trader"), "Trader");
364    }
365
366    #[test]
367    fn default_mapping_multi_topic_dot_separated() {
368        assert_eq!(
369            default_mapping::multi_topic("Order", "items"),
370            "Order.items"
371        );
372    }
373
374    #[test]
375    fn default_mapping_constants_match_spec() {
376        assert_eq!(default_mapping::DEFAULT_OID_FIELD, "oid");
377        assert_eq!(default_mapping::DEFAULT_CLASS_FIELD, "class");
378        assert_eq!(default_mapping::DEFAULT_INDEX_FIELD, "index");
379    }
380
381    #[test]
382    fn multi_attribute_metamodel_construct() {
383        let m = MultiAttributeMetamodel {
384            name: "items".into(),
385            topic: "Order.items".into(),
386            target_field: "value".into(),
387            index_field: "index".into(),
388            key_fields: alloc::vec!["order_id".into()],
389        };
390        assert_eq!(m.key_fields.len(), 1);
391    }
392
393    #[test]
394    fn mono_relation_metamodel_construct() {
395        let r = MonoRelationMetamodel {
396            name: "owner".into(),
397            topic: "Trade.owner".into(),
398            target_fields: alloc::vec!["trader_id".into()],
399            key_fields: alloc::vec!["trade_id".into()],
400            full_oid_required: true,
401            is_composition: false,
402        };
403        assert!(r.full_oid_required);
404    }
405
406    // §8.1.6.2 17 entity classes
407    #[test]
408    fn dlrl_entity_kinds_count_17() {
409        assert_eq!(DlrlEntityKind::all().len(), 17);
410    }
411
412    #[test]
413    fn dlrl_entity_kinds_distinct() {
414        let kinds = DlrlEntityKind::all();
415        for (i, k1) in kinds.iter().enumerate() {
416            for k2 in kinds.iter().skip(i + 1) {
417                assert_ne!(k1, k2);
418            }
419        }
420    }
421
422    #[test]
423    fn dlrl_entity_kind_spec_names_match() {
424        assert_eq!(DlrlEntityKind::CacheFactory.spec_name(), "CacheFactory");
425        assert_eq!(DlrlEntityKind::ObjectRoot.spec_name(), "ObjectRoot");
426        assert_eq!(DlrlEntityKind::IntMap.spec_name(), "IntMap");
427    }
428
429    // §8.1.6.2 8 exception types
430    #[test]
431    fn dlrl_exception_repository_ids_distinct() {
432        let exceptions = [
433            DlrlException::DcpsError { reason: "x".into() },
434            DlrlException::BadHomeDefinition { reason: "x".into() },
435            DlrlException::NotFound,
436            DlrlException::AlreadyExisting,
437            DlrlException::AlreadyDeleted,
438            DlrlException::PreconditionNotMet {
439                constraint: "x".into(),
440            },
441            DlrlException::NoSuchElement,
442            DlrlException::SqlError { reason: "x".into() },
443        ];
444        let mut seen = std::collections::HashSet::new();
445        for e in &exceptions {
446            assert!(
447                seen.insert(e.repository_id()),
448                "duplicate: {}",
449                e.repository_id()
450            );
451        }
452        assert_eq!(seen.len(), 8);
453    }
454
455    #[test]
456    fn dlrl_exception_repo_id_omg_namespace() {
457        for e in [
458            DlrlException::NotFound,
459            DlrlException::AlreadyExisting,
460            DlrlException::NoSuchElement,
461        ] {
462            assert!(e.repository_id().starts_with("IDL:omg.org/DLRL/"));
463        }
464    }
465
466    // §8.1.5 cache lifecycle
467    #[test]
468    fn cache_attachment_states_distinct() {
469        assert_ne!(
470            CacheAttachmentState::Detached,
471            CacheAttachmentState::AttachedSubscriber
472        );
473        assert_ne!(
474            CacheAttachmentState::AttachedPublisher,
475            CacheAttachmentState::AttachedBoth
476        );
477    }
478
479    #[test]
480    fn cache_modes_distinct() {
481        assert_ne!(CacheMode::Transparent, CacheMode::OnDemand);
482    }
483
484    // Annex B
485    #[test]
486    fn query_expression_construct() {
487        let mut q = QueryExpression::new("price > ?");
488        q.add_param("100");
489        assert_eq!(q.expr, "price > ?");
490        assert_eq!(q.params, alloc::vec!["100".to_string()]);
491    }
492}