1use alloc::format;
18use alloc::string::{String, ToString};
19
20use crate::errors::XmlError;
21use crate::inheritance::resolve_chain;
22use crate::qos::{EntityQos, QosLibrary, QosProfile};
23
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct ResolvedQos {
32 pub lookup_path: String,
34 pub topic_filter: Option<String>,
36 pub datawriter_qos: Option<EntityQos>,
38 pub datareader_qos: Option<EntityQos>,
40 pub topic_qos: Option<EntityQos>,
42 pub publisher_qos: Option<EntityQos>,
44 pub subscriber_qos: Option<EntityQos>,
46 pub domainparticipant_qos: Option<EntityQos>,
48}
49
50pub fn resolve_profile(
64 libraries: &[QosLibrary],
65 lookup_path: &str,
66) -> Result<ResolvedQos, XmlError> {
67 let (lib_name, prof_name) = split_path(lookup_path)?;
69
70 let chain = resolve_chain(&format!("{lib_name}::{prof_name}"), |canonical| {
75 let (l, p) = split_path(canonical)?;
76 let prof = locate(libraries, l, p)?;
77 Ok(prof.base_name.as_deref().map(|b| {
80 if b.contains("::") {
81 b.to_string()
82 } else {
83 format!("{l}::{b}")
84 }
85 }))
86 })?;
87
88 let mut topic_filter: Option<String> = None;
91 let mut dw: Option<EntityQos> = None;
92 let mut dr: Option<EntityQos> = None;
93 let mut topic: Option<EntityQos> = None;
94 let mut pub_q: Option<EntityQos> = None;
95 let mut sub_q: Option<EntityQos> = None;
96 let mut dp: Option<EntityQos> = None;
97
98 for key in &chain {
99 let (l, p) = split_path(key)?;
100 let prof = locate(libraries, l, p)?;
101 if let Some(t) = &prof.topic_filter {
102 topic_filter = Some(t.clone());
103 }
104 dw = merge_entity(dw, prof.datawriter_qos.as_ref());
105 dr = merge_entity(dr, prof.datareader_qos.as_ref());
106 topic = merge_entity(topic, prof.topic_qos.as_ref());
107 pub_q = merge_entity(pub_q, prof.publisher_qos.as_ref());
108 sub_q = merge_entity(sub_q, prof.subscriber_qos.as_ref());
109 dp = merge_entity(dp, prof.domainparticipant_qos.as_ref());
110 }
111
112 Ok(ResolvedQos {
113 lookup_path: lookup_path.to_string(),
114 topic_filter,
115 datawriter_qos: dw,
116 datareader_qos: dr,
117 topic_qos: topic,
118 publisher_qos: pub_q,
119 subscriber_qos: sub_q,
120 domainparticipant_qos: dp,
121 })
122}
123
124fn split_path(path: &str) -> Result<(&str, &str), XmlError> {
126 match path.split_once("::") {
127 Some((l, p)) if !l.is_empty() && !p.is_empty() => Ok((l, p)),
128 _ => Err(XmlError::UnresolvedReference(format!(
129 "expected `library::profile`, got `{path}`"
130 ))),
131 }
132}
133
134fn locate<'a>(
135 libraries: &'a [QosLibrary],
136 lib_name: &str,
137 prof_name: &str,
138) -> Result<&'a QosProfile, XmlError> {
139 let lib = libraries
140 .iter()
141 .find(|l| l.name == lib_name)
142 .ok_or_else(|| XmlError::UnresolvedReference(format!("library `{lib_name}`")))?;
143 lib.profile(prof_name)
144 .ok_or_else(|| XmlError::UnresolvedReference(format!("profile `{lib_name}::{prof_name}`")))
145}
146
147fn merge_entity(acc: Option<EntityQos>, child: Option<&EntityQos>) -> Option<EntityQos> {
150 match (acc, child) {
151 (None, None) => None,
152 (Some(a), None) => Some(a),
153 (None, Some(c)) => Some(c.clone()),
154 (Some(a), Some(c)) => Some(a.merge(c)),
155 }
156}
157
158#[cfg(test)]
159#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
160mod tests {
161 use super::*;
162 use crate::qos_parser::parse_qos_libraries;
163 use alloc::vec;
164 use zerodds_qos::{DurabilityKind, HistoryKind};
165
166 fn parse(xml: &str) -> Vec<QosLibrary> {
167 parse_qos_libraries(xml).expect("parse")
168 }
169
170 #[test]
171 fn split_path_ok() {
172 assert_eq!(split_path("L::P").unwrap(), ("L", "P"));
173 }
174
175 #[test]
176 fn split_path_invalid() {
177 assert!(matches!(
178 split_path("just_one"),
179 Err(XmlError::UnresolvedReference(_))
180 ));
181 assert!(matches!(
182 split_path("::P"),
183 Err(XmlError::UnresolvedReference(_))
184 ));
185 assert!(matches!(
186 split_path("L::"),
187 Err(XmlError::UnresolvedReference(_))
188 ));
189 }
190
191 #[test]
192 fn child_inherits_parent_reliability() {
193 let xml = r#"<dds><qos_library name="L">
194 <qos_profile name="Base">
195 <datawriter_qos>
196 <reliability><kind>RELIABLE</kind></reliability>
197 <history><kind>KEEP_LAST</kind><depth>5</depth></history>
198 </datawriter_qos>
199 </qos_profile>
200 <qos_profile name="Derived" base_name="Base">
201 <datawriter_qos>
202 <history><kind>KEEP_ALL</kind></history>
203 </datawriter_qos>
204 </qos_profile>
205 </qos_library></dds>"#;
206 let libs = parse(xml);
207 let r = resolve_profile(&libs, "L::Derived").expect("resolve");
208 let dw = r.datawriter_qos.as_ref().expect("dw");
209 assert_eq!(
211 dw.reliability.unwrap().kind,
212 zerodds_qos::ReliabilityKind::Reliable
213 );
214 assert_eq!(dw.history.unwrap().kind, HistoryKind::KeepAll);
216 }
217
218 #[test]
219 fn three_level_inheritance_propagates() {
220 let xml = r#"<dds><qos_library name="L">
221 <qos_profile name="A">
222 <datawriter_qos>
223 <durability><kind>VOLATILE</kind></durability>
224 <reliability><kind>BEST_EFFORT</kind></reliability>
225 </datawriter_qos>
226 </qos_profile>
227 <qos_profile name="B" base_name="A">
228 <datawriter_qos>
229 <durability><kind>TRANSIENT_LOCAL</kind></durability>
230 </datawriter_qos>
231 </qos_profile>
232 <qos_profile name="C" base_name="B">
233 <datawriter_qos>
234 <reliability><kind>RELIABLE</kind></reliability>
235 </datawriter_qos>
236 </qos_profile>
237 </qos_library></dds>"#;
238 let libs = parse(xml);
239 let r = resolve_profile(&libs, "L::C").expect("resolve");
240 let dw = r.datawriter_qos.as_ref().expect("dw");
241 assert_eq!(dw.durability.unwrap().kind, DurabilityKind::TransientLocal);
243 assert_eq!(
245 dw.reliability.unwrap().kind,
246 zerodds_qos::ReliabilityKind::Reliable
247 );
248 }
249
250 #[test]
251 fn cycle_detected() {
252 let xml = r#"<dds><qos_library name="L">
253 <qos_profile name="A" base_name="B"/>
254 <qos_profile name="B" base_name="A"/>
255 </qos_library></dds>"#;
256 let libs = parse(xml);
257 let err = resolve_profile(&libs, "L::A").expect_err("cycle");
258 assert!(matches!(err, XmlError::CircularInheritance(_)));
259 }
260
261 #[test]
262 fn unresolved_base_name_errors() {
263 let xml = r#"<dds><qos_library name="L">
264 <qos_profile name="A" base_name="DoesNotExist"/>
265 </qos_library></dds>"#;
266 let libs = parse(xml);
267 let err = resolve_profile(&libs, "L::A").expect_err("missing-base");
268 assert!(matches!(err, XmlError::UnresolvedReference(_)));
269 }
270
271 #[test]
272 fn missing_profile_in_library_errors() {
273 let libs = vec![QosLibrary {
274 name: "L".into(),
275 profiles: vec![],
276 }];
277 let err = resolve_profile(&libs, "L::Missing").expect_err("missing");
278 assert!(matches!(err, XmlError::UnresolvedReference(_)));
279 }
280
281 #[test]
282 fn missing_library_errors() {
283 let libs = vec![QosLibrary {
284 name: "L".into(),
285 profiles: vec![QosProfile {
286 name: "P".into(),
287 ..Default::default()
288 }],
289 }];
290 let err = resolve_profile(&libs, "Other::P").expect_err("missing-lib");
291 assert!(matches!(err, XmlError::UnresolvedReference(_)));
292 }
293
294 #[test]
295 fn deep_inheritance_cap_enforced() {
296 let mut xml = String::from(r#"<dds><qos_library name="L">"#);
298 for i in 0..40 {
299 if i == 0 {
300 xml.push_str(&format!(r#"<qos_profile name="P{i}"/>"#));
301 } else {
302 let prev = i - 1;
303 xml.push_str(&format!(
304 r#"<qos_profile name="P{i}" base_name="P{prev}"/>"#
305 ));
306 }
307 }
308 xml.push_str("</qos_library></dds>");
309 let libs = parse(&xml);
310 let err = resolve_profile(&libs, "L::P39").expect_err("depth");
311 assert!(matches!(err, XmlError::LimitExceeded(_)));
312 }
313
314 #[test]
315 fn cross_library_base_name_two_segment() {
316 let xml = r#"<dds>
317 <qos_library name="LibBase">
318 <qos_profile name="P">
319 <datawriter_qos>
320 <reliability><kind>RELIABLE</kind></reliability>
321 </datawriter_qos>
322 </qos_profile>
323 </qos_library>
324 <qos_library name="LibDerived">
325 <qos_profile name="C" base_name="LibBase::P">
326 <datawriter_qos>
327 <history><kind>KEEP_ALL</kind></history>
328 </datawriter_qos>
329 </qos_profile>
330 </qos_library>
331 </dds>"#;
332 let libs = parse(xml);
333 let r = resolve_profile(&libs, "LibDerived::C").expect("resolve");
334 let dw = r.datawriter_qos.as_ref().expect("dw");
335 assert_eq!(
336 dw.reliability.unwrap().kind,
337 zerodds_qos::ReliabilityKind::Reliable
338 );
339 assert_eq!(dw.history.unwrap().kind, HistoryKind::KeepAll);
340 }
341
342 #[test]
343 fn topic_filter_inherited_and_overridden() {
344 let xml = r#"<dds><qos_library name="L">
345 <qos_profile name="A">
346 <topic_filter>foo_*</topic_filter>
347 </qos_profile>
348 <qos_profile name="B" base_name="A"/>
349 <qos_profile name="C" base_name="A">
350 <topic_filter>bar_*</topic_filter>
351 </qos_profile>
352 </qos_library></dds>"#;
353 let libs = parse(xml);
354 let rb = resolve_profile(&libs, "L::B").expect("B");
355 assert_eq!(rb.topic_filter.as_deref(), Some("foo_*"));
356 let rc = resolve_profile(&libs, "L::C").expect("C");
357 assert_eq!(rc.topic_filter.as_deref(), Some("bar_*"));
358 }
359}