use zerodds_web::bridge::BackendError;
use zerodds_xml::{EntityQos, XmlElement};
use crate::qos_update::EntityKind;
use crate::metadata::MetadataStore;
#[derive(Debug, Clone)]
pub struct ParsedDataWriter {
pub name: String,
pub topic_name: String,
pub qos: Option<EntityQos>,
}
#[derive(Debug, Clone)]
pub struct ParsedDataReader {
pub name: String,
pub topic_name: String,
pub qos: Option<EntityQos>,
}
#[derive(Debug, Clone)]
pub struct ParsedPublisher {
pub name: String,
pub qos: Option<EntityQos>,
pub data_writers: Vec<ParsedDataWriter>,
}
#[derive(Debug, Clone)]
pub struct ParsedSubscriber {
pub name: String,
pub qos: Option<EntityQos>,
pub data_readers: Vec<ParsedDataReader>,
}
fn required_name(el: &XmlElement, what: &str) -> Result<String, BackendError> {
el.child("name")
.map(|e| e.text.clone())
.filter(|s| !s.is_empty())
.ok_or_else(|| BackendError::BadRequest(format!("a contained {what} needs a <name>")))
}
pub fn parse_publishers(
root: &XmlElement,
metadata: &MetadataStore,
) -> Result<Vec<ParsedPublisher>, BackendError> {
let Some(container) = root.child("publishers") else {
return Ok(Vec::new());
};
container
.sequence_elements()
.map(|el| parse_publisher(el, metadata))
.collect()
}
fn parse_publisher(
el: &XmlElement,
metadata: &MetadataStore,
) -> Result<ParsedPublisher, BackendError> {
let name = required_name(el, "publisher")?;
let qos =
crate::qos_update::resolve_entity_qos_from_element(el, metadata, EntityKind::Publisher)?;
let data_writers = parse_data_writers(el, metadata)?;
Ok(ParsedPublisher {
name,
qos,
data_writers,
})
}
pub fn parse_data_writers(
root: &XmlElement,
metadata: &MetadataStore,
) -> Result<Vec<ParsedDataWriter>, BackendError> {
let Some(container) = root.child("data_writers") else {
return Ok(Vec::new());
};
container
.sequence_elements()
.map(|el| {
let name = required_name(el, "data_writer")?;
let topic_name = el
.child("topic_name")
.map(|e| e.text.clone())
.unwrap_or_default();
let qos = crate::qos_update::resolve_entity_qos_from_element_for_topic(
el,
metadata,
EntityKind::DataWriter,
&topic_name,
)?;
Ok(ParsedDataWriter {
name,
topic_name,
qos,
})
})
.collect()
}
pub fn parse_subscribers(
root: &XmlElement,
metadata: &MetadataStore,
) -> Result<Vec<ParsedSubscriber>, BackendError> {
let Some(container) = root.child("subscribers") else {
return Ok(Vec::new());
};
container
.sequence_elements()
.map(|el| parse_subscriber(el, metadata))
.collect()
}
fn parse_subscriber(
el: &XmlElement,
metadata: &MetadataStore,
) -> Result<ParsedSubscriber, BackendError> {
let name = required_name(el, "subscriber")?;
let qos =
crate::qos_update::resolve_entity_qos_from_element(el, metadata, EntityKind::Subscriber)?;
let data_readers = parse_data_readers(el, metadata)?;
Ok(ParsedSubscriber {
name,
qos,
data_readers,
})
}
pub fn parse_data_readers(
root: &XmlElement,
metadata: &MetadataStore,
) -> Result<Vec<ParsedDataReader>, BackendError> {
let Some(container) = root.child("data_readers") else {
return Ok(Vec::new());
};
container
.sequence_elements()
.map(|el| {
let name = required_name(el, "data_reader")?;
let topic_name = el
.child("topic_name")
.map(|e| e.text.clone())
.unwrap_or_default();
let qos = crate::qos_update::resolve_entity_qos_from_element_for_topic(
el,
metadata,
EntityKind::DataReader,
&topic_name,
)?;
Ok(ParsedDataReader {
name,
topic_name,
qos,
})
})
.collect()
}
pub fn parse_body(body: &[u8]) -> Result<Option<zerodds_xml::DdsXmlDocument>, BackendError> {
if body.is_empty() {
return Ok(None);
}
let text = core::str::from_utf8(body)
.map_err(|e| BackendError::BadRequest(format!("body is not UTF-8: {e}")))?;
if text.trim().is_empty() {
return Ok(None);
}
zerodds_xml::parse_xml_tree(text)
.map(Some)
.map_err(|e| BackendError::BadRequest(format!("malformed object body: {e}")))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn a_body_with_no_publishers_element_has_none() {
let doc = parse_body(b"<domain_participant><domain_id>0</domain_id></domain_participant>")
.unwrap()
.unwrap();
assert!(
parse_publishers(&doc.root, &MetadataStore::new())
.unwrap()
.is_empty()
);
}
#[test]
fn nested_publishers_and_data_writers_parse() {
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>";
let doc = parse_body(body).unwrap().unwrap();
let pubs = parse_publishers(&doc.root, &MetadataStore::new()).unwrap();
assert_eq!(pubs.len(), 1);
assert_eq!(pubs[0].name, "pub1");
assert_eq!(pubs[0].data_writers.len(), 1);
assert_eq!(pubs[0].data_writers[0].name, "dw1");
assert_eq!(pubs[0].data_writers[0].topic_name, "Sensor");
}
#[test]
fn a_contained_entity_without_a_name_is_a_bad_request() {
let body =
b"<domain_participant><publishers><element><data_writers/></element></publishers></domain_participant>";
let doc = parse_body(body).unwrap().unwrap();
let err = parse_publishers(&doc.root, &MetadataStore::new()).unwrap_err();
assert!(matches!(err, BackendError::BadRequest(_)));
}
#[test]
fn a_publishers_qos_profile_reference_resolves() {
let metadata = MetadataStore::new();
metadata
.qos_libraries
.create(
"Lib",
crate::metadata::StoredDocument {
content_type: "application/dds-web+xml".to_string(),
body: b"<qos_library><name>Lib</name></qos_library>".to_vec(),
},
)
.unwrap();
metadata
.qos_profiles
.create(
&MetadataStore::profile_key("Lib", "P"),
crate::metadata::StoredDocument {
content_type: "application/dds-web+xml".to_string(),
body: b"<qos_profile><name>P</name><publisher_qos><partition><name><element>g1</element></name></partition></publisher_qos></qos_profile>".to_vec(),
},
)
.unwrap();
let body = b"<domain_participant><publishers><element><name>pub1</name><qos_profile>Lib::P</qos_profile></element></publishers></domain_participant>";
let doc = parse_body(body).unwrap().unwrap();
let pubs = parse_publishers(&doc.root, &metadata).unwrap();
assert_eq!(
pubs[0]
.qos
.as_ref()
.unwrap()
.partition
.as_ref()
.unwrap()
.names,
vec!["g1".to_string()]
);
}
}