1use alloc::format;
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28
29use crate::errors::XmlError;
30use crate::parser::{XmlElement, parse_xml_tree};
31use crate::qos::EntityQos;
32use crate::qos_parser::parse_entity_qos_public;
33use crate::types::parse_ulong;
34
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
37pub struct DomainLibrary {
38 pub name: String,
40 pub domains: Vec<DomainEntry>,
42}
43
44impl DomainLibrary {
45 #[must_use]
47 pub fn domain(&self, name: &str) -> Option<&DomainEntry> {
48 self.domains.iter().find(|d| d.name == name)
49 }
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct DomainEntry {
55 pub name: String,
57 pub domain_id: u32,
59 pub base_name: Option<String>,
64 pub register_types: Vec<RegisterType>,
66 pub topics: Vec<TopicEntry>,
68}
69
70impl DomainEntry {
71 #[must_use]
73 pub fn register_type(&self, name: &str) -> Option<&RegisterType> {
74 self.register_types.iter().find(|r| r.name == name)
75 }
76 #[must_use]
78 pub fn topic(&self, name: &str) -> Option<&TopicEntry> {
79 self.topics.iter().find(|t| t.name == name)
80 }
81}
82
83#[derive(Debug, Clone, Default, PartialEq, Eq)]
88pub struct RegisterType {
89 pub name: String,
91 pub type_ref: String,
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
97pub struct TopicEntry {
98 pub name: String,
100 pub register_type_ref: String,
102 pub topic_qos: Option<EntityQos>,
105 pub qos_profile_ref: Option<String>,
107 pub topic_filter: Option<String>,
110}
111
112pub fn parse_domain_libraries(xml: &str) -> Result<Vec<DomainLibrary>, XmlError> {
119 let doc = parse_xml_tree(xml)?;
120 if doc.root.name != "dds" {
121 return Err(XmlError::InvalidXml(format!(
122 "expected <dds> root, got <{}>",
123 doc.root.name
124 )));
125 }
126 let mut libs = Vec::new();
127 for lib_node in doc.root.children_named("domain_library") {
128 libs.push(parse_domain_library_element(lib_node)?);
129 }
130 Ok(libs)
131}
132
133pub(crate) fn parse_domain_library_element(el: &XmlElement) -> Result<DomainLibrary, XmlError> {
134 let name = el
135 .attribute("name")
136 .ok_or_else(|| XmlError::MissingRequiredElement("domain_library@name".into()))?
137 .to_string();
138 let mut domains = Vec::new();
139 for d in el.children_named("domain") {
140 domains.push(parse_domain_element(d)?);
141 }
142 Ok(DomainLibrary { name, domains })
143}
144
145fn parse_domain_element(el: &XmlElement) -> Result<DomainEntry, XmlError> {
146 let name = el
147 .attribute("name")
148 .ok_or_else(|| XmlError::MissingRequiredElement("domain@name".into()))?
149 .to_string();
150 let domain_id_str = el
151 .attribute("domain_id")
152 .ok_or_else(|| XmlError::MissingRequiredElement("domain@domain_id".into()))?;
153 let domain_id = parse_ulong(domain_id_str)?;
154 if domain_id > 232 {
155 return Err(XmlError::ValueOutOfRange(format!(
157 "domain_id `{domain_id}` exceeds 232"
158 )));
159 }
160
161 let base_name = el.attribute("base_name").map(ToString::to_string);
162 let mut register_types = Vec::new();
163 let mut topics = Vec::new();
164 for child in &el.children {
165 match child.name.as_str() {
166 "register_type" => register_types.push(parse_register_type(child)?),
167 "topic" => topics.push(parse_topic_entry(child)?),
168 _ => {}
169 }
170 }
171
172 Ok(DomainEntry {
173 name,
174 domain_id,
175 base_name,
176 register_types,
177 topics,
178 })
179}
180
181fn parse_register_type(el: &XmlElement) -> Result<RegisterType, XmlError> {
182 let name = el
183 .attribute("name")
184 .ok_or_else(|| XmlError::MissingRequiredElement("register_type@name".into()))?
185 .to_string();
186 let type_ref = el
191 .attribute("type_ref")
192 .map_or_else(|| name.clone(), ToString::to_string);
193 Ok(RegisterType { name, type_ref })
194}
195
196fn parse_topic_entry(el: &XmlElement) -> Result<TopicEntry, XmlError> {
197 let name = el
198 .attribute("name")
199 .ok_or_else(|| XmlError::MissingRequiredElement("topic@name".into()))?
200 .to_string();
201 let register_type_ref = el
202 .attribute("register_type_ref")
203 .ok_or_else(|| XmlError::MissingRequiredElement("topic@register_type_ref".into()))?
204 .to_string();
205 let qos_profile_ref = el.attribute("qos_profile_ref").map(ToString::to_string);
206 let mut topic_filter = el.attribute("topic_filter").map(ToString::to_string);
207 let mut topic_qos: Option<EntityQos> = None;
208 for child in &el.children {
209 match child.name.as_str() {
210 "topic_qos" => topic_qos = Some(parse_entity_qos_public(child)?),
211 "topic_filter" => topic_filter = Some(child.text.clone()),
212 _ => {}
213 }
214 }
215 Ok(TopicEntry {
216 name,
217 register_type_ref,
218 topic_qos,
219 qos_profile_ref,
220 topic_filter,
221 })
222}
223
224#[cfg(test)]
225#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn parse_minimal_domain_library() {
231 let xml = r#"<dds>
232 <domain_library name="L">
233 <domain name="D" domain_id="42"/>
234 </domain_library>
235 </dds>"#;
236 let libs = parse_domain_libraries(xml).expect("parse");
237 assert_eq!(libs.len(), 1);
238 assert_eq!(libs[0].name, "L");
239 assert_eq!(libs[0].domains[0].name, "D");
240 assert_eq!(libs[0].domains[0].domain_id, 42);
241 }
242
243 #[test]
244 fn parse_domain_with_topic() {
245 let xml = r#"<dds>
246 <domain_library name="L">
247 <domain name="D" domain_id="0">
248 <register_type name="StateType" type_ref="MyTypes::State"/>
249 <topic name="StateTopic" register_type_ref="StateType"/>
250 </domain>
251 </domain_library>
252 </dds>"#;
253 let libs = parse_domain_libraries(xml).expect("parse");
254 let d = &libs[0].domains[0];
255 assert_eq!(d.register_types[0].name, "StateType");
256 assert_eq!(d.register_types[0].type_ref, "MyTypes::State");
257 assert_eq!(d.topics[0].name, "StateTopic");
258 assert_eq!(d.topics[0].register_type_ref, "StateType");
259 }
260
261 #[test]
262 fn parse_topic_with_inline_qos() {
263 let xml = r#"<dds>
264 <domain_library name="L">
265 <domain name="D" domain_id="1">
266 <register_type name="T1"/>
267 <topic name="Topic1" register_type_ref="T1">
268 <topic_qos>
269 <reliability><kind>RELIABLE</kind></reliability>
270 </topic_qos>
271 </topic>
272 </domain>
273 </domain_library>
274 </dds>"#;
275 let libs = parse_domain_libraries(xml).expect("parse");
276 let t = &libs[0].domains[0].topics[0];
277 assert!(t.topic_qos.is_some());
278 let q = t.topic_qos.as_ref().unwrap();
279 assert!(q.reliability.is_some());
280 }
281
282 #[test]
283 fn missing_domain_id_rejected() {
284 let xml = r#"<dds>
285 <domain_library name="L">
286 <domain name="D"/>
287 </domain_library>
288 </dds>"#;
289 let err = parse_domain_libraries(xml).expect_err("missing");
290 assert!(matches!(err, XmlError::MissingRequiredElement(_)));
291 }
292
293 #[test]
294 fn domain_id_out_of_range() {
295 let xml = r#"<dds>
296 <domain_library name="L">
297 <domain name="D" domain_id="500"/>
298 </domain_library>
299 </dds>"#;
300 let err = parse_domain_libraries(xml).expect_err("oor");
301 assert!(matches!(err, XmlError::ValueOutOfRange(_)));
302 }
303
304 #[test]
305 fn topic_missing_register_type_ref_rejected() {
306 let xml = r#"<dds>
307 <domain_library name="L">
308 <domain name="D" domain_id="0">
309 <topic name="T1"/>
310 </domain>
311 </domain_library>
312 </dds>"#;
313 let err = parse_domain_libraries(xml).expect_err("missing");
314 assert!(matches!(err, XmlError::MissingRequiredElement(_)));
315 }
316
317 #[test]
320 fn parse_domain_with_base_name() {
321 let xml = r#"<dds>
322 <domain_library name="L">
323 <domain name="Base" domain_id="0">
324 <register_type name="T"/>
325 </domain>
326 <domain name="Derived" domain_id="1" base_name="Base">
327 <register_type name="T2"/>
328 </domain>
329 </domain_library>
330 </dds>"#;
331 let libs = parse_domain_libraries(xml).expect("parse");
332 let lib = &libs[0];
333 let base = lib.domains.iter().find(|d| d.name == "Base").expect("base");
334 let derived = lib
335 .domains
336 .iter()
337 .find(|d| d.name == "Derived")
338 .expect("derived");
339 assert_eq!(base.base_name, None);
340 assert_eq!(derived.base_name.as_deref(), Some("Base"));
341 }
342
343 #[test]
344 fn domain_inheritance_chain_resolves_via_resolve_chain() {
345 use crate::inheritance::resolve_chain;
348 use alloc::collections::BTreeMap;
349
350 let xml = r#"<dds>
351 <domain_library name="L">
352 <domain name="A" domain_id="0"/>
353 <domain name="B" domain_id="1" base_name="A"/>
354 <domain name="C" domain_id="2" base_name="B"/>
355 </domain_library>
356 </dds>"#;
357 let libs = parse_domain_libraries(xml).expect("parse");
358 let lib = &libs[0];
359 let mut by_name: BTreeMap<String, Option<String>> = BTreeMap::new();
360 for d in &lib.domains {
361 by_name.insert(d.name.clone(), d.base_name.clone());
362 }
363 let chain = resolve_chain("C", |n| {
364 by_name
365 .get(n)
366 .cloned()
367 .ok_or_else(|| XmlError::MissingRequiredElement(n.into()))
368 })
369 .expect("chain");
370 assert_eq!(chain, alloc::vec!["A".to_string(), "B".into(), "C".into()]);
371 }
372
373 #[test]
374 fn domain_inheritance_cycle_detected() {
375 use crate::inheritance::resolve_chain;
376 use alloc::collections::BTreeMap;
377
378 let xml = r#"<dds>
379 <domain_library name="L">
380 <domain name="A" domain_id="0" base_name="B"/>
381 <domain name="B" domain_id="1" base_name="A"/>
382 </domain_library>
383 </dds>"#;
384 let libs = parse_domain_libraries(xml).expect("parse");
385 let lib = &libs[0];
386 let mut by_name: BTreeMap<String, Option<String>> = BTreeMap::new();
387 for d in &lib.domains {
388 by_name.insert(d.name.clone(), d.base_name.clone());
389 }
390 let err = resolve_chain("A", |n| {
391 by_name
392 .get(n)
393 .cloned()
394 .ok_or_else(|| XmlError::MissingRequiredElement(n.into()))
395 })
396 .expect_err("cycle");
397 assert!(matches!(err, XmlError::CircularInheritance(_)));
398 }
399}