Skip to main content

zerodds_dcps/
factory.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `DomainParticipantFactory` — Singleton fuer das Anlegen von
4//! Participants (Spec OMG DDS 1.4 §2.2.2.2.1).
5//!
6//! Die Spec verlangt: "The DomainParticipantFactory is a singleton.
7//! The get_instance() method returns a reference to the only
8//! instance of the factory."
9//!
10//! Singleton via `OnceLock`. `create_participant(domain_id, qos)`
11//! legt einen neuen `DomainParticipant` an, startet eine Live-
12//! Runtime und gibt einen clone-baren Handle zurueck. Daneben gibt
13//! es `create_participant_offline(domain_id, qos)` fuer Skeleton-
14//! Tests ohne Netzwerk und `create_participant_with_config` fuer
15//! Tests mit angepasster `RuntimeConfig`.
16
17extern crate alloc;
18
19use alloc::collections::BTreeMap;
20use alloc::vec::Vec;
21
22#[cfg(feature = "std")]
23use std::sync::{Mutex, OnceLock};
24
25use crate::error::{DdsError, Result};
26use crate::participant::{DomainId, DomainParticipant};
27use crate::qos::DomainParticipantQos;
28use crate::runtime::RuntimeConfig;
29
30/// QoS-Policy fuer den DomainParticipantFactory selbst (Spec
31/// §2.2.2.2.2.6 `DomainParticipantFactoryQos`). Aktuell ein einziges
32/// Feld `autoenable_created_entities` (Default `true`); spaetere
33/// Spec-Erweiterungen werden hier addiert.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct DomainParticipantFactoryQos {
36    /// Wenn `true`, werden neu erzeugte Entities automatisch beim
37    /// Erzeugen `enable()`-d. Spec-Default: `true`.
38    pub autoenable_created_entities: bool,
39}
40
41impl Default for DomainParticipantFactoryQos {
42    fn default() -> Self {
43        Self {
44            autoenable_created_entities: true,
45        }
46    }
47}
48
49/// Factory-Singleton.
50#[cfg(feature = "std")]
51#[derive(Debug)]
52pub struct DomainParticipantFactory {
53    /// Registry aller per `create_participant*` erzeugten Participants —
54    /// indexiert nach `DomainId`, mehrere Participants pro Domain
55    /// erlaubt (Spec §2.2.2.2.2.4 `lookup_participant` liefert "any"
56    /// Participant fuer die gegebene Domain).
57    participants: Mutex<BTreeMap<DomainId, Vec<DomainParticipant>>>,
58    /// Factory-Default-QoS fuer neu erzeugte Participants (Spec
59    /// §2.2.2.2.2.5 `set_default_participant_qos`).
60    default_participant_qos: Mutex<DomainParticipantQos>,
61    /// Factory-eigene QoS (Spec §2.2.2.2.2.6 `set_qos`/`get_qos`).
62    factory_qos: Mutex<DomainParticipantFactoryQos>,
63}
64
65#[cfg(not(feature = "std"))]
66#[derive(Debug, Default)]
67pub struct DomainParticipantFactory {}
68
69#[cfg(feature = "std")]
70impl DomainParticipantFactory {
71    /// Liefert den Prozess-weiten Factory-Singleton (Spec §2.2.2.2.2.1
72    /// `get_instance`).
73    pub fn instance() -> &'static Self {
74        static INSTANCE: OnceLock<DomainParticipantFactory> = OnceLock::new();
75        INSTANCE.get_or_init(|| Self {
76            participants: Mutex::new(BTreeMap::new()),
77            default_participant_qos: Mutex::new(DomainParticipantQos::default()),
78            factory_qos: Mutex::new(DomainParticipantFactoryQos::default()),
79        })
80    }
81
82    fn track(&self, p: &DomainParticipant) {
83        if let Ok(mut reg) = self.participants.lock() {
84            reg.entry(p.domain_id()).or_default().push(p.clone());
85        }
86    }
87
88    /// Erzeugt einen neuen `DomainParticipant` fuer die gegebene
89    /// Domain-Id. Startet die `DcpsRuntime` mit Default-Config —
90    /// UDP-Sockets + SPDP/SEDP-Threads.
91    ///
92    /// # Errors
93    /// `DdsError::TransportError` wenn die UDP-Sockets nicht binden.
94    pub fn create_participant(
95        &self,
96        domain_id: DomainId,
97        qos: DomainParticipantQos,
98    ) -> Result<DomainParticipant> {
99        // DDS 1.4 §2.2.3.1 UserDataQosPolicy → SPDP-Beacon PID_USER_DATA.
100        let config = RuntimeConfig {
101            user_data: qos.user_data.value.clone(),
102            ..RuntimeConfig::default()
103        };
104        let p = DomainParticipant::new_with_runtime(domain_id, qos, config)?;
105        self.track(&p);
106        Ok(p)
107    }
108
109    /// Variante mit explizit uebergebener `RuntimeConfig` (z.B. fuer
110    /// Tests mit kurzen SPDP-Periods).
111    ///
112    /// # Errors
113    /// `DdsError::TransportError` wenn die UDP-Sockets nicht binden.
114    pub fn create_participant_with_config(
115        &self,
116        domain_id: DomainId,
117        qos: DomainParticipantQos,
118        config: RuntimeConfig,
119    ) -> Result<DomainParticipant> {
120        let p = DomainParticipant::new_with_runtime(domain_id, qos, config)?;
121        self.track(&p);
122        Ok(p)
123    }
124
125    /// Offline-Variante ohne Runtime — nur fuer Unit-Tests die kein
126    /// Netzwerk wollen. Der zurueckgegebene Participant kann Topics
127    /// erzeugen, aber keine DataWriter/Reader.
128    #[must_use]
129    pub fn create_participant_offline(
130        &self,
131        domain_id: DomainId,
132        qos: DomainParticipantQos,
133    ) -> DomainParticipant {
134        let p = DomainParticipant::new(domain_id, qos);
135        self.track(&p);
136        p
137    }
138
139    /// Spec §2.2.2.2.2.4 `lookup_participant(domain_id)` — liefert
140    /// einen vorher erzeugten Participant zur gleichen Domain-Id, oder
141    /// `None` wenn keiner registriert ist. Bei mehreren Participants
142    /// derselben Domain liefert die Implementation den ersten.
143    #[must_use]
144    pub fn lookup_participant(&self, domain_id: DomainId) -> Option<DomainParticipant> {
145        let reg = self.participants.lock().ok()?;
146        reg.get(&domain_id).and_then(|v| v.first().cloned())
147    }
148
149    /// Spec §2.2.2.2.2.3 `delete_participant`. Entfernt den Participant
150    /// aus der Factory-Registry und ruft `delete_contained_entities`
151    /// auf. Liefert `PreconditionNotMet` wenn der Participant nicht
152    /// in der Registry ist.
153    ///
154    /// # Errors
155    /// `DdsError::PreconditionNotMet` wenn der Participant nicht
156    /// registriert ist.
157    pub fn delete_participant(&self, p: &DomainParticipant) -> Result<()> {
158        let mut reg = self
159            .participants
160            .lock()
161            .map_err(|_| DdsError::PreconditionNotMet {
162                reason: "factory participants poisoned",
163            })?;
164        let target_handle = p.instance_handle();
165        let mut found = false;
166        if let Some(vec) = reg.get_mut(&p.domain_id()) {
167            let before = vec.len();
168            vec.retain(|q| q.instance_handle() != target_handle);
169            found = vec.len() < before;
170            if vec.is_empty() {
171                reg.remove(&p.domain_id());
172            }
173        }
174        drop(reg);
175        if !found {
176            return Err(DdsError::PreconditionNotMet {
177                reason: "participant not registered with this factory",
178            });
179        }
180        p.delete_contained_entities()
181    }
182
183    /// Spec §2.2.2.2.2.5 `set_default_participant_qos` — Default-QoS
184    /// fuer ab jetzt erzeugte Participants.
185    ///
186    /// # Errors
187    /// `DdsError::PreconditionNotMet` bei Lock-Poisoning.
188    pub fn set_default_participant_qos(&self, qos: DomainParticipantQos) -> Result<()> {
189        let mut current =
190            self.default_participant_qos
191                .lock()
192                .map_err(|_| DdsError::PreconditionNotMet {
193                    reason: "default qos poisoned",
194                })?;
195        *current = qos;
196        Ok(())
197    }
198
199    /// Spec §2.2.2.2.2.5 `get_default_participant_qos`.
200    #[must_use]
201    pub fn get_default_participant_qos(&self) -> DomainParticipantQos {
202        self.default_participant_qos
203            .lock()
204            .map(|q| q.clone())
205            .unwrap_or_default()
206    }
207
208    /// Spec §2.2.2.2.2.6 `set_qos` (Factory-Level QoS).
209    ///
210    /// # Errors
211    /// `DdsError::PreconditionNotMet` bei Lock-Poisoning.
212    pub fn set_qos(&self, qos: DomainParticipantFactoryQos) -> Result<()> {
213        let mut current = self
214            .factory_qos
215            .lock()
216            .map_err(|_| DdsError::PreconditionNotMet {
217                reason: "factory qos poisoned",
218            })?;
219        *current = qos;
220        Ok(())
221    }
222
223    /// Spec §2.2.2.2.2.6 `get_qos` (Factory-Level QoS).
224    #[must_use]
225    pub fn get_qos(&self) -> DomainParticipantFactoryQos {
226        self.factory_qos.lock().map(|q| *q).unwrap_or_default()
227    }
228}
229
230#[cfg(test)]
231#[allow(clippy::expect_used, clippy::unwrap_used)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn factory_is_singleton() {
237        let a = DomainParticipantFactory::instance();
238        let b = DomainParticipantFactory::instance();
239        assert!(core::ptr::eq(a, b));
240    }
241
242    #[test]
243    fn factory_creates_participant_with_correct_domain_id() {
244        // Offline-Variante — kein UDP-Bind im Unit-Test. Live-Variante
245        // wird in runtime.rs getestet.
246        let p = DomainParticipantFactory::instance()
247            .create_participant_offline(7, DomainParticipantQos::default());
248        assert_eq!(p.domain_id(), 7);
249    }
250
251    // ---- §2.2.2.2.2.4 lookup_participant ----
252
253    #[test]
254    fn lookup_participant_finds_registered_offline_participant() {
255        let f = DomainParticipantFactory::instance();
256        // Eindeutige Domain-Id, damit kein anderer Test-Participant
257        // den Lookup beeinflusst.
258        let domain = 91;
259        let _p = f.create_participant_offline(domain, DomainParticipantQos::default());
260        let found = f.lookup_participant(domain);
261        assert!(found.is_some());
262        assert_eq!(found.unwrap().domain_id(), domain);
263    }
264
265    #[test]
266    fn lookup_participant_returns_none_for_unknown_domain() {
267        let f = DomainParticipantFactory::instance();
268        // Sehr hoher domain_id-Wert, fuer den vermutlich nichts existiert.
269        assert!(f.lookup_participant(60_001).is_none());
270    }
271
272    // ---- §2.2.2.2.2.3 delete_participant ----
273
274    #[test]
275    fn delete_participant_removes_from_registry() {
276        let f = DomainParticipantFactory::instance();
277        let domain = 92;
278        let p = f.create_participant_offline(domain, DomainParticipantQos::default());
279        assert!(f.lookup_participant(domain).is_some());
280        f.delete_participant(&p).unwrap();
281        assert!(f.lookup_participant(domain).is_none());
282    }
283
284    #[test]
285    fn delete_participant_unknown_returns_precondition_not_met() {
286        let f = DomainParticipantFactory::instance();
287        // Erzeuge einen Participant ohne ihn ueber die Factory zu
288        // tracken (DomainParticipant::new direkt).
289        let detached =
290            crate::participant::DomainParticipant::new(93, DomainParticipantQos::default());
291        let res = f.delete_participant(&detached);
292        assert!(matches!(
293            res,
294            Err(crate::error::DdsError::PreconditionNotMet { .. })
295        ));
296    }
297
298    // ---- §2.2.2.2.2.5 default_participant_qos ----
299
300    #[test]
301    fn default_participant_qos_roundtrips() {
302        let f = DomainParticipantFactory::instance();
303        let mut new_qos = f.get_default_participant_qos();
304        // Wir mutieren irgendeinen erkennbaren Default. DomainParticipantQos
305        // ist Default-konstruierbar; um den Roundtrip zu verifizieren
306        // genuegt es, set/get zu durchlaufen.
307        new_qos = new_qos.clone();
308        f.set_default_participant_qos(new_qos.clone()).unwrap();
309        let got = f.get_default_participant_qos();
310        // Gleichheit modulo Equality-Impl der DomainParticipantQos.
311        assert_eq!(format!("{got:?}"), format!("{new_qos:?}"));
312    }
313
314    // ---- §2.2.2.2.2.6 Factory-eigene QoS ----
315
316    #[test]
317    fn factory_qos_default_is_autoenable_true() {
318        // Spec-Default §2.2.2.2.2.6: autoenable_created_entities = TRUE.
319        let q = DomainParticipantFactoryQos::default();
320        assert!(q.autoenable_created_entities);
321    }
322
323    #[test]
324    fn factory_set_get_qos_roundtrip() {
325        let f = DomainParticipantFactory::instance();
326        let q = DomainParticipantFactoryQos {
327            autoenable_created_entities: false,
328        };
329        f.set_qos(q).unwrap();
330        let got = f.get_qos();
331        assert!(!got.autoenable_created_entities);
332        // Restore default fuer andere Tests.
333        f.set_qos(DomainParticipantFactoryQos::default()).unwrap();
334    }
335}