Skip to main content

zerodds_webdds_bridge/
gateway.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `impl DdsBackend for Gateway` — Spec §7.4, backed by `zerodds-dcps`'s
5//! public typed API.
6//!
7//! One [`Gateway`] per daemon process. It holds a tree that mirrors the
8//! WebDDS object model: applications, each with `DomainParticipant`s,
9//! each with topics/publishers/subscribers, each publisher/subscriber
10//! with `DataWriter`/`DataReader`s. Names the trait's caller does not
11//! supply (a participant, a publisher, a subscriber, a writer, a
12//! reader) are generated here and returned in
13//! `BackendResult::Created`'s path, the way `WebDdsRoot::
14//! create_application` returns a generated `SessionId` — the caller
15//! addresses the entity by that path in every later call.
16//!
17//! Sample bodies are always the DDS-WEB §8.1 XML representation
18//! (`dynxml`), converted to/from wire bytes via `zerodds_wire_codec`
19//! directly (the same encode/decode the type's `SerializedTypeSupport`
20//! uses internally) plus `SerializedTypeSupport::read_body` for
21//! validation and key-hash derivation before a write.
22
23use std::collections::BTreeMap;
24use std::path::PathBuf;
25use std::sync::{Arc, Mutex};
26use std::time::Duration;
27
28use zerodds_ast_layout::{Encoding, Endianness};
29use zerodds_dcps::serialized_type::SerializedSample;
30use zerodds_dcps::topic::Topic;
31use zerodds_dcps::{
32    Condition, DataReader, DataReaderQos, DataWriter, DataWriterQos, DomainParticipant,
33    DomainParticipantFactory, DomainParticipantQos, Entity, Publisher, PublisherQos, ReadCondition,
34    Subscriber, SubscriberQos, TopicQos, WaitSet,
35};
36use zerodds_web::bridge::{BackendError, BackendResult, DdsBackend};
37use zerodds_wire_codec::ProbeRequest;
38
39use crate::dynxml;
40use crate::qos_update::EntityKind;
41use crate::types_registry::{ResolvedType, TypeRegistry};
42
43/// Always-used sample representation: XCDR2, little-endian. Consistent
44/// throughout the gateway; a peer that needs a different one is a
45/// per-writer/-reader `data_representation_offer` override this crate
46/// does not (yet) expose through the REST/SOAP surface. The CDR dialect is
47/// the one `resolved`'s support carries, its participant's
48/// `wire.codec_dialect`.
49fn probe_request(resolved: &ResolvedType) -> ProbeRequest {
50    ProbeRequest::in_dialect(
51        &resolved.dynamic_type.qualified_name(),
52        Encoding::PlainCdr2,
53        Endianness::Little,
54        resolved.support.dialect(),
55    )
56}
57
58/// Bound on automatic TypeLookup discovery in
59/// [`Gateway::resolve_type_for_topic`] — an HTTP/SOAP request waits at
60/// most this long for a `getTypes` round trip before the response names
61/// the type as unresolved (never silently — see `route_to_outcome`'s
62/// `BadRequest` mapping).
63const TYPE_LOOKUP_TIMEOUT: Duration = Duration::from_secs(5);
64
65struct WriterEntry {
66    writer: DataWriter<SerializedSample>,
67    resolved: ResolvedType,
68}
69
70struct ReaderEntry {
71    reader: Arc<DataReader<SerializedSample>>,
72    resolved: ResolvedType,
73}
74
75struct PublisherEntry {
76    publisher: Publisher,
77    writers: BTreeMap<String, WriterEntry>,
78}
79
80struct SubscriberEntry {
81    subscriber: Subscriber,
82    readers: BTreeMap<String, ReaderEntry>,
83}
84
85struct ParticipantEntry {
86    participant: DomainParticipant,
87    topics: BTreeMap<String, Topic<SerializedSample>>,
88    publishers: BTreeMap<String, PublisherEntry>,
89    subscribers: BTreeMap<String, SubscriberEntry>,
90}
91
92struct WaitsetEntry {
93    waitset: WaitSet,
94    /// Reader path -> the `Arc<ReadCondition>` attached for it, so a
95    /// triggered condition (returned by identity from `WaitSet::wait`)
96    /// maps back to the reader path the caller asked to name.
97    conditions: Vec<(String, Arc<ReadCondition>)>,
98    /// One HTTP client waits at a time: a DCPS `WaitSet` refuses a second
99    /// concurrent `wait` with PRECONDITION_NOT_MET (DDS 1.4
100    /// §2.2.2.1.6), so a second client waits here for the first.
101    turn: Mutex<()>,
102}
103
104/// The default long-poll of a waitset when the caller names no timeout.
105pub const DEFAULT_WAITSET_TIMEOUT: Duration = Duration::from_secs(10);
106
107/// A waitset taken out of the gateway, to wait on without holding the
108/// gateway: a long-poll held it before, and every other request waited
109/// for it (measured on the Linux test host: a list of applications took 3.7 s during
110/// a 4 s long-poll, 0.2 ms without).
111pub struct WaitsetHandle(Arc<WaitsetEntry>);
112
113impl WaitsetHandle {
114    /// Waits up to `timeout` for a condition of the waitset and lists the
115    /// reader paths whose condition triggered; empty when the timeout
116    /// elapsed.
117    ///
118    /// # Errors
119    /// [`BackendError::Internal`] when the DCPS wait fails otherwise.
120    pub fn wait(&self, timeout: Duration) -> Result<BackendResult, BackendError> {
121        let entry = &self.0;
122        let _turn = entry
123            .turn
124            .lock()
125            .unwrap_or_else(std::sync::PoisonError::into_inner);
126        let triggered = match entry.waitset.wait(timeout) {
127            Ok(v) => v,
128            Err(zerodds_dcps::DdsError::Timeout) => Vec::new(),
129            Err(e) => return Err(BackendError::Internal(format!("{e:?}"))),
130        };
131        let mut paths = Vec::new();
132        for cond in &triggered {
133            for (path, rc) in &entry.conditions {
134                if Arc::ptr_eq(&(Arc::clone(rc) as Arc<dyn Condition>), cond) {
135                    paths.push(path.clone());
136                }
137            }
138        }
139        Ok(BackendResult::List(paths))
140    }
141}
142
143/// A caller's long-poll timeout in seconds; none is
144/// [`DEFAULT_WAITSET_TIMEOUT`].
145///
146/// # Errors
147/// [`BackendError::BadRequest`] for text that is not a finite, non-negative
148/// number of seconds a `Duration` holds. `Duration::from_secs_f64` panicked
149/// on `-1`, `nan` and `1e300` (measured on the Linux test host: the connection's thread
150/// ended without a response), and `abc` silently waited the default.
151pub fn waitset_timeout(text: Option<&str>) -> Result<Duration, BackendError> {
152    let Some(text) = text else {
153        return Ok(DEFAULT_WAITSET_TIMEOUT);
154    };
155    text.trim()
156        .parse::<f64>()
157        .ok()
158        .and_then(|secs| Duration::try_from_secs_f64(secs).ok())
159        .ok_or_else(|| {
160            BackendError::BadRequest(format!("timeout `{text}` is not a number of seconds"))
161        })
162}
163
164/// A participant's `domain_id` as a request carries it; none is domain 0.
165///
166/// # Errors
167/// [`BackendError::BadRequest`] for text that is not a domain id. It was
168/// domain 0 before, whatever the text said, and a value above `i32::MAX`
169/// wrapped to a negative id.
170pub fn domain_id_of(text: Option<&str>) -> Result<u32, BackendError> {
171    let Some(text) = text else {
172        return Ok(0);
173    };
174    text.trim()
175        .parse::<u32>()
176        .ok()
177        .filter(|id| i32::try_from(*id).is_ok())
178        .ok_or_else(|| BackendError::BadRequest(format!("domain_id `{text}` is not a domain id")))
179}
180
181#[derive(Default)]
182struct AppEntry {
183    participants: BTreeMap<String, ParticipantEntry>,
184    waitsets: BTreeMap<String, Arc<WaitsetEntry>>,
185    next_participant: u64,
186    next_publisher: u64,
187    next_subscriber: u64,
188    next_writer: u64,
189    next_reader: u64,
190}
191
192/// Where a config-named type's definition lives.
193#[derive(Debug, Clone)]
194pub enum TypeSource {
195    /// `types[].idl_file` — `TypeRegistry::load_idl_file`.
196    Idl(PathBuf),
197    /// `types[].xml_file` — a DDS-XML 1.0 `<types>` document,
198    /// `TypeRegistry::load_xml_file`.
199    Xml(PathBuf),
200}
201
202/// The DCPS-backed gateway. `Arc<Gateway>` is shared between the REST
203/// and SOAP dispatchers of one daemon process.
204pub struct Gateway {
205    apps: Mutex<BTreeMap<String, AppEntry>>,
206    types: Arc<TypeRegistry>,
207    /// `type_name -> source`, from the daemon config's `types:`
208    /// section — resolved lazily on first use of that type name.
209    type_sources: BTreeMap<String, TypeSource>,
210    /// The daemon's §7.3.1 `Root` metadata store (QosLibrary/QosProfile/
211    /// Type) — not a DCPS entity, so not part of `DdsBackend` itself
212    /// (see `crate::metadata`'s module doc), but `update_*`'s
213    /// `<qos_profile>library::profile</qos_profile>` reference
214    /// (Spec SS7.4.4.4) resolves through it, so `Gateway` holds a
215    /// handle rather than needing a `DdsBackend` signature change.
216    metadata: Arc<crate::metadata::MetadataStore>,
217}
218
219impl Gateway {
220    /// New, empty gateway. `type_sources` names every type this gateway
221    /// can resolve from a config-named IDL or DDS-XML file; a type not
222    /// in it must already be resolvable via
223    /// [`crate::types_registry::TypeRegistry::resolve_discovered`] (or
224    /// via automatic TypeLookup — see `Self::resolve_type_for_topic`)
225    /// before `create_topic` is asked for it. `metadata` is the same
226    /// store `rest_service`/`soap_service` dispatch QosLibrary/
227    /// QosProfile/Type CRUD against.
228    #[must_use]
229    pub fn new(
230        type_sources: BTreeMap<String, TypeSource>,
231        metadata: Arc<crate::metadata::MetadataStore>,
232    ) -> Self {
233        Self {
234            apps: Mutex::new(BTreeMap::new()),
235            types: Arc::new(TypeRegistry::new()),
236            type_sources,
237            metadata,
238        }
239    }
240
241    /// The shared type registry — the daemon's TypeLookup poll loop
242    /// (`daemon::server`) resolves discovered types into it directly.
243    #[must_use]
244    pub fn types(&self) -> &Arc<TypeRegistry> {
245        &self.types
246    }
247
248    /// Resolves `type_name` for a `create_topic` call naming `topic_name`:
249    /// the cache, then a config-named IDL/XML file, then — only when
250    /// neither answers — automatic TypeLookup discovery bounded by
251    /// [`TYPE_LOOKUP_TIMEOUT`] (`TypeRegistry::resolve_named`), which
252    /// needs `participant`'s live `DcpsRuntime` to issue `getTypes`
253    /// against a peer SEDP already discovered for this topic.
254    ///
255    /// The support comes back in `participant`'s CDR dialect
256    /// (`wire.codec_dialect`), so the samples this gateway writes and reads
257    /// for the type are encoded and decoded in it.
258    fn resolve_type_for_topic(
259        &self,
260        type_name: &str,
261        topic_name: &str,
262        participant: &DomainParticipant,
263    ) -> Result<ResolvedType, BackendError> {
264        let dialect = participant
265            .runtime()
266            .map(|rt| rt.codec_dialect())
267            .unwrap_or_default();
268        self.resolve_type_in_registry(type_name, topic_name, participant)
269            .map(|resolved| ResolvedType {
270                support: resolved.support.in_dialect(dialect),
271                ..resolved
272            })
273    }
274
275    fn resolve_type_in_registry(
276        &self,
277        type_name: &str,
278        topic_name: &str,
279        participant: &DomainParticipant,
280    ) -> Result<ResolvedType, BackendError> {
281        if let Some(r) = self.types.get(type_name) {
282            return Ok(r);
283        }
284        if let Some(source) = self.type_sources.get(type_name) {
285            return match source {
286                TypeSource::Idl(path) => self.types.load_idl_file(path, type_name),
287                TypeSource::Xml(path) => self.types.load_xml_file(path, type_name),
288            }
289            .map_err(|e| BackendError::Internal(e.to_string()));
290        }
291        let Some(runtime) = participant.runtime() else {
292            return Err(BackendError::BadRequest(format!(
293                "type `{type_name}` is not in this daemon's config, and this participant has \
294                 no live runtime to discover it via TypeLookup"
295            )));
296        };
297        self.types
298            .resolve_named(runtime, topic_name, type_name, TYPE_LOOKUP_TIMEOUT)
299            .map_err(|e| {
300                BackendError::BadRequest(format!(
301                    "type `{type_name}` for topic `{topic_name}`: {e}"
302                ))
303            })
304    }
305
306    /// The waitset `waitset` of application `app_name`, to wait on after
307    /// the caller let go of the gateway.
308    ///
309    /// # Errors
310    /// [`BackendError::NotFound`] for an unknown application or waitset.
311    pub fn waitset_handle(
312        &self,
313        app_name: &str,
314        waitset: &str,
315    ) -> Result<WaitsetHandle, BackendError> {
316        self.with_apps(|apps| {
317            let app = apps
318                .get(app_name)
319                .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
320            let entry = app
321                .waitsets
322                .get(waitset)
323                .ok_or_else(|| BackendError::NotFound(waitset.to_string()))?;
324            Ok(WaitsetHandle(Arc::clone(entry)))
325        })
326    }
327
328    fn with_apps<R>(&self, f: impl FnOnce(&mut BTreeMap<String, AppEntry>) -> R) -> R {
329        let mut apps = self
330            .apps
331            .lock()
332            .unwrap_or_else(std::sync::PoisonError::into_inner);
333        f(&mut apps)
334    }
335}
336
337/// Encodes a `DynamicData` sample of `resolved`'s type to wire bytes and
338/// validates it into a `SerializedSample`, ready for `DataWriter::write`.
339fn encode_sample(resolved: &ResolvedType, xml: &str) -> Result<SerializedSample, BackendError> {
340    let value = dynxml::from_xml_document(xml, &resolved.dynamic_type)
341        .map_err(|e| BackendError::BadRequest(e.to_string()))?;
342    let request = probe_request(resolved);
343    let bytes = zerodds_wire_codec::encode(resolved.dynamic_type.ast(), &request, &value)
344        .map_err(|e| BackendError::BadRequest(format!("{e:?}")))?;
345    resolved
346        .support
347        .read_body(&bytes, 2, false)
348        .map_err(BackendError::BadRequest)
349}
350
351/// Decodes a `SerializedSample`'s wire body back to XML through the
352/// type's own `DynamicType` descriptor. Uses the sample's own
353/// representation/byte order (a reader may see either of a writer's
354/// offer), not the fixed [`probe_request`] a write always uses.
355fn sample_to_xml(
356    resolved: &ResolvedType,
357    sample: &SerializedSample,
358) -> Result<String, BackendError> {
359    let request = probe_request_for(sample, resolved);
360    let value: zerodds_types::dynamic::DynamicData =
361        zerodds_wire_codec::decode(resolved.dynamic_type.ast(), &request, sample.body())
362            .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
363    dynxml::to_xml_document("data", &value, &resolved.dynamic_type)
364        .map_err(|e| BackendError::Internal(e.to_string()))
365}
366
367fn probe_request_for(sample: &SerializedSample, resolved: &ResolvedType) -> ProbeRequest {
368    ProbeRequest::in_dialect(
369        &resolved.dynamic_type.qualified_name(),
370        if sample.representation() == 0 {
371            Encoding::PlainCdr1
372        } else {
373            Encoding::PlainCdr2
374        },
375        if sample.big_endian() {
376            Endianness::Big
377        } else {
378            Endianness::Little
379        },
380        resolved.support.dialect(),
381    )
382}
383
384/// Splits `app/participant[/...]` style paths this trait's callers hand
385/// back in. Not used at the trait boundary (the trait already takes
386/// `app`/`participant` as separate strings) — kept for the WaitSet
387/// reader-path bookkeeping, which stores composite paths as opaque
388/// strings.
389fn writer_path(app: &str, participant: &str, publisher: &str, writer: &str) -> String {
390    format!(
391        "applications/{app}/domain_participants/{participant}/publishers/{publisher}/data_writers/{writer}"
392    )
393}
394fn reader_path(app: &str, participant: &str, subscriber: &str, reader: &str) -> String {
395    format!(
396        "applications/{app}/domain_participants/{participant}/subscribers/{subscriber}/data_readers/{reader}"
397    )
398}
399
400impl DdsBackend for Gateway {
401    fn create_application(&mut self, app_name: &str) -> Result<BackendResult, BackendError> {
402        self.with_apps(|apps| {
403            if apps.contains_key(app_name) {
404                return Err(BackendError::Conflict(app_name.to_string()));
405            }
406            apps.insert(app_name.to_string(), AppEntry::default());
407            Ok(BackendResult::Created(format!("applications/{app_name}")))
408        })
409    }
410
411    fn delete_application(&mut self, app_name: &str) -> Result<BackendResult, BackendError> {
412        self.with_apps(|apps| {
413            if apps.remove(app_name).is_none() {
414                return Err(BackendError::NotFound(app_name.to_string()));
415            }
416            Ok(BackendResult::Deleted)
417        })
418    }
419
420    fn list_applications(&self, pattern: &str) -> Result<BackendResult, BackendError> {
421        let apps = self
422            .apps
423            .lock()
424            .unwrap_or_else(std::sync::PoisonError::into_inner);
425        let names = apps
426            .keys()
427            .filter(|n| pattern == "*" || pattern.is_empty() || n.as_str() == pattern)
428            .cloned()
429            .collect();
430        Ok(BackendResult::List(names))
431    }
432
433    fn create_participant(
434        &mut self,
435        app_name: &str,
436        domain_id: u32,
437        body: &[u8],
438    ) -> Result<BackendResult, BackendError> {
439        let doc = crate::contained_entities::parse_body(body)?;
440        let qos_eq = match &doc {
441            Some(doc) => crate::qos_update::resolve_entity_qos_from_element(
442                &doc.root,
443                &self.metadata,
444                EntityKind::DomainParticipant,
445            )?,
446            None => None,
447        };
448        let publishers = match &doc {
449            Some(doc) => crate::contained_entities::parse_publishers(&doc.root, &self.metadata)?,
450            None => Vec::new(),
451        };
452        let subscribers = match &doc {
453            Some(doc) => crate::contained_entities::parse_subscribers(&doc.root, &self.metadata)?,
454            None => Vec::new(),
455        };
456        self.with_apps(|apps| {
457            let app = apps
458                .get_mut(app_name)
459                .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
460            let mut qos = DomainParticipantQos::default();
461            if let Some(eq) = &qos_eq {
462                qos = crate::qos_update::participant_qos_from(eq, qos);
463            }
464            let participant = DomainParticipantFactory::instance()
465                .create_participant(
466                    i32::try_from(domain_id).map_err(|_| {
467                        BackendError::BadRequest(format!(
468                            "domain_id {domain_id} is not a domain id"
469                        ))
470                    })?,
471                    qos,
472                )
473                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
474            app.next_participant += 1;
475            let name = format!("p{}", app.next_participant);
476            let mut entry = ParticipantEntry {
477                participant,
478                topics: BTreeMap::new(),
479                publishers: BTreeMap::new(),
480                subscribers: BTreeMap::new(),
481            };
482            sync_publishers(
483                &mut entry.publishers,
484                &entry.participant,
485                &entry.topics,
486                &self.types,
487                &publishers,
488            )?;
489            sync_subscribers(
490                &mut entry.subscribers,
491                &entry.participant,
492                &entry.topics,
493                &self.types,
494                &subscribers,
495            )?;
496            app.participants.insert(name.clone(), entry);
497            Ok(BackendResult::Created(format!(
498                "applications/{app_name}/domain_participants/{name}"
499            )))
500        })
501    }
502
503    fn update_participant(
504        &mut self,
505        app_name: &str,
506        participant: &str,
507        body: &[u8],
508    ) -> Result<BackendResult, BackendError> {
509        let Some(doc) = crate::contained_entities::parse_body(body)? else {
510            return self.with_apps(|apps| {
511                participant_mut(apps, app_name, participant)?;
512                Ok(BackendResult::Deleted)
513            });
514        };
515        let qos_eq = crate::qos_update::resolve_entity_qos_from_element(
516            &doc.root,
517            &self.metadata,
518            EntityKind::DomainParticipant,
519        )?;
520        // Absent from the body at all (not merely empty) means "leave
521        // it as it is" — Spec: "If the participantObjectRepresentation
522        // specifies a set of contained entities, then..." An empty
523        // `<publishers/>` is instead a real "delete every publisher".
524        let has_publishers = doc.root.child("publishers").is_some();
525        let has_subscribers = doc.root.child("subscribers").is_some();
526        let publishers = crate::contained_entities::parse_publishers(&doc.root, &self.metadata)?;
527        let subscribers = crate::contained_entities::parse_subscribers(&doc.root, &self.metadata)?;
528        self.with_apps(|apps| {
529            let part = participant_mut(apps, app_name, participant)?;
530            if let Some(eq) = &qos_eq {
531                let qos = crate::qos_update::participant_qos_from(eq, part.participant.get_qos());
532                part.participant
533                    .set_qos(qos)
534                    .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
535            }
536            if has_publishers {
537                sync_publishers(
538                    &mut part.publishers,
539                    &part.participant,
540                    &part.topics,
541                    &self.types,
542                    &publishers,
543                )?;
544            }
545            if has_subscribers {
546                sync_subscribers(
547                    &mut part.subscribers,
548                    &part.participant,
549                    &part.topics,
550                    &self.types,
551                    &subscribers,
552                )?;
553            }
554            Ok(BackendResult::Deleted)
555        })
556    }
557
558    fn delete_participant(
559        &mut self,
560        app_name: &str,
561        participant: &str,
562    ) -> Result<BackendResult, BackendError> {
563        self.with_apps(|apps| {
564            let app = apps
565                .get_mut(app_name)
566                .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
567            if app.participants.remove(participant).is_none() {
568                return Err(BackendError::NotFound(participant.to_string()));
569            }
570            Ok(BackendResult::Deleted)
571        })
572    }
573
574    fn list_participants(&self, app_name: &str) -> Result<BackendResult, BackendError> {
575        let apps = self
576            .apps
577            .lock()
578            .unwrap_or_else(std::sync::PoisonError::into_inner);
579        let app = apps
580            .get(app_name)
581            .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
582        Ok(BackendResult::List(
583            app.participants.keys().cloned().collect(),
584        ))
585    }
586
587    fn create_topic(
588        &mut self,
589        app_name: &str,
590        participant: &str,
591        topic_name: &str,
592        type_name: &str,
593    ) -> Result<BackendResult, BackendError> {
594        // Cloned without holding `self.apps`'s lock: `resolve_type_for_topic`
595        // may block up to `TYPE_LOOKUP_TIMEOUT` on a `getTypes` round trip
596        // when the type is not already known, and holding the apps lock
597        // across that would stall every other request in the meantime.
598        // `DomainParticipant` is `Clone` (a handle onto shared state).
599        let participant_handle = self.with_apps(|apps| {
600            participant_ref(apps, app_name, participant).map(|p| p.participant.clone())
601        })?;
602        let resolved = self.resolve_type_for_topic(type_name, topic_name, &participant_handle)?;
603        self.with_apps(|apps| {
604            let part = participant_mut(apps, app_name, participant)?;
605            if part.topics.contains_key(topic_name) {
606                return Err(BackendError::Conflict(topic_name.to_string()));
607            }
608            let topic = part
609                .participant
610                .create_serialized_topic(
611                    topic_name,
612                    TopicQos::default(),
613                    Arc::clone(&resolved.support),
614                )
615                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
616            part.topics.insert(topic_name.to_string(), topic);
617            self.types.insert(type_name, resolved.clone());
618            Ok(BackendResult::Created(format!(
619                "applications/{app_name}/domain_participants/{participant}/topics/{topic_name}"
620            )))
621        })
622    }
623
624    fn update_topic(
625        &mut self,
626        app_name: &str,
627        participant: &str,
628        topic: &str,
629        body: &[u8],
630    ) -> Result<BackendResult, BackendError> {
631        // The topic name is at hand, so the profile's §7.3.2.4.3
632        // `topic_filter`s are evaluated against it.
633        let Some(eq) = crate::qos_update::resolve_entity_qos_for_topic(
634            body,
635            &self.metadata,
636            EntityKind::Topic,
637            topic,
638        )?
639        else {
640            return self.with_apps(|apps| {
641                let part = participant_mut(apps, app_name, participant)?;
642                if part.topics.contains_key(topic) {
643                    Ok(BackendResult::Deleted)
644                } else {
645                    Err(BackendError::NotFound(topic.to_string()))
646                }
647            });
648        };
649        self.with_apps(|apps| {
650            let part = participant_mut(apps, app_name, participant)?;
651            let topic_handle = part
652                .topics
653                .get(topic)
654                .ok_or_else(|| BackendError::NotFound(topic.to_string()))?;
655            let qos = crate::qos_update::topic_qos_from(&eq, topic_handle.get_qos());
656            topic_handle
657                .set_qos(qos)
658                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
659            Ok(BackendResult::Deleted)
660        })
661    }
662
663    fn delete_topic(
664        &mut self,
665        app_name: &str,
666        participant: &str,
667        topic: &str,
668    ) -> Result<BackendResult, BackendError> {
669        self.with_apps(|apps| {
670            let part = participant_mut(apps, app_name, participant)?;
671            if part.topics.remove(topic).is_none() {
672                return Err(BackendError::NotFound(topic.to_string()));
673            }
674            Ok(BackendResult::Deleted)
675        })
676    }
677
678    fn list_topics(
679        &self,
680        app_name: &str,
681        participant: &str,
682    ) -> Result<BackendResult, BackendError> {
683        let apps = self
684            .apps
685            .lock()
686            .unwrap_or_else(std::sync::PoisonError::into_inner);
687        let part = participant_ref(&apps, app_name, participant)?;
688        Ok(BackendResult::List(part.topics.keys().cloned().collect()))
689    }
690
691    fn create_publisher(
692        &mut self,
693        app_name: &str,
694        participant: &str,
695        body: &[u8],
696    ) -> Result<BackendResult, BackendError> {
697        let doc = crate::contained_entities::parse_body(body)?;
698        let qos_eq = match &doc {
699            Some(doc) => crate::qos_update::resolve_entity_qos_from_element(
700                &doc.root,
701                &self.metadata,
702                EntityKind::Publisher,
703            )?,
704            None => None,
705        };
706        let data_writers = match &doc {
707            Some(doc) => crate::contained_entities::parse_data_writers(&doc.root, &self.metadata)?,
708            None => Vec::new(),
709        };
710        self.with_apps(|apps| {
711            let app_entry_next = {
712                let app = apps
713                    .get_mut(app_name)
714                    .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
715                app.next_publisher += 1;
716                app.next_publisher
717            };
718            let part = participant_mut(apps, app_name, participant)?;
719            let mut qos = PublisherQos::default();
720            if let Some(eq) = &qos_eq {
721                qos = crate::qos_update::publisher_qos_from(eq, qos);
722            }
723            let publisher = part.participant.create_publisher(qos);
724            let name = format!("pub{app_entry_next}");
725            let mut writers = BTreeMap::new();
726            sync_data_writers(
727                &mut writers,
728                &publisher,
729                &part.topics,
730                &self.types,
731                &data_writers,
732            )?;
733            part.publishers
734                .insert(name.clone(), PublisherEntry { publisher, writers });
735            Ok(BackendResult::Created(format!(
736                "applications/{app_name}/domain_participants/{participant}/publishers/{name}"
737            )))
738        })
739    }
740
741    fn update_publisher(
742        &mut self,
743        app_name: &str,
744        participant: &str,
745        publisher: &str,
746        body: &[u8],
747    ) -> Result<BackendResult, BackendError> {
748        let Some(doc) = crate::contained_entities::parse_body(body)? else {
749            return self.with_apps(|apps| {
750                let part = participant_mut(apps, app_name, participant)?;
751                if part.publishers.contains_key(publisher) {
752                    Ok(BackendResult::Deleted)
753                } else {
754                    Err(BackendError::NotFound(publisher.to_string()))
755                }
756            });
757        };
758        let qos_eq = crate::qos_update::resolve_entity_qos_from_element(
759            &doc.root,
760            &self.metadata,
761            EntityKind::Publisher,
762        )?;
763        let has_data_writers = doc.root.child("data_writers").is_some();
764        let data_writers =
765            crate::contained_entities::parse_data_writers(&doc.root, &self.metadata)?;
766        self.with_apps(|apps| {
767            let part = participant_mut(apps, app_name, participant)?;
768            let topics = part.topics.clone();
769            let pub_entry = part
770                .publishers
771                .get_mut(publisher)
772                .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
773            if let Some(eq) = &qos_eq {
774                let qos = crate::qos_update::publisher_qos_from(eq, pub_entry.publisher.get_qos());
775                pub_entry
776                    .publisher
777                    .set_qos(qos)
778                    .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
779            }
780            if has_data_writers {
781                sync_data_writers(
782                    &mut pub_entry.writers,
783                    &pub_entry.publisher,
784                    &topics,
785                    &self.types,
786                    &data_writers,
787                )?;
788            }
789            Ok(BackendResult::Deleted)
790        })
791    }
792
793    fn delete_publisher(
794        &mut self,
795        app_name: &str,
796        participant: &str,
797        publisher: &str,
798    ) -> Result<BackendResult, BackendError> {
799        self.with_apps(|apps| {
800            let part = participant_mut(apps, app_name, participant)?;
801            if part.publishers.remove(publisher).is_none() {
802                return Err(BackendError::NotFound(publisher.to_string()));
803            }
804            Ok(BackendResult::Deleted)
805        })
806    }
807
808    fn list_publishers(
809        &self,
810        app_name: &str,
811        participant: &str,
812    ) -> Result<BackendResult, BackendError> {
813        let apps = self
814            .apps
815            .lock()
816            .unwrap_or_else(std::sync::PoisonError::into_inner);
817        let part = participant_ref(&apps, app_name, participant)?;
818        Ok(BackendResult::List(
819            part.publishers.keys().cloned().collect(),
820        ))
821    }
822
823    fn create_subscriber(
824        &mut self,
825        app_name: &str,
826        participant: &str,
827        body: &[u8],
828    ) -> Result<BackendResult, BackendError> {
829        let doc = crate::contained_entities::parse_body(body)?;
830        let qos_eq = match &doc {
831            Some(doc) => crate::qos_update::resolve_entity_qos_from_element(
832                &doc.root,
833                &self.metadata,
834                EntityKind::Subscriber,
835            )?,
836            None => None,
837        };
838        let data_readers = match &doc {
839            Some(doc) => crate::contained_entities::parse_data_readers(&doc.root, &self.metadata)?,
840            None => Vec::new(),
841        };
842        self.with_apps(|apps| {
843            let app_entry_next = {
844                let app = apps
845                    .get_mut(app_name)
846                    .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
847                app.next_subscriber += 1;
848                app.next_subscriber
849            };
850            let part = participant_mut(apps, app_name, participant)?;
851            let mut qos = SubscriberQos::default();
852            if let Some(eq) = &qos_eq {
853                qos = crate::qos_update::subscriber_qos_from(eq, qos);
854            }
855            let subscriber = part.participant.create_subscriber(qos);
856            let name = format!("sub{app_entry_next}");
857            let mut readers = BTreeMap::new();
858            sync_data_readers(
859                &mut readers,
860                &subscriber,
861                &part.topics,
862                &self.types,
863                &data_readers,
864            )?;
865            part.subscribers.insert(
866                name.clone(),
867                SubscriberEntry {
868                    subscriber,
869                    readers,
870                },
871            );
872            Ok(BackendResult::Created(format!(
873                "applications/{app_name}/domain_participants/{participant}/subscribers/{name}"
874            )))
875        })
876    }
877
878    fn update_subscriber(
879        &mut self,
880        app_name: &str,
881        participant: &str,
882        subscriber: &str,
883        body: &[u8],
884    ) -> Result<BackendResult, BackendError> {
885        let Some(doc) = crate::contained_entities::parse_body(body)? else {
886            return self.with_apps(|apps| {
887                let part = participant_mut(apps, app_name, participant)?;
888                if part.subscribers.contains_key(subscriber) {
889                    Ok(BackendResult::Deleted)
890                } else {
891                    Err(BackendError::NotFound(subscriber.to_string()))
892                }
893            });
894        };
895        let qos_eq = crate::qos_update::resolve_entity_qos_from_element(
896            &doc.root,
897            &self.metadata,
898            EntityKind::Subscriber,
899        )?;
900        let has_data_readers = doc.root.child("data_readers").is_some();
901        let data_readers =
902            crate::contained_entities::parse_data_readers(&doc.root, &self.metadata)?;
903        self.with_apps(|apps| {
904            let part = participant_mut(apps, app_name, participant)?;
905            let topics = part.topics.clone();
906            let sub_entry = part
907                .subscribers
908                .get_mut(subscriber)
909                .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
910            if let Some(eq) = &qos_eq {
911                let qos =
912                    crate::qos_update::subscriber_qos_from(eq, sub_entry.subscriber.get_qos());
913                sub_entry
914                    .subscriber
915                    .set_qos(qos)
916                    .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
917            }
918            if has_data_readers {
919                sync_data_readers(
920                    &mut sub_entry.readers,
921                    &sub_entry.subscriber,
922                    &topics,
923                    &self.types,
924                    &data_readers,
925                )?;
926            }
927            Ok(BackendResult::Deleted)
928        })
929    }
930
931    fn delete_subscriber(
932        &mut self,
933        app_name: &str,
934        participant: &str,
935        subscriber: &str,
936    ) -> Result<BackendResult, BackendError> {
937        self.with_apps(|apps| {
938            let part = participant_mut(apps, app_name, participant)?;
939            if part.subscribers.remove(subscriber).is_none() {
940                return Err(BackendError::NotFound(subscriber.to_string()));
941            }
942            Ok(BackendResult::Deleted)
943        })
944    }
945
946    fn list_subscribers(
947        &self,
948        app_name: &str,
949        participant: &str,
950    ) -> Result<BackendResult, BackendError> {
951        let apps = self
952            .apps
953            .lock()
954            .unwrap_or_else(std::sync::PoisonError::into_inner);
955        let part = participant_ref(&apps, app_name, participant)?;
956        Ok(BackendResult::List(
957            part.subscribers.keys().cloned().collect(),
958        ))
959    }
960
961    fn create_data_writer(
962        &mut self,
963        app_name: &str,
964        participant: &str,
965        publisher: &str,
966        topic: &str,
967    ) -> Result<BackendResult, BackendError> {
968        self.with_apps(|apps| {
969            let app_entry_next = {
970                let app = apps
971                    .get_mut(app_name)
972                    .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
973                app.next_writer += 1;
974                app.next_writer
975            };
976            let part = participant_mut(apps, app_name, participant)?;
977            let topic_handle = part
978                .topics
979                .get(topic)
980                .ok_or_else(|| BackendError::NotFound(topic.to_string()))?
981                .clone();
982            let type_name = topic_handle.type_name().to_string();
983            let resolved = self.types.get(&type_name).ok_or_else(|| {
984                BackendError::Internal(format!("type `{type_name}` not resolved"))
985            })?;
986            let pub_entry = part
987                .publishers
988                .get_mut(publisher)
989                .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
990            let writer = pub_entry
991                .publisher
992                .create_datawriter(&topic_handle, DataWriterQos::default())
993                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
994            let name = format!("dw{app_entry_next}");
995            pub_entry
996                .writers
997                .insert(name.clone(), WriterEntry { writer, resolved });
998            Ok(BackendResult::Created(writer_path(
999                app_name,
1000                participant,
1001                publisher,
1002                &name,
1003            )))
1004        })
1005    }
1006
1007    fn update_data_writer(
1008        &mut self,
1009        app_name: &str,
1010        participant: &str,
1011        publisher: &str,
1012        writer: &str,
1013        body: &[u8],
1014    ) -> Result<BackendResult, BackendError> {
1015        // The body carries no topic name here (the writer's own topic
1016        // lives on the DDS entity, not in this API shape), so the
1017        // profile's §7.3.2.4.3 slots narrow via the filterless
1018        // container, else the first one.
1019        let Some(eq) =
1020            crate::qos_update::resolve_entity_qos(body, &self.metadata, EntityKind::DataWriter)?
1021        else {
1022            return self.with_apps(|apps| {
1023                let part = participant_mut(apps, app_name, participant)?;
1024                let pub_entry = part
1025                    .publishers
1026                    .get(publisher)
1027                    .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
1028                if pub_entry.writers.contains_key(writer) {
1029                    Ok(BackendResult::Deleted)
1030                } else {
1031                    Err(BackendError::NotFound(writer.to_string()))
1032                }
1033            });
1034        };
1035        self.with_apps(|apps| {
1036            let part = participant_mut(apps, app_name, participant)?;
1037            let pub_entry = part
1038                .publishers
1039                .get(publisher)
1040                .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
1041            let writer_entry = pub_entry
1042                .writers
1043                .get(writer)
1044                .ok_or_else(|| BackendError::NotFound(writer.to_string()))?;
1045            let qos = crate::qos_update::writer_qos_from(&eq, writer_entry.writer.get_qos());
1046            writer_entry
1047                .writer
1048                .set_qos(qos)
1049                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1050            Ok(BackendResult::Deleted)
1051        })
1052    }
1053
1054    fn delete_data_writer(
1055        &mut self,
1056        app_name: &str,
1057        participant: &str,
1058        publisher: &str,
1059        writer: &str,
1060    ) -> Result<BackendResult, BackendError> {
1061        self.with_apps(|apps| {
1062            let part = participant_mut(apps, app_name, participant)?;
1063            let pub_entry = part
1064                .publishers
1065                .get_mut(publisher)
1066                .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
1067            if pub_entry.writers.remove(writer).is_none() {
1068                return Err(BackendError::NotFound(writer.to_string()));
1069            }
1070            Ok(BackendResult::Deleted)
1071        })
1072    }
1073
1074    fn list_data_writers(
1075        &self,
1076        app_name: &str,
1077        participant: &str,
1078        publisher: &str,
1079    ) -> Result<BackendResult, BackendError> {
1080        let apps = self
1081            .apps
1082            .lock()
1083            .unwrap_or_else(std::sync::PoisonError::into_inner);
1084        let part = participant_ref(&apps, app_name, participant)?;
1085        let pub_entry = part
1086            .publishers
1087            .get(publisher)
1088            .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
1089        Ok(BackendResult::List(
1090            pub_entry.writers.keys().cloned().collect(),
1091        ))
1092    }
1093
1094    fn create_data_reader(
1095        &mut self,
1096        app_name: &str,
1097        participant: &str,
1098        subscriber: &str,
1099        topic: &str,
1100    ) -> Result<BackendResult, BackendError> {
1101        self.with_apps(|apps| {
1102            let app_entry_next = {
1103                let app = apps
1104                    .get_mut(app_name)
1105                    .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
1106                app.next_reader += 1;
1107                app.next_reader
1108            };
1109            let part = participant_mut(apps, app_name, participant)?;
1110            let topic_handle = part
1111                .topics
1112                .get(topic)
1113                .ok_or_else(|| BackendError::NotFound(topic.to_string()))?
1114                .clone();
1115            let type_name = topic_handle.type_name().to_string();
1116            let resolved = self.types.get(&type_name).ok_or_else(|| {
1117                BackendError::Internal(format!("type `{type_name}` not resolved"))
1118            })?;
1119            let sub_entry = part
1120                .subscribers
1121                .get_mut(subscriber)
1122                .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
1123            let reader = sub_entry
1124                .subscriber
1125                .create_datareader(&topic_handle, DataReaderQos::default())
1126                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1127            let name = format!("dr{app_entry_next}");
1128            sub_entry.readers.insert(
1129                name.clone(),
1130                ReaderEntry {
1131                    reader: Arc::new(reader),
1132                    resolved,
1133                },
1134            );
1135            Ok(BackendResult::Created(reader_path(
1136                app_name,
1137                participant,
1138                subscriber,
1139                &name,
1140            )))
1141        })
1142    }
1143
1144    fn update_data_reader(
1145        &mut self,
1146        app_name: &str,
1147        participant: &str,
1148        subscriber: &str,
1149        reader: &str,
1150        body: &[u8],
1151    ) -> Result<BackendResult, BackendError> {
1152        // Same slot narrowing as `update_data_writer`: no topic name
1153        // in the body, so the filterless container, else the first.
1154        let Some(eq) =
1155            crate::qos_update::resolve_entity_qos(body, &self.metadata, EntityKind::DataReader)?
1156        else {
1157            return self.with_apps(|apps| {
1158                let part = participant_mut(apps, app_name, participant)?;
1159                let sub_entry = part
1160                    .subscribers
1161                    .get(subscriber)
1162                    .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
1163                if sub_entry.readers.contains_key(reader) {
1164                    Ok(BackendResult::Deleted)
1165                } else {
1166                    Err(BackendError::NotFound(reader.to_string()))
1167                }
1168            });
1169        };
1170        self.with_apps(|apps| {
1171            let part = participant_mut(apps, app_name, participant)?;
1172            let sub_entry = part
1173                .subscribers
1174                .get(subscriber)
1175                .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
1176            let reader_entry = sub_entry
1177                .readers
1178                .get(reader)
1179                .ok_or_else(|| BackendError::NotFound(reader.to_string()))?;
1180            let qos = crate::qos_update::reader_qos_from(&eq, reader_entry.reader.get_qos());
1181            reader_entry
1182                .reader
1183                .set_qos(qos)
1184                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1185            Ok(BackendResult::Deleted)
1186        })
1187    }
1188
1189    fn delete_data_reader(
1190        &mut self,
1191        app_name: &str,
1192        participant: &str,
1193        subscriber: &str,
1194        reader: &str,
1195    ) -> Result<BackendResult, BackendError> {
1196        self.with_apps(|apps| {
1197            let part = participant_mut(apps, app_name, participant)?;
1198            let sub_entry = part
1199                .subscribers
1200                .get_mut(subscriber)
1201                .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
1202            if sub_entry.readers.remove(reader).is_none() {
1203                return Err(BackendError::NotFound(reader.to_string()));
1204            }
1205            Ok(BackendResult::Deleted)
1206        })
1207    }
1208
1209    fn list_data_readers(
1210        &self,
1211        app_name: &str,
1212        participant: &str,
1213        subscriber: &str,
1214    ) -> Result<BackendResult, BackendError> {
1215        let apps = self
1216            .apps
1217            .lock()
1218            .unwrap_or_else(std::sync::PoisonError::into_inner);
1219        let part = participant_ref(&apps, app_name, participant)?;
1220        let sub_entry = part
1221            .subscribers
1222            .get(subscriber)
1223            .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
1224        Ok(BackendResult::List(
1225            sub_entry.readers.keys().cloned().collect(),
1226        ))
1227    }
1228
1229    fn write_sample(
1230        &mut self,
1231        app_name: &str,
1232        participant: &str,
1233        publisher: &str,
1234        writer: &str,
1235        body: &[u8],
1236    ) -> Result<BackendResult, BackendError> {
1237        let xml = std::str::from_utf8(body)
1238            .map_err(|e| BackendError::BadRequest(format!("body is not UTF-8: {e}")))?;
1239        self.with_apps(|apps| {
1240            let part = participant_mut(apps, app_name, participant)?;
1241            let pub_entry = part
1242                .publishers
1243                .get_mut(publisher)
1244                .ok_or_else(|| BackendError::NotFound(publisher.to_string()))?;
1245            let writer_entry = pub_entry
1246                .writers
1247                .get(writer)
1248                .ok_or_else(|| BackendError::NotFound(writer.to_string()))?;
1249            let sample = encode_sample(&writer_entry.resolved, xml)?;
1250            writer_entry
1251                .writer
1252                .write(&sample)
1253                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1254            Ok(BackendResult::Deleted)
1255        })
1256    }
1257
1258    fn read_samples(
1259        &mut self,
1260        app_name: &str,
1261        participant: &str,
1262        subscriber: &str,
1263        reader: &str,
1264        _selector: Option<&str>,
1265    ) -> Result<BackendResult, BackendError> {
1266        self.with_apps(|apps| {
1267            let part = participant_mut(apps, app_name, participant)?;
1268            let sub_entry = part
1269                .subscribers
1270                .get_mut(subscriber)
1271                .ok_or_else(|| BackendError::NotFound(subscriber.to_string()))?;
1272            let reader_entry = sub_entry
1273                .readers
1274                .get(reader)
1275                .ok_or_else(|| BackendError::NotFound(reader.to_string()))?;
1276            let samples = reader_entry
1277                .reader
1278                .read()
1279                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1280            let mut emitter = zerodds_xml_wire::emitter::XmlEmitter::new();
1281            emitter.declaration();
1282            emitter
1283                .start_element(
1284                    zerodds_web::representation::element_name(
1285                        zerodds_web::representation::ContentType::ReadSampleSeq,
1286                        zerodds_web::representation::RepresentationKind::List,
1287                    ),
1288                    &[],
1289                )
1290                .map_err(|e| BackendError::Internal(e.to_string()))?;
1291            let mut body = emitter.finish();
1292            for sample in &samples {
1293                let xml = sample_to_xml(&reader_entry.resolved, sample)?;
1294                // Strip the `<?xml ...?>` declaration each per-sample
1295                // document carries — only the outer document needs one.
1296                let stripped = xml.split_once("?>").map(|(_, rest)| rest).unwrap_or(&xml);
1297                body.push_str(stripped);
1298            }
1299            body.push_str(&format!(
1300                "</{}>",
1301                zerodds_web::representation::element_name(
1302                    zerodds_web::representation::ContentType::ReadSampleSeq,
1303                    zerodds_web::representation::RepresentationKind::List,
1304                )
1305            ));
1306            Ok(BackendResult::Body {
1307                content_type: zerodds_web::representation::CONTENT_TYPE_DDS_WEB_XML.to_string(),
1308                body: body.into_bytes(),
1309            })
1310        })
1311    }
1312
1313    fn create_waitset(
1314        &mut self,
1315        app_name: &str,
1316        waitset: &str,
1317        readers: &[String],
1318    ) -> Result<BackendResult, BackendError> {
1319        self.with_apps(|apps| {
1320            let app = apps
1321                .get_mut(app_name)
1322                .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
1323            if app.waitsets.contains_key(waitset) {
1324                return Err(BackendError::Conflict(waitset.to_string()));
1325            }
1326            let entry = build_waitset_entry(app, readers)?;
1327            app.waitsets.insert(waitset.to_string(), Arc::new(entry));
1328            Ok(BackendResult::Created(format!(
1329                "applications/{app_name}/waitsets/{waitset}"
1330            )))
1331        })
1332    }
1333
1334    fn update_waitset(
1335        &mut self,
1336        app_name: &str,
1337        waitset: &str,
1338        readers: &[String],
1339    ) -> Result<BackendResult, BackendError> {
1340        // Spec §7.4.3.5 `update_waitset`: "It updates the conditions
1341        // associated with the WebDDS::WaitSet" — unlike every other
1342        // `update_*`, no QoS is involved; `readers` fully replaces the
1343        // condition set, built the same way `create_waitset`'s is.
1344        self.with_apps(|apps| {
1345            let app = apps
1346                .get_mut(app_name)
1347                .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
1348            if !app.waitsets.contains_key(waitset) {
1349                return Err(BackendError::NotFound(waitset.to_string()));
1350            }
1351            let entry = build_waitset_entry(app, readers)?;
1352            app.waitsets.insert(waitset.to_string(), Arc::new(entry));
1353            Ok(BackendResult::Deleted)
1354        })
1355    }
1356
1357    fn wait_waitset(
1358        &mut self,
1359        app_name: &str,
1360        waitset: &str,
1361        timeout: Duration,
1362    ) -> Result<BackendResult, BackendError> {
1363        self.waitset_handle(app_name, waitset)?.wait(timeout)
1364    }
1365
1366    fn delete_waitset(
1367        &mut self,
1368        app_name: &str,
1369        waitset: &str,
1370    ) -> Result<BackendResult, BackendError> {
1371        self.with_apps(|apps| {
1372            let app = apps
1373                .get_mut(app_name)
1374                .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
1375            if app.waitsets.remove(waitset).is_none() {
1376                return Err(BackendError::NotFound(waitset.to_string()));
1377            }
1378            Ok(BackendResult::Deleted)
1379        })
1380    }
1381
1382    fn list_waitsets(&self, app_name: &str) -> Result<BackendResult, BackendError> {
1383        let apps = self
1384            .apps
1385            .lock()
1386            .unwrap_or_else(std::sync::PoisonError::into_inner);
1387        let app = apps
1388            .get(app_name)
1389            .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?;
1390        Ok(BackendResult::List(app.waitsets.keys().cloned().collect()))
1391    }
1392}
1393
1394/// Reconciles `writers` (a `Publisher`'s contained `DataWriter`s)
1395/// against `wanted` — Spec §7.4.4.8's rule for `update_publisher`,
1396/// reused as-is by `create_publisher`/`create_participant` (an empty
1397/// `writers` map makes every entry in `wanted` "does not yet exist"):
1398/// one already present (by name) has its QoS re-applied if `wanted`
1399/// named one; one absent from `wanted` is removed; one named in
1400/// `wanted` that does not yet exist is created against the topic it
1401/// names (which must already exist on the enclosing participant).
1402fn sync_data_writers(
1403    writers: &mut BTreeMap<String, WriterEntry>,
1404    publisher: &Publisher,
1405    topics: &BTreeMap<String, Topic<SerializedSample>>,
1406    types: &TypeRegistry,
1407    wanted: &[crate::contained_entities::ParsedDataWriter],
1408) -> Result<(), BackendError> {
1409    let wanted_names: std::collections::BTreeSet<&str> =
1410        wanted.iter().map(|w| w.name.as_str()).collect();
1411    writers.retain(|name, _| wanted_names.contains(name.as_str()));
1412    for w in wanted {
1413        if let Some(entry) = writers.get(&w.name) {
1414            if let Some(eq) = &w.qos {
1415                let qos = crate::qos_update::writer_qos_from(eq, entry.writer.get_qos());
1416                entry
1417                    .writer
1418                    .set_qos(qos)
1419                    .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1420            }
1421        } else {
1422            let topic_handle = topics.get(&w.topic_name).cloned().ok_or_else(|| {
1423                BackendError::BadRequest(format!(
1424                    "data_writer `{}`: unknown topic `{}`",
1425                    w.name, w.topic_name
1426                ))
1427            })?;
1428            let type_name = topic_handle.type_name().to_string();
1429            let resolved = types.get(&type_name).ok_or_else(|| {
1430                BackendError::Internal(format!("type `{type_name}` not resolved"))
1431            })?;
1432            let mut qos = DataWriterQos::default();
1433            if let Some(eq) = &w.qos {
1434                qos = crate::qos_update::writer_qos_from(eq, qos);
1435            }
1436            let writer = publisher
1437                .create_datawriter(&topic_handle, qos)
1438                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1439            writers.insert(w.name.clone(), WriterEntry { writer, resolved });
1440        }
1441    }
1442    Ok(())
1443}
1444
1445/// Reconciles `readers` (a `Subscriber`'s contained `DataReader`s)
1446/// against `wanted`. See [`sync_data_writers`].
1447fn sync_data_readers(
1448    readers: &mut BTreeMap<String, ReaderEntry>,
1449    subscriber: &Subscriber,
1450    topics: &BTreeMap<String, Topic<SerializedSample>>,
1451    types: &TypeRegistry,
1452    wanted: &[crate::contained_entities::ParsedDataReader],
1453) -> Result<(), BackendError> {
1454    let wanted_names: std::collections::BTreeSet<&str> =
1455        wanted.iter().map(|r| r.name.as_str()).collect();
1456    readers.retain(|name, _| wanted_names.contains(name.as_str()));
1457    for r in wanted {
1458        if let Some(entry) = readers.get(&r.name) {
1459            if let Some(eq) = &r.qos {
1460                let qos = crate::qos_update::reader_qos_from(eq, entry.reader.get_qos());
1461                entry
1462                    .reader
1463                    .set_qos(qos)
1464                    .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1465            }
1466        } else {
1467            let topic_handle = topics.get(&r.topic_name).cloned().ok_or_else(|| {
1468                BackendError::BadRequest(format!(
1469                    "data_reader `{}`: unknown topic `{}`",
1470                    r.name, r.topic_name
1471                ))
1472            })?;
1473            let type_name = topic_handle.type_name().to_string();
1474            let resolved = types.get(&type_name).ok_or_else(|| {
1475                BackendError::Internal(format!("type `{type_name}` not resolved"))
1476            })?;
1477            let mut qos = DataReaderQos::default();
1478            if let Some(eq) = &r.qos {
1479                qos = crate::qos_update::reader_qos_from(eq, qos);
1480            }
1481            let reader = subscriber
1482                .create_datareader(&topic_handle, qos)
1483                .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1484            readers.insert(
1485                r.name.clone(),
1486                ReaderEntry {
1487                    reader: Arc::new(reader),
1488                    resolved,
1489                },
1490            );
1491        }
1492    }
1493    Ok(())
1494}
1495
1496/// Reconciles `publishers` (a `DomainParticipant`'s contained
1497/// `Publisher`s) against `wanted`, recursing into each one's own
1498/// contained `DataWriter`s ([`sync_data_writers`]). See
1499/// [`sync_data_writers`] for the add/re-QoS/remove rule.
1500fn sync_publishers(
1501    publishers: &mut BTreeMap<String, PublisherEntry>,
1502    participant: &DomainParticipant,
1503    topics: &BTreeMap<String, Topic<SerializedSample>>,
1504    types: &TypeRegistry,
1505    wanted: &[crate::contained_entities::ParsedPublisher],
1506) -> Result<(), BackendError> {
1507    let wanted_names: std::collections::BTreeSet<&str> =
1508        wanted.iter().map(|p| p.name.as_str()).collect();
1509    publishers.retain(|name, _| wanted_names.contains(name.as_str()));
1510    for p in wanted {
1511        let entry = match publishers.entry(p.name.clone()) {
1512            std::collections::btree_map::Entry::Occupied(occ) => {
1513                if let Some(eq) = &p.qos {
1514                    let qos =
1515                        crate::qos_update::publisher_qos_from(eq, occ.get().publisher.get_qos());
1516                    occ.get()
1517                        .publisher
1518                        .set_qos(qos)
1519                        .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1520                }
1521                occ.into_mut()
1522            }
1523            std::collections::btree_map::Entry::Vacant(vac) => {
1524                let mut qos = PublisherQos::default();
1525                if let Some(eq) = &p.qos {
1526                    qos = crate::qos_update::publisher_qos_from(eq, qos);
1527                }
1528                let publisher = participant.create_publisher(qos);
1529                vac.insert(PublisherEntry {
1530                    publisher,
1531                    writers: BTreeMap::new(),
1532                })
1533            }
1534        };
1535        sync_data_writers(
1536            &mut entry.writers,
1537            &entry.publisher,
1538            topics,
1539            types,
1540            &p.data_writers,
1541        )?;
1542    }
1543    Ok(())
1544}
1545
1546/// Reconciles `subscribers` (a `DomainParticipant`'s contained
1547/// `Subscriber`s) against `wanted`, recursing into each one's own
1548/// contained `DataReader`s ([`sync_data_readers`]). See
1549/// [`sync_publishers`].
1550fn sync_subscribers(
1551    subscribers: &mut BTreeMap<String, SubscriberEntry>,
1552    participant: &DomainParticipant,
1553    topics: &BTreeMap<String, Topic<SerializedSample>>,
1554    types: &TypeRegistry,
1555    wanted: &[crate::contained_entities::ParsedSubscriber],
1556) -> Result<(), BackendError> {
1557    let wanted_names: std::collections::BTreeSet<&str> =
1558        wanted.iter().map(|s| s.name.as_str()).collect();
1559    subscribers.retain(|name, _| wanted_names.contains(name.as_str()));
1560    for s in wanted {
1561        let entry = match subscribers.entry(s.name.clone()) {
1562            std::collections::btree_map::Entry::Occupied(occ) => {
1563                if let Some(eq) = &s.qos {
1564                    let qos =
1565                        crate::qos_update::subscriber_qos_from(eq, occ.get().subscriber.get_qos());
1566                    occ.get()
1567                        .subscriber
1568                        .set_qos(qos)
1569                        .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1570                }
1571                occ.into_mut()
1572            }
1573            std::collections::btree_map::Entry::Vacant(vac) => {
1574                let mut qos = SubscriberQos::default();
1575                if let Some(eq) = &s.qos {
1576                    qos = crate::qos_update::subscriber_qos_from(eq, qos);
1577                }
1578                let subscriber = participant.create_subscriber(qos);
1579                vac.insert(SubscriberEntry {
1580                    subscriber,
1581                    readers: BTreeMap::new(),
1582                })
1583            }
1584        };
1585        sync_data_readers(
1586            &mut entry.readers,
1587            &entry.subscriber,
1588            topics,
1589            types,
1590            &s.data_readers,
1591        )?;
1592    }
1593    Ok(())
1594}
1595
1596fn participant_mut<'a>(
1597    apps: &'a mut BTreeMap<String, AppEntry>,
1598    app_name: &str,
1599    participant: &str,
1600) -> Result<&'a mut ParticipantEntry, BackendError> {
1601    apps.get_mut(app_name)
1602        .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?
1603        .participants
1604        .get_mut(participant)
1605        .ok_or_else(|| BackendError::NotFound(participant.to_string()))
1606}
1607
1608fn participant_ref<'a>(
1609    apps: &'a BTreeMap<String, AppEntry>,
1610    app_name: &str,
1611    participant: &str,
1612) -> Result<&'a ParticipantEntry, BackendError> {
1613    apps.get(app_name)
1614        .ok_or_else(|| BackendError::NotFound(app_name.to_string()))?
1615        .participants
1616        .get(participant)
1617        .ok_or_else(|| BackendError::NotFound(participant.to_string()))
1618}
1619
1620/// Splits a `.../subscribers/<sub>/data_readers/<dr>` path (as returned
1621/// by `create_data_reader`) into `(app, participant, subscriber,
1622/// reader)`.
1623/// Builds a fresh [`WaitsetEntry`] — a new `DDS::WaitSet` with one
1624/// `ReadCondition` per reader path in `readers`, attached — shared by
1625/// `create_waitset` and `update_waitset` (Spec §7.4.3.4/§7.4.3.5): a
1626/// `WaitSet`'s conditions are always a full replacement, never an
1627/// incremental add.
1628fn build_waitset_entry(app: &AppEntry, readers: &[String]) -> Result<WaitsetEntry, BackendError> {
1629    let ws = WaitSet::new();
1630    let mut conditions = Vec::with_capacity(readers.len());
1631    for reader_path in readers {
1632        let (_, _, subscriber, reader_name) = split_reader_path(reader_path)
1633            .ok_or_else(|| BackendError::BadRequest(format!("not a reader path: {reader_path}")))?;
1634        let mut found = None;
1635        for part in app.participants.values() {
1636            if let Some(sub_entry) = part.subscribers.get(&subscriber) {
1637                if let Some(r) = sub_entry.readers.get(&reader_name) {
1638                    found = Some(Arc::clone(&r.reader));
1639                }
1640            }
1641        }
1642        let reader_handle = found.ok_or_else(|| BackendError::NotFound(reader_path.clone()))?;
1643        let condition = ReadCondition::new(0, 0, 0, move |_, _, _| {
1644            !reader_handle.read().unwrap_or_default().is_empty()
1645        });
1646        ws.attach_condition(Arc::clone(&condition) as Arc<dyn Condition>)
1647            .map_err(|e| BackendError::Internal(format!("{e:?}")))?;
1648        conditions.push((reader_path.clone(), condition));
1649    }
1650    Ok(WaitsetEntry {
1651        waitset: ws,
1652        conditions,
1653        turn: Mutex::new(()),
1654    })
1655}
1656
1657fn split_reader_path(path: &str) -> Option<(String, String, String, String)> {
1658    let segments: Vec<&str> = path.trim_matches('/').split('/').collect();
1659    if segments.len() != 8 || segments[0] != "applications" || segments[2] != "domain_participants"
1660    {
1661        return None;
1662    }
1663    Some((
1664        segments[1].to_string(),
1665        segments[3].to_string(),
1666        segments[5].to_string(),
1667        segments[7].to_string(),
1668    ))
1669}
1670
1671#[cfg(test)]
1672#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1673mod tests {
1674    use super::*;
1675    use std::sync::atomic::{AtomicU64, Ordering};
1676
1677    #[test]
1678    fn a_timeout_is_a_finite_non_negative_number_of_seconds() {
1679        assert_eq!(waitset_timeout(None), Ok(DEFAULT_WAITSET_TIMEOUT));
1680        assert_eq!(
1681            waitset_timeout(Some("2.5")),
1682            Ok(Duration::from_millis(2500))
1683        );
1684        assert_eq!(waitset_timeout(Some("0")), Ok(Duration::ZERO));
1685        for bad in ["-1", "nan", "inf", "1e300", "abc", ""] {
1686            assert!(
1687                matches!(waitset_timeout(Some(bad)), Err(BackendError::BadRequest(_))),
1688                "{bad}"
1689            );
1690        }
1691    }
1692
1693    #[test]
1694    fn a_domain_id_is_read_or_refused() {
1695        assert_eq!(domain_id_of(None), Ok(0));
1696        assert_eq!(domain_id_of(Some(" 42 ")), Ok(42));
1697        for bad in ["abc", "-1", "2147483648", "4294967295", ""] {
1698            assert!(
1699                matches!(domain_id_of(Some(bad)), Err(BackendError::BadRequest(_))),
1700                "{bad}"
1701            );
1702        }
1703    }
1704
1705    fn idl_file(idl: &str) -> std::path::PathBuf {
1706        static COUNTER: AtomicU64 = AtomicU64::new(0);
1707        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
1708        let dir =
1709            std::env::temp_dir().join(format!("zd-webdds-gateway-{}-{n}", std::process::id()));
1710        std::fs::create_dir_all(&dir).unwrap();
1711        let path = dir.join("t.idl");
1712        std::fs::write(&path, idl).unwrap();
1713        path
1714    }
1715
1716    #[test]
1717    fn full_lifecycle_write_then_read_offline() {
1718        let path = idl_file("module M { @final struct Sensor { long value; }; };");
1719        let mut types = BTreeMap::new();
1720        types.insert(
1721            "M::Sensor".to_string(),
1722            crate::gateway::TypeSource::Idl(path),
1723        );
1724        let mut gw = Gateway::new(types, Arc::new(crate::metadata::MetadataStore::new()));
1725
1726        gw.create_application("app1").expect("app");
1727        let BackendResult::Created(p) = gw.create_participant("app1", 0, b"").expect("participant")
1728        else {
1729            panic!()
1730        };
1731        let participant = p.rsplit('/').next().unwrap().to_string();
1732        gw.create_topic("app1", &participant, "Sensor", "M::Sensor")
1733            .expect("topic");
1734        let BackendResult::Created(p) =
1735            gw.create_publisher("app1", &participant, b"").expect("pub")
1736        else {
1737            panic!()
1738        };
1739        let publisher = p.rsplit('/').next().unwrap().to_string();
1740        let BackendResult::Created(p) = gw
1741            .create_data_writer("app1", &participant, &publisher, "Sensor")
1742            .expect("writer")
1743        else {
1744            panic!()
1745        };
1746        let writer = p.rsplit('/').next().unwrap().to_string();
1747        let r = gw.write_sample(
1748            "app1",
1749            &participant,
1750            &publisher,
1751            &writer,
1752            b"<data><value>42</value></data>",
1753        );
1754        assert!(r.is_ok(), "{r:?}");
1755    }
1756
1757    /// `update_participant`'s recursive contained-entity sync end to
1758    /// end: a publisher + a data_writer nested in the PUT body, created
1759    /// (rather than the flat, per-resource POSTs
1760    /// `full_lifecycle_write_then_read_offline` exercises); a second
1761    /// `update_participant` with a DIFFERENT nested representation then
1762    /// deletes what the first one created and adds something new —
1763    /// Spec §7.4.3.2's own "creating/re-QoS'ing/deleting contained
1764    /// entities" rule, checked against actual DCPS state via
1765    /// `list_publishers`/`list_data_writers`, not just that the calls
1766    /// returned `Ok`.
1767    #[test]
1768    fn update_participant_creates_and_reconciles_contained_entities() {
1769        let path = idl_file("module M { @final struct Sensor { long value; }; };");
1770        let mut types = BTreeMap::new();
1771        types.insert(
1772            "M::Sensor".to_string(),
1773            crate::gateway::TypeSource::Idl(path),
1774        );
1775        let mut gw = Gateway::new(types, Arc::new(crate::metadata::MetadataStore::new()));
1776
1777        gw.create_application("app1").expect("app");
1778        let BackendResult::Created(p) = gw.create_participant("app1", 0, b"").expect("participant")
1779        else {
1780            panic!()
1781        };
1782        let participant = p.rsplit('/').next().unwrap().to_string();
1783        gw.create_topic("app1", &participant, "Sensor", "M::Sensor")
1784            .expect("topic");
1785
1786        // First update: creates a publisher AND its contained writer in
1787        // one call — neither existed before.
1788        let body = b"<domain_participant><publishers><element><name>pub1</name><data_writers><element><name>dw1</name><topic_name>Sensor</topic_name></element></data_writers></element></publishers></domain_participant>";
1789        gw.update_participant("app1", &participant, body)
1790            .expect("first update");
1791        let BackendResult::List(pubs) =
1792            gw.list_publishers("app1", &participant).expect("list pubs")
1793        else {
1794            panic!()
1795        };
1796        assert_eq!(pubs, vec!["pub1".to_string()]);
1797        let BackendResult::List(writers) = gw
1798            .list_data_writers("app1", &participant, "pub1")
1799            .expect("list writers")
1800        else {
1801            panic!()
1802        };
1803        assert_eq!(writers, vec!["dw1".to_string()]);
1804
1805        // Second update: a body naming a DIFFERENT publisher ("pub2",
1806        // no "pub1") reconciles pub1 away and creates pub2 instead —
1807        // Spec §7.4.3.2's "deleting the entities ... which do not
1808        // appear in the representation" rule.
1809        let body = b"<domain_participant><publishers><element><name>pub2</name></element></publishers></domain_participant>";
1810        gw.update_participant("app1", &participant, body)
1811            .expect("second update");
1812        let BackendResult::List(pubs) = gw
1813            .list_publishers("app1", &participant)
1814            .expect("list pubs 2")
1815        else {
1816            panic!()
1817        };
1818        assert_eq!(pubs, vec!["pub2".to_string()]);
1819
1820        // A body naming no `<publishers>` element at all leaves pub2
1821        // untouched — absent is "don't touch", not "delete everything".
1822        gw.update_participant("app1", &participant, b"<domain_participant/>")
1823            .expect("third update");
1824        let BackendResult::List(pubs) = gw
1825            .list_publishers("app1", &participant)
1826            .expect("list pubs 3")
1827        else {
1828            panic!()
1829        };
1830        assert_eq!(pubs, vec!["pub2".to_string()]);
1831    }
1832
1833    #[test]
1834    fn unknown_application_is_not_found() {
1835        let mut gw = Gateway::new(
1836            BTreeMap::new(),
1837            Arc::new(crate::metadata::MetadataStore::new()),
1838        );
1839        let err = gw.create_participant("nope", 0, b"").unwrap_err();
1840        assert!(matches!(err, BackendError::NotFound(_)));
1841    }
1842
1843    #[test]
1844    fn split_reader_path_roundtrip() {
1845        let p = "applications/a/domain_participants/p1/subscribers/s1/data_readers/dr1";
1846        assert_eq!(
1847            split_reader_path(p),
1848            Some(("a".into(), "p1".into(), "s1".into(), "dr1".into()))
1849        );
1850    }
1851
1852    /// `create_topic` with a type this daemon's config does not name at
1853    /// all resolves it purely from a peer's SEDP announcement + a
1854    /// `getTypes` round trip (item 5 end to end, through the public
1855    /// `DdsBackend` surface rather than `types_registry` directly).
1856    #[test]
1857    fn create_topic_resolves_an_unconfigured_type_via_type_lookup() {
1858        let path = idl_file("module M { @final struct Discovered { @key long id; }; };");
1859        let lease = zerodds_test_domains::lease_domain();
1860        let domain = lease.id();
1861
1862        // Peer: publishes the topic, with the type known locally.
1863        let reg = crate::types_registry::TypeRegistry::new();
1864        let resolved = reg
1865            .load_idl_file(&path, "M::Discovered")
1866            .expect("resolve peer side");
1867        let peer = zerodds_dcps::DomainParticipantFactory::instance()
1868            .create_participant(domain, zerodds_dcps::DomainParticipantQos::default())
1869            .expect("peer participant");
1870        let peer_topic = peer
1871            .create_serialized_topic("Discovered", TopicQos::default(), resolved.support)
1872            .expect("peer topic");
1873        let peer_publisher = peer.create_publisher(zerodds_dcps::PublisherQos::default());
1874        let _peer_writer = peer_publisher
1875            .create_datawriter(&peer_topic, zerodds_dcps::DataWriterQos::default())
1876            .expect("peer writer");
1877
1878        // Gateway: empty `types:` config — the type is entirely
1879        // unconfigured here.
1880        let mut gw = Gateway::new(
1881            BTreeMap::new(),
1882            Arc::new(crate::metadata::MetadataStore::new()),
1883        );
1884        gw.create_application("app1").expect("app");
1885        let BackendResult::Created(p) = gw
1886            .create_participant("app1", domain as u32, b"")
1887            .expect("gateway participant")
1888        else {
1889            panic!()
1890        };
1891        let participant = p.rsplit('/').next().unwrap().to_string();
1892
1893        let deadline = std::time::Instant::now() + Duration::from_secs(20);
1894        let mut created = None;
1895        while created.is_none() && std::time::Instant::now() < deadline {
1896            match gw.create_topic("app1", &participant, "Discovered", "M::Discovered") {
1897                Ok(r) => created = Some(r),
1898                Err(BackendError::BadRequest(m)) if m.contains("no discovered") => {
1899                    std::thread::sleep(Duration::from_millis(100));
1900                }
1901                Err(e) => panic!("create_topic failed: {e:?}"),
1902            }
1903        }
1904        assert!(
1905            matches!(created, Some(BackendResult::Created(_))),
1906            "topic was not created via TypeLookup discovery within 20s"
1907        );
1908    }
1909}