Skip to main content

zerodds_rtc/
resource.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! OMG RTC 1.0 §5.4 Resource Data Model + Introspection Interfaces.
5//!
6//! Phase-B-Cluster-10 (Spec-Cycle 5).
7//!
8//! Spec-Quelle: OMG RTC 1.0 §5.4.1 (S. 61-70) Resource-Datenmodell +
9//! §5.4.2 (S. 71-77) Stereotypes-and-Interfaces (Introspection-Iface-
10//! Operations).
11//!
12//! # Modell
13//!
14//! Der Datenstrom ist:
15//!
16//! ```text
17//!   ComponentProfile
18//!     ├── ports: Vec<PortProfile>
19//!     └── connectors: Vec<ConnectorProfile>
20//! ```
21//!
22//! Discovery-Wire (z.B. DDS-Topic-Push der Profiles) ist Caller-
23//! Layer und ausserhalb dieses Crates.
24
25use alloc::string::String;
26use alloc::vec::Vec;
27
28/// Eindeutiger Identifier eines RTC-Modells (Component / Port /
29/// Connector). Als Spec-§5.4.1 vorgesehen ist eine UUID-Form;
30/// wir benutzen einen 16-Byte-Opaque-Vec.
31#[derive(Debug, Clone, PartialEq, Eq, Hash)]
32pub struct ProfileId(pub [u8; 16]);
33
34impl ProfileId {
35    /// Erzeugt eine Null-ID (alle Bytes 0). Wird vom Discovery-Layer
36    /// vor Vergabe einer echten UUID genutzt.
37    #[must_use]
38    pub const fn nil() -> Self {
39        Self([0u8; 16])
40    }
41}
42
43impl Default for ProfileId {
44    fn default() -> Self {
45        Self::nil()
46    }
47}
48
49/// Port-Direction (Spec §5.4.1 Tab 5.10).
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum PortDirection {
52    /// Eingang.
53    #[default]
54    In,
55    /// Ausgang.
56    Out,
57    /// Bidirektional (Service-Port).
58    InOut,
59}
60
61/// Spec §5.4.1 — Port-Profile (Beschreibung eines Ports einer
62/// Komponente).
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct PortProfile {
65    /// UUID des Ports.
66    pub id: ProfileId,
67    /// Lokaler Name (z.B. `"odom_in"`).
68    pub name: String,
69    /// IDL-Type-Name des Port-Datenmodells (z.B. `"geometry::Pose"`).
70    pub data_type: String,
71    /// Direction.
72    pub direction: PortDirection,
73    /// User-defined Properties (Key-Value).
74    pub properties: Vec<(String, String)>,
75}
76
77/// Spec §5.4.1 — Connector-Profile (Bindung zwischen 2+ Port-Profiles).
78#[derive(Debug, Clone, PartialEq, Eq, Default)]
79pub struct ConnectorProfile {
80    /// UUID des Connectors.
81    pub id: ProfileId,
82    /// Connector-Name.
83    pub name: String,
84    /// Liste der Port-IDs, die dieser Connector verbindet.
85    pub port_ids: Vec<ProfileId>,
86    /// User-defined Properties.
87    pub properties: Vec<(String, String)>,
88}
89
90/// Spec §5.4.1 — Component-Profile.
91#[derive(Debug, Clone, PartialEq, Eq, Default)]
92pub struct ComponentProfile {
93    /// UUID der Komponente.
94    pub id: ProfileId,
95    /// Component-Type (analog Spec §5.2 Component-Profile-Type).
96    pub type_name: String,
97    /// Komponenten-Instanz-Name.
98    pub instance_name: String,
99    /// Vendor (z.B. `"ZeroDDS"`).
100    pub vendor: String,
101    /// Version (Semver-String).
102    pub version: String,
103    /// Liste der Ports.
104    pub ports: Vec<PortProfile>,
105    /// Liste der Connectors.
106    pub connectors: Vec<ConnectorProfile>,
107    /// User-defined Properties.
108    pub properties: Vec<(String, String)>,
109}
110
111/// Spec §5.4.2 — Introspection-Operations.
112///
113/// Die Spec definiert ein abstraktes UML-Interface mit drei Methoden;
114/// wir realisieren es als Rust-Trait, sodass eine konkrete RTC-
115/// Implementation es per Compile-Time-Dispatch bedient.
116pub trait Introspection {
117    /// Spec §5.4.2 — `ComponentProfile get_component_profile()`.
118    fn get_component_profile(&self) -> &ComponentProfile;
119
120    /// Spec §5.4.2 — `PortProfile get_port_profile(in PortId id)`.
121    /// Returns `None` wenn der Port nicht zur Komponente gehoert.
122    fn get_port_profile(&self, id: &ProfileId) -> Option<&PortProfile> {
123        self.get_component_profile()
124            .ports
125            .iter()
126            .find(|p| &p.id == id)
127    }
128
129    /// Spec §5.4.2 — `ConnectorProfile get_connector_profile(in
130    /// ConnectorId id)`.
131    fn get_connector_profile(&self, id: &ProfileId) -> Option<&ConnectorProfile> {
132        self.get_component_profile()
133            .connectors
134            .iter()
135            .find(|c| &c.id == id)
136    }
137
138    /// Spec §5.4.2 — `sequence<PortProfile> get_ports()`.
139    fn get_ports(&self) -> &[PortProfile] {
140        &self.get_component_profile().ports
141    }
142
143    /// Spec §5.4.2 — `sequence<ConnectorProfile> get_connectors()`.
144    fn get_connectors(&self) -> &[ConnectorProfile] {
145        &self.get_component_profile().connectors
146    }
147}
148
149#[cfg(test)]
150#[allow(clippy::expect_used)]
151mod tests {
152    use super::*;
153
154    fn pid(b: u8) -> ProfileId {
155        let mut a = [0u8; 16];
156        a[15] = b;
157        ProfileId(a)
158    }
159
160    fn sample_port(b: u8, name: &str, dir: PortDirection) -> PortProfile {
161        PortProfile {
162            id: pid(b),
163            name: name.into(),
164            data_type: "geometry::Pose".into(),
165            direction: dir,
166            properties: Vec::new(),
167        }
168    }
169
170    struct StubComponent {
171        profile: ComponentProfile,
172    }
173
174    impl Introspection for StubComponent {
175        fn get_component_profile(&self) -> &ComponentProfile {
176            &self.profile
177        }
178    }
179
180    fn build_stub() -> StubComponent {
181        let p1 = sample_port(1, "in_port", PortDirection::In);
182        let p2 = sample_port(2, "out_port", PortDirection::Out);
183        let conn = ConnectorProfile {
184            id: pid(10),
185            name: "loop".into(),
186            port_ids: alloc::vec![pid(1), pid(2)],
187            properties: Vec::new(),
188        };
189        let comp = ComponentProfile {
190            id: pid(99),
191            type_name: "robotics::Localizer".into(),
192            instance_name: "loc1".into(),
193            vendor: "ZeroDDS".into(),
194            version: "1.0".into(),
195            ports: alloc::vec![p1, p2],
196            connectors: alloc::vec![conn],
197            properties: Vec::new(),
198        };
199        StubComponent { profile: comp }
200    }
201
202    #[test]
203    fn get_component_profile_returns_component() {
204        let s = build_stub();
205        assert_eq!(s.get_component_profile().instance_name, "loc1");
206    }
207
208    #[test]
209    fn get_port_profile_returns_some_when_known() {
210        let s = build_stub();
211        let p = s.get_port_profile(&pid(1)).expect("port present");
212        assert_eq!(p.name, "in_port");
213        assert_eq!(p.direction, PortDirection::In);
214    }
215
216    #[test]
217    fn get_port_profile_returns_none_when_unknown() {
218        let s = build_stub();
219        assert!(s.get_port_profile(&pid(99)).is_none());
220    }
221
222    #[test]
223    fn get_connector_profile_returns_known_connector() {
224        let s = build_stub();
225        let c = s.get_connector_profile(&pid(10)).expect("connector");
226        assert_eq!(c.name, "loop");
227        assert_eq!(c.port_ids.len(), 2);
228    }
229
230    #[test]
231    fn get_ports_returns_all_two_ports() {
232        let s = build_stub();
233        assert_eq!(s.get_ports().len(), 2);
234    }
235
236    #[test]
237    fn get_connectors_returns_one_connector() {
238        let s = build_stub();
239        assert_eq!(s.get_connectors().len(), 1);
240    }
241
242    #[test]
243    fn nil_profile_id_has_zero_bytes() {
244        assert_eq!(ProfileId::nil().0, [0u8; 16]);
245    }
246
247    #[test]
248    fn default_port_direction_is_in() {
249        assert_eq!(PortDirection::default(), PortDirection::In);
250    }
251
252    #[test]
253    fn introspection_default_methods_compose_correctly() {
254        // Stelle sicher, dass die Trait-Default-Implementierungen das
255        // Component-Profile als Wahrheits-Quelle ehren.
256        let s = build_stub();
257        assert_eq!(s.get_ports().len(), s.get_component_profile().ports.len());
258    }
259
260    #[test]
261    fn component_profile_field_round_trip() {
262        let cp = ComponentProfile {
263            id: pid(1),
264            type_name: "T".into(),
265            instance_name: "I".into(),
266            vendor: "V".into(),
267            version: "1.0".into(),
268            ports: Vec::new(),
269            connectors: Vec::new(),
270            properties: alloc::vec![("k".into(), "v".into())],
271        };
272        assert_eq!(
273            cp.properties[0],
274            (
275                alloc::string::String::from("k"),
276                alloc::string::String::from("v")
277            )
278        );
279    }
280}