1use 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
43fn 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
58const 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 conditions: Vec<(String, Arc<ReadCondition>)>,
98 turn: Mutex<()>,
102}
103
104pub const DEFAULT_WAITSET_TIMEOUT: Duration = Duration::from_secs(10);
106
107pub struct WaitsetHandle(Arc<WaitsetEntry>);
112
113impl WaitsetHandle {
114 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
143pub 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
164pub 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#[derive(Debug, Clone)]
194pub enum TypeSource {
195 Idl(PathBuf),
197 Xml(PathBuf),
200}
201
202pub struct Gateway {
205 apps: Mutex<BTreeMap<String, AppEntry>>,
206 types: Arc<TypeRegistry>,
207 type_sources: BTreeMap<String, TypeSource>,
210 metadata: Arc<crate::metadata::MetadataStore>,
217}
218
219impl Gateway {
220 #[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 #[must_use]
244 pub fn types(&self) -> &Arc<TypeRegistry> {
245 &self.types
246 }
247
248 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 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
337fn 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
351fn 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
384fn 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 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 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 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 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 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 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 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
1394fn 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
1445fn 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
1496fn 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
1546fn 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
1620fn 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 #[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 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 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 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 #[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 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 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}