1use std::collections::HashMap;
61use std::sync::{Arc, Mutex};
62
63use zerodds_ast::Ast;
64use zerodds_dcps::runtime::DcpsRuntime;
65use zerodds_dcps::serialized_type::SerializedTypeSupport;
66use zerodds_types::EquivalenceHash;
67use zerodds_types::dynamic::DynamicType;
68use zerodds_types::type_object::TypeObject;
69
70#[derive(Clone)]
74pub struct ResolvedType {
75 pub support: Arc<SerializedTypeSupport>,
78 pub dynamic_type: DynamicType,
80 pub keyed: bool,
82}
83
84impl std::fmt::Debug for ResolvedType {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 f.debug_struct("ResolvedType")
87 .field("type_name", &self.dynamic_type.name())
88 .field("keyed", &self.keyed)
89 .finish_non_exhaustive()
90 }
91}
92
93#[derive(Debug)]
95pub enum TypeResolveError {
96 Io(std::io::Error),
98 Idl(String),
100 Model(String),
103 NotFound(String),
105 NotYetResolved,
108 NoDiscoveredEndpoint(String),
111 TimedOut(String),
114}
115
116impl std::fmt::Display for TypeResolveError {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 Self::Io(e) => write!(f, "io: {e}"),
120 Self::Idl(m) => write!(f, "idl: {m}"),
121 Self::Model(m) => write!(f, "type model: {m}"),
122 Self::NotFound(name) => write!(f, "type `{name}` not found"),
123 Self::NotYetResolved => f.write_str("type not yet resolved via TypeLookup"),
124 Self::NoDiscoveredEndpoint(topic) => {
125 write!(
126 f,
127 "no discovered publication or subscription names topic `{topic}`"
128 )
129 }
130 Self::TimedOut(name) => write!(
131 f,
132 "type `{name}` never resolved via TypeLookup within the bound"
133 ),
134 }
135 }
136}
137
138impl std::error::Error for TypeResolveError {}
139
140#[derive(Default)]
142pub struct TypeRegistry {
143 by_name: Mutex<HashMap<String, ResolvedType>>,
144}
145
146impl TypeRegistry {
147 #[must_use]
149 pub fn new() -> Self {
150 Self::default()
151 }
152
153 #[must_use]
155 pub fn get(&self, type_name: &str) -> Option<ResolvedType> {
156 self.by_name
157 .lock()
158 .ok()
159 .and_then(|m| m.get(type_name).cloned())
160 }
161
162 pub fn insert(&self, type_name: &str, resolved: ResolvedType) {
166 if let Ok(mut m) = self.by_name.lock() {
167 m.insert(type_name.to_string(), resolved);
168 }
169 }
170
171 pub fn load_idl_file(
179 &self,
180 idl_path: &std::path::Path,
181 type_name: &str,
182 ) -> Result<ResolvedType, TypeResolveError> {
183 let source = std::fs::read_to_string(idl_path).map_err(TypeResolveError::Io)?;
184 let ast = Arc::new(
185 zerodds_wire_probe::encode::model_from_idl(&source, &idl_path.to_string_lossy())
186 .map_err(|e| TypeResolveError::Idl(format!("{e}")))?,
187 );
188 let resolved = resolve_from_ast(&ast, type_name)?;
189 self.insert(type_name, resolved.clone());
190 Ok(resolved)
191 }
192
193 pub fn load_xml_file(
210 &self,
211 xml_path: &std::path::Path,
212 type_name: &str,
213 ) -> Result<ResolvedType, TypeResolveError> {
214 let source = std::fs::read_to_string(xml_path).map_err(TypeResolveError::Io)?;
215 let libraries = zerodds_xml::parse_type_libraries(&source)
216 .map_err(|e| TypeResolveError::Idl(format!("{e}")))?;
217 let mut objects: HashMap<String, zerodds_types::CompleteTypeObject> = HashMap::new();
218 for lib in &libraries {
219 let bridged = zerodds_xml::bridge_library_complete(lib)
220 .map_err(|e| TypeResolveError::Model(format!("{e}")))?;
221 objects.extend(bridged);
222 }
223 let Some(root) = objects.remove(type_name) else {
224 return Err(TypeResolveError::NotFound(format!(
225 "{type_name} (not declared in {})",
226 xml_path.display()
227 )));
228 };
229 let dependencies: Vec<zerodds_types::CompleteTypeObject> = objects.into_values().collect();
230 let resolved = resolve_from_complete(type_name, root, dependencies)?;
231 self.insert(type_name, resolved.clone());
232 Ok(resolved)
233 }
234
235 pub fn resolve_discovered(
246 &self,
247 runtime: &DcpsRuntime,
248 type_name: &str,
249 hash: EquivalenceHash,
250 ) -> Result<ResolvedType, TypeResolveError> {
251 let (root, dependencies) = {
252 let server = runtime
253 .type_lookup_server
254 .lock()
255 .map_err(|_| TypeResolveError::Model("type_lookup_server poisoned".into()))?;
256 let Some(root) = server.registry.get_complete(&hash).cloned() else {
257 return Err(TypeResolveError::NotYetResolved);
258 };
259 let dep_hashes = server
260 .registry
261 .transitive_dependencies(&hash, zerodds_types::resolve::DEFAULT_MAX_RESOLVE_NODES);
262 let mut deps = Vec::with_capacity(dep_hashes.len());
263 for dep_hash in &dep_hashes {
264 let Some(dep) = server.registry.get_complete(dep_hash).cloned() else {
265 return Err(TypeResolveError::NotYetResolved);
266 };
267 deps.push(dep);
268 }
269 (root, deps)
270 };
271 let resolved = resolve_from_complete(type_name, root, dependencies)?;
272 self.insert(type_name, resolved.clone());
273 Ok(resolved)
274 }
275
276 pub fn resolve_named(
300 &self,
301 runtime: &DcpsRuntime,
302 topic_name: &str,
303 type_name: &str,
304 timeout: core::time::Duration,
305 ) -> Result<ResolvedType, TypeResolveError> {
306 let endpoint = runtime
307 .discovered_publication_endpoints()
308 .into_iter()
309 .chain(runtime.discovered_subscription_endpoints())
310 .find(|e| e.topic_name == topic_name)
311 .ok_or_else(|| TypeResolveError::NoDiscoveredEndpoint(topic_name.to_string()))?;
312
313 let zerodds_types::TypeIdentifier::EquivalenceHashComplete(hash) =
314 endpoint.type_identifier.clone()
315 else {
316 return Err(TypeResolveError::Model(format!(
317 "topic `{topic_name}`'s discovered TypeIdentifier is not the \
318 EquivalenceHashComplete form this daemon can resolve"
319 )));
320 };
321
322 match self.resolve_discovered(runtime, type_name, hash) {
323 Ok(r) => return Ok(r),
324 Err(TypeResolveError::NotYetResolved) => {}
325 Err(e) => return Err(e),
326 }
327
328 let mut peer_prefix = [0u8; 12];
329 peer_prefix.copy_from_slice(&endpoint.endpoint_guid[..12]);
330 let peer = zerodds_rtps::wire_types::GuidPrefix(peer_prefix);
331 let _ = runtime.send_type_lookup_request(peer, &[endpoint.type_identifier.clone()]);
332
333 let deadline = std::time::Instant::now() + timeout;
334 let mut last_generation = current_generation(runtime);
335 loop {
336 if std::time::Instant::now() >= deadline {
337 return Err(TypeResolveError::TimedOut(type_name.to_string()));
338 }
339 std::thread::sleep(core::time::Duration::from_millis(10));
340 let generation = current_generation(runtime);
341 if generation == last_generation {
342 continue;
343 }
344 last_generation = generation;
345 match self.resolve_discovered(runtime, type_name, hash) {
346 Ok(r) => return Ok(r),
347 Err(TypeResolveError::NotYetResolved) => {}
348 Err(e) => return Err(e),
349 }
350 }
351 }
352
353 pub fn resolve_synthetic(
367 &self,
368 type_name: &str,
369 root: zerodds_types::CompleteTypeObject,
370 dependencies: Vec<zerodds_types::CompleteTypeObject>,
371 ) -> Result<ResolvedType, TypeResolveError> {
372 let resolved = resolve_from_complete(type_name, root, dependencies)?;
373 self.insert(type_name, resolved.clone());
374 Ok(resolved)
375 }
376}
377
378fn current_generation(runtime: &DcpsRuntime) -> u64 {
382 runtime
383 .type_lookup_server
384 .lock()
385 .map(|s| s.registry.generation())
386 .unwrap_or(0)
387}
388
389fn resolve_from_ast(ast: &Arc<Ast>, type_name: &str) -> Result<ResolvedType, TypeResolveError> {
392 let dynamic_type = DynamicType::find(ast, type_name)
393 .map_err(|e| TypeResolveError::NotFound(format!("{type_name}: {e}")))?;
394 let root_id = dynamic_type.id();
395 let complete = zerodds_types::from_ast::complete_type_object(ast, root_id)
396 .map_err(|e| TypeResolveError::Model(format!("{e:?}")))?;
397 let mut dependencies = Vec::new();
402 for (id, _) in ast.types() {
403 if id == root_id {
404 continue;
405 }
406 if let Ok(dep) = zerodds_types::from_ast::complete_type_object(ast, id) {
407 dependencies.push(dep);
408 }
409 }
410 resolve_from_complete(type_name, complete, dependencies)
411}
412
413fn resolve_from_complete(
430 type_name: &str,
431 root: zerodds_types::CompleteTypeObject,
432 dependencies: Vec<zerodds_types::CompleteTypeObject>,
433) -> Result<ResolvedType, TypeResolveError> {
434 let root_bytes = TypeObject::Complete(root.clone())
435 .to_bytes_le()
436 .map_err(|e| TypeResolveError::Model(format!("re-encoding root TypeObject: {e:?}")))?;
437 let dep_bytes: Vec<Vec<u8>> = dependencies
438 .iter()
439 .map(|d| {
440 TypeObject::Complete(d.clone())
441 .to_bytes_le()
442 .map_err(|e| TypeResolveError::Model(format!("re-encoding dependency: {e:?}")))
443 })
444 .collect::<Result<_, _>>()?;
445
446 let mut registry = zerodds_types::resolve::TypeRegistry::new();
447 for dep in &dependencies {
448 registry
449 .insert_object(TypeObject::Complete(dep.clone()))
450 .map_err(|e| TypeResolveError::Model(format!("dependency hash: {e:?}")))?;
451 }
452 registry
453 .insert_object(TypeObject::Complete(root.clone()))
454 .map_err(|e| TypeResolveError::Model(format!("root hash: {e:?}")))?;
455 let mut model = zerodds_types::dynamic::ModelFromTypeObjects::new(®istry);
456 let root_id = model
457 .add_type_object(&TypeObject::Complete(root))
458 .map_err(|e| TypeResolveError::Model(format!("{e:?}")))?;
459 let ast = model.finish();
460
461 let dynamic_type = DynamicType::new(Arc::clone(&ast), root_id);
462 let keyed = zerodds_ast_layout::keyhash::holder_keeps_only_keys(&ast, root_id)
463 || zerodds_ast_layout::keyhash::union_key_discriminator(&ast, root_id).is_some();
464 let extensibility = match dynamic_type.extensibility() {
465 Some(zerodds_types::dynamic::ExtensibilityKind::Final) => 0,
466 Some(zerodds_types::dynamic::ExtensibilityKind::Appendable) => 1,
467 Some(zerodds_types::dynamic::ExtensibilityKind::Mutable) => 2,
468 None => {
469 return Err(TypeResolveError::Model(format!(
470 "`{type_name}`'s root has no aggregate (struct/union) extensibility"
471 )));
472 }
473 };
474
475 let support = SerializedTypeSupport::from_type_objects(
476 type_name,
477 &root_bytes,
478 &dep_bytes,
479 keyed,
480 extensibility,
481 )
482 .map_err(TypeResolveError::Model)?;
483
484 Ok(ResolvedType {
485 support,
486 dynamic_type,
487 keyed,
488 })
489}
490
491#[cfg(test)]
492#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
493mod tests {
494 use super::*;
495
496 fn write_idl(dir: &std::path::Path, text: &str) -> std::path::PathBuf {
497 let path = dir.join("t.idl");
498 std::fs::write(&path, text).unwrap();
499 path
500 }
501
502 fn write_xml(dir: &std::path::Path, text: &str) -> std::path::PathBuf {
503 let path = dir.join("t.xml");
504 std::fs::write(&path, text).unwrap();
505 path
506 }
507
508 #[test]
509 fn loads_a_keyed_struct_from_a_dds_xml_types_file() {
510 let dir = std::env::temp_dir().join(format!("zd-webdds-xmltypes-{}", std::process::id()));
511 std::fs::create_dir_all(&dir).unwrap();
512 let path = write_xml(
513 &dir,
514 r#"<types>
515 <struct name="Sample">
516 <member name="id" type="int32" key="true"/>
517 <member name="value" type="float64"/>
518 </struct>
519 </types>"#,
520 );
521 let reg = TypeRegistry::new();
522 let resolved = reg.load_xml_file(&path, "Sample").expect("resolved");
523 assert!(resolved.keyed);
524 assert_eq!(resolved.dynamic_type.member_count(), 2);
525 assert!(reg.get("Sample").is_some());
526 let _ = std::fs::remove_dir_all(&dir);
527 }
528
529 #[test]
530 fn dds_xml_types_file_with_an_enum_dependency_resolves() {
531 let dir = std::env::temp_dir().join(format!("zd-webdds-xmltypes2-{}", std::process::id()));
532 std::fs::create_dir_all(&dir).unwrap();
533 let path = write_xml(
534 &dir,
535 r#"<types>
536 <enum name="Color">
537 <enumerator name="RED"/>
538 <enumerator name="GREEN"/>
539 </enum>
540 <struct name="Pixel">
541 <member name="id" type="int32" key="true"/>
542 <member name="color" type="nonBasic" nonBasicTypeName="Color"/>
543 </struct>
544 </types>"#,
545 );
546 let reg = TypeRegistry::new();
547 let resolved = reg.load_xml_file(&path, "Pixel").expect("resolved");
548 assert!(resolved.keyed);
549 assert_eq!(resolved.dynamic_type.member_count(), 2);
550 let _ = std::fs::remove_dir_all(&dir);
551 }
552
553 #[test]
554 fn unknown_type_in_xml_file_is_reported() {
555 let dir = std::env::temp_dir().join(format!("zd-webdds-xmltypes3-{}", std::process::id()));
556 std::fs::create_dir_all(&dir).unwrap();
557 let path = write_xml(
558 &dir,
559 r#"<types><struct name="A"><member name="x" type="int32"/></struct></types>"#,
560 );
561 let reg = TypeRegistry::new();
562 let err = reg.load_xml_file(&path, "NoSuchType").unwrap_err();
563 assert!(matches!(err, TypeResolveError::NotFound(_)));
564 let _ = std::fs::remove_dir_all(&dir);
565 }
566
567 #[test]
568 fn loads_a_keyed_struct_from_an_idl_file() {
569 let dir = std::env::temp_dir().join(format!("zd-webdds-types-{}", std::process::id()));
570 std::fs::create_dir_all(&dir).unwrap();
571 let path = write_idl(
572 &dir,
573 "module M { @final struct Sample { @key long id; string name; }; };",
574 );
575 let reg = TypeRegistry::new();
576 let resolved = reg.load_idl_file(&path, "M::Sample").expect("resolved");
577 assert!(resolved.keyed);
578 assert_eq!(resolved.dynamic_type.member_count(), 2);
579 assert!(reg.get("M::Sample").is_some());
581 let _ = std::fs::remove_dir_all(&dir);
582 }
583
584 #[test]
585 fn unknown_type_name_is_reported() {
586 let dir = std::env::temp_dir().join(format!("zd-webdds-types2-{}", std::process::id()));
587 std::fs::create_dir_all(&dir).unwrap();
588 let path = write_idl(&dir, "module M { @final struct A { long x; }; };");
589 let reg = TypeRegistry::new();
590 let err = reg.load_idl_file(&path, "M::NoSuchType").unwrap_err();
591 assert!(matches!(err, TypeResolveError::NotFound(_)));
592 let _ = std::fs::remove_dir_all(&dir);
593 }
594
595 #[test]
596 fn unkeyed_struct_roundtrip() {
597 let dir = std::env::temp_dir().join(format!("zd-webdds-types3-{}", std::process::id()));
598 std::fs::create_dir_all(&dir).unwrap();
599 let path = write_idl(
600 &dir,
601 "module M { @final struct Plain { long x; long y; }; };",
602 );
603 let reg = TypeRegistry::new();
604 let resolved = reg.load_idl_file(&path, "M::Plain").expect("resolved");
605 assert!(!resolved.keyed);
606 let _ = std::fs::remove_dir_all(&dir);
607 }
608
609 #[test]
614 fn resolve_named_discovers_a_type_never_loaded_locally() {
615 let dir = std::env::temp_dir().join(format!("zd-webdds-types-tl-{}", std::process::id()));
616 std::fs::create_dir_all(&dir).unwrap();
617 let path = write_idl(
618 &dir,
619 "module M { @final struct Tlq { @key long id; long value; }; };",
620 );
621
622 let lease = zerodds_test_domains::lease_domain();
623 let domain = lease.id();
624
625 let reg_a = TypeRegistry::new();
627 let resolved_a = reg_a.load_idl_file(&path, "M::Tlq").expect("resolve A");
628 let participant_a = zerodds_dcps::DomainParticipantFactory::instance()
629 .create_participant(domain, zerodds_dcps::DomainParticipantQos::default())
630 .expect("participant A");
631 let topic_a = participant_a
632 .create_serialized_topic("Tlq", zerodds_dcps::TopicQos::default(), resolved_a.support)
633 .expect("topic A");
634 let publisher_a = participant_a.create_publisher(zerodds_dcps::PublisherQos::default());
635 let _writer_a = publisher_a
636 .create_datawriter(&topic_a, zerodds_dcps::DataWriterQos::default())
637 .expect("writer A");
638
639 let participant_b = zerodds_dcps::DomainParticipantFactory::instance()
641 .create_participant(domain, zerodds_dcps::DomainParticipantQos::default())
642 .expect("participant B");
643 let subscriber_b = participant_b.create_subscriber(zerodds_dcps::SubscriberQos::default());
644 let _ = &subscriber_b;
651
652 let reg_b = TypeRegistry::new();
653 let runtime_b = participant_b.runtime().expect("live runtime B").clone();
654 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
655 let mut resolved_b = None;
656 while resolved_b.is_none() && std::time::Instant::now() < deadline {
657 match reg_b.resolve_named(
658 &runtime_b,
659 "Tlq",
660 "M::Tlq",
661 std::time::Duration::from_secs(2),
662 ) {
663 Ok(r) => resolved_b = Some(r),
664 Err(TypeResolveError::NoDiscoveredEndpoint(_)) => {
665 std::thread::sleep(std::time::Duration::from_millis(100));
666 }
667 Err(e) => panic!("resolve_named failed: {e}"),
668 }
669 }
670 let resolved_b = resolved_b.expect("type resolved via TypeLookup within 20s");
671 assert!(resolved_b.keyed);
672 assert_eq!(resolved_b.dynamic_type.member_count(), 2);
673
674 let _ = std::fs::remove_dir_all(&dir);
675 }
676}