use zerodds_dcps::qos::{
DataReaderQos, DataWriterQos, DomainParticipantQos, PublisherQos, SubscriberQos, TopicQos,
};
use zerodds_web::bridge::BackendError;
use zerodds_xml::{EntityQos, FilteredEntityQos};
use crate::metadata::MetadataStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityKind {
Topic,
Publisher,
Subscriber,
DataWriter,
DataReader,
DomainParticipant,
}
impl EntityKind {
fn container(self) -> &'static str {
match self {
Self::Topic => "topic_qos",
Self::Publisher => "publisher_qos",
Self::Subscriber => "subscriber_qos",
Self::DataWriter => "datawriter_qos",
Self::DataReader => "datareader_qos",
Self::DomainParticipant => "domain_participant_qos",
}
}
fn slots(self, r: &zerodds_xml::ResolvedQos) -> &[FilteredEntityQos] {
match self {
Self::Topic => &r.topic_qos,
Self::Publisher => &r.publisher_qos,
Self::Subscriber => &r.subscriber_qos,
Self::DataWriter => &r.datawriter_qos,
Self::DataReader => &r.datareader_qos,
Self::DomainParticipant => &r.domainparticipant_qos,
}
}
}
pub fn resolve_entity_qos(
body: &[u8],
metadata: &MetadataStore,
kind: EntityKind,
) -> Result<Option<EntityQos>, BackendError> {
resolve_entity_qos_inner(body, metadata, kind, None)
}
pub fn resolve_entity_qos_for_topic(
body: &[u8],
metadata: &MetadataStore,
kind: EntityKind,
topic_name: &str,
) -> Result<Option<EntityQos>, BackendError> {
resolve_entity_qos_inner(body, metadata, kind, Some(topic_name))
}
fn resolve_entity_qos_inner(
body: &[u8],
metadata: &MetadataStore,
kind: EntityKind,
topic_name: Option<&str>,
) -> Result<Option<EntityQos>, 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);
}
let doc = zerodds_xml::parse_xml_tree(text)
.map_err(|e| BackendError::BadRequest(format!("malformed object body: {e}")))?;
resolve_entity_qos_from_element_inner(&doc.root, metadata, kind, topic_name)
}
pub fn resolve_entity_qos_from_element(
el: &zerodds_xml::XmlElement,
metadata: &MetadataStore,
kind: EntityKind,
) -> Result<Option<EntityQos>, BackendError> {
resolve_entity_qos_from_element_inner(el, metadata, kind, None)
}
pub fn resolve_entity_qos_from_element_for_topic(
el: &zerodds_xml::XmlElement,
metadata: &MetadataStore,
kind: EntityKind,
topic_name: &str,
) -> Result<Option<EntityQos>, BackendError> {
resolve_entity_qos_from_element_inner(el, metadata, kind, Some(topic_name))
}
fn resolve_entity_qos_from_element_inner(
el: &zerodds_xml::XmlElement,
metadata: &MetadataStore,
kind: EntityKind,
topic_name: Option<&str>,
) -> Result<Option<EntityQos>, BackendError> {
let profile_eq = match el.child("qos_profile") {
Some(el) => {
let path = el.text.trim();
if path.is_empty() {
None
} else {
let resolved = resolve_qos_profile_ref(metadata, path)?;
match topic_name {
Some(topic) => {
zerodds_xml::select_qos_for_topic(kind.slots(&resolved), topic).cloned()
}
None => zerodds_xml::default_qos(kind.slots(&resolved)).cloned(),
}
}
}
None => None,
};
let inline_eq = match el.child("qos") {
Some(el) => {
let mut as_container = el.clone();
as_container.name = kind.container().to_string();
let mut inline = zerodds_xml::parse_entity_qos_public(&as_container)
.map_err(|e| BackendError::BadRequest(format!("<qos>: {e}")))?;
match inline.base_name.take() {
Some(base) => {
let resolved = resolve_qos_profile_ref(metadata, &base)?;
let slots = kind.slots(&resolved);
let base_eq = match topic_name {
Some(topic) => zerodds_xml::select_qos_for_topic(slots, topic),
None => zerodds_xml::default_qos(slots),
};
Some(match base_eq {
Some(b) => b.clone().merge(&inline),
None => inline,
})
}
None => Some(inline),
}
}
None => None,
};
Ok(match (profile_eq, inline_eq) {
(None, None) => None,
(Some(p), None) => Some(p),
(None, Some(i)) => Some(i),
(Some(p), Some(i)) => Some(p.merge(&i)),
})
}
pub fn resolve_qos_profile_ref(
metadata: &MetadataStore,
path: &str,
) -> Result<zerodds_xml::ResolvedQos, BackendError> {
let libraries = all_qos_libraries(metadata);
zerodds_xml::resolve_profile(&libraries, path)
.map_err(|e| BackendError::BadRequest(format!("<qos_profile>{path}</qos_profile>: {e}")))
}
fn all_qos_libraries(metadata: &MetadataStore) -> Vec<zerodds_xml::QosLibrary> {
metadata
.qos_libraries
.names()
.into_iter()
.map(|lib_name| {
let profiles = metadata
.qos_profiles
.names()
.into_iter()
.filter_map(|key| {
let (lib, _profile) = key.split_once('/')?;
(lib == lib_name).then_some(key)
})
.filter_map(|key| {
let doc = metadata.qos_profiles.get(&key).ok()?;
let text = core::str::from_utf8(&doc.body).ok()?;
parse_stored_qos_profile(text).ok()
})
.collect();
zerodds_xml::QosLibrary {
name: lib_name,
profiles,
}
})
.collect()
}
fn parse_stored_qos_profile(xml: &str) -> Result<zerodds_xml::QosProfile, BackendError> {
let doc = zerodds_xml::parse_xml_tree(xml)
.map_err(|e| BackendError::BadRequest(format!("stored qos_profile: {e}")))?;
let root = &doc.root;
let mut profile = zerodds_xml::QosProfile {
name: root
.child("name")
.map(|e| e.text.clone())
.unwrap_or_default(),
base_name: root.child("base_name").map(|e| e.text.clone()),
topic_filter: root.child("topic_filter").map(|e| e.text.clone()),
datawriter_qos: Vec::new(),
datareader_qos: Vec::new(),
topic_qos: Vec::new(),
publisher_qos: Vec::new(),
subscriber_qos: Vec::new(),
domainparticipant_qos: Vec::new(),
};
let slot =
|el: &zerodds_xml::XmlElement, kind: &str| -> Result<FilteredEntityQos, BackendError> {
Ok(FilteredEntityQos {
topic_filter: None,
name: None,
qos: zerodds_xml::parse_entity_qos_public(el)
.map_err(|e| BackendError::BadRequest(format!("{kind}: {e}")))?,
})
};
if let Some(el) = root.child("datawriter_qos") {
profile.datawriter_qos.push(slot(el, "datawriter_qos")?);
}
if let Some(el) = root.child("datareader_qos") {
profile.datareader_qos.push(slot(el, "datareader_qos")?);
}
if let Some(el) = root.child("topic_qos") {
profile.topic_qos.push(slot(el, "topic_qos")?);
}
if let Some(el) = root.child("publisher_qos") {
profile.publisher_qos.push(slot(el, "publisher_qos")?);
}
if let Some(el) = root.child("subscriber_qos") {
profile.subscriber_qos.push(slot(el, "subscriber_qos")?);
}
if let Some(el) = root.child("domainparticipant_qos") {
profile
.domainparticipant_qos
.push(slot(el, "domainparticipant_qos")?);
}
Ok(profile)
}
#[must_use]
pub fn topic_qos_from(eq: &EntityQos, mut base: TopicQos) -> TopicQos {
if let Some(p) = eq.durability {
base.durability = p;
}
if let Some(p) = eq.durability_service {
base.durability_service = p;
}
if let Some(p) = eq.deadline {
base.deadline = p;
}
if let Some(p) = eq.latency_budget {
base.latency_budget = p;
}
if let Some(p) = eq.liveliness {
base.liveliness = p;
}
if let Some(p) = eq.reliability {
base.reliability = p;
}
if let Some(p) = eq.destination_order {
base.destination_order = p;
}
if let Some(p) = eq.history {
base.history = p;
}
if let Some(p) = eq.resource_limits {
base.resource_limits = p;
}
if let Some(p) = eq.transport_priority {
base.transport_priority = p;
}
if let Some(p) = eq.lifespan {
base.lifespan = p;
}
if let Some(p) = eq.ownership {
base.ownership = p;
}
if let Some(p) = eq.topic_data.clone() {
base.topic_data = p;
}
base
}
#[must_use]
pub fn publisher_qos_from(eq: &EntityQos, mut base: PublisherQos) -> PublisherQos {
if let Some(p) = eq.presentation {
base.presentation = p;
}
if let Some(p) = eq.partition.clone() {
base.partition = p;
}
if let Some(p) = eq.group_data.clone() {
base.group_data = p;
}
if let Some(p) = eq.entity_factory {
base.entity_factory = p;
}
base
}
#[must_use]
pub fn subscriber_qos_from(eq: &EntityQos, mut base: SubscriberQos) -> SubscriberQos {
if let Some(p) = eq.presentation {
base.presentation = p;
}
if let Some(p) = eq.partition.clone() {
base.partition = p;
}
if let Some(p) = eq.group_data.clone() {
base.group_data = p;
}
if let Some(p) = eq.entity_factory {
base.entity_factory = p;
}
base
}
#[must_use]
pub fn participant_qos_from(
eq: &EntityQos,
mut base: DomainParticipantQos,
) -> DomainParticipantQos {
if let Some(p) = eq.user_data.clone() {
base.user_data = p;
}
if let Some(p) = eq.entity_factory {
base.entity_factory = p;
}
base
}
#[must_use]
pub fn writer_qos_from(eq: &EntityQos, mut base: DataWriterQos) -> DataWriterQos {
if let Some(p) = eq.reliability {
base.reliability = p;
}
if let Some(p) = eq.durability {
base.durability = p;
}
if let Some(p) = eq.durability_service {
base.durability_service = p;
}
if let Some(p) = eq.deadline {
base.deadline = p;
}
if let Some(p) = eq.latency_budget {
base.latency_budget = p;
}
if let Some(p) = eq.liveliness {
base.liveliness = p;
}
if let Some(p) = eq.destination_order {
base.destination_order = p;
}
if let Some(p) = eq.lifespan {
base.lifespan = p;
}
if let Some(p) = eq.ownership {
base.ownership = p;
}
if let Some(p) = eq.ownership_strength {
base.ownership_strength = p;
}
if let Some(p) = eq.partition.clone() {
base.partition = p;
}
if let Some(p) = eq.presentation {
base.presentation = p;
}
if let Some(p) = eq.history {
base.history = p;
}
if let Some(p) = eq.resource_limits {
base.resource_limits = p;
}
if let Some(p) = eq.transport_priority {
base.transport_priority = p;
}
if let Some(p) = eq.writer_data_lifecycle {
base.writer_data_lifecycle = p;
}
if let Some(p) = eq.user_data.clone() {
base.user_data = p;
}
if let Some(p) = eq.topic_data.clone() {
base.topic_data = p;
}
if let Some(p) = eq.group_data.clone() {
base.group_data = p;
}
base
}
#[must_use]
pub fn reader_qos_from(eq: &EntityQos, mut base: DataReaderQos) -> DataReaderQos {
if let Some(p) = eq.reliability {
base.reliability = p;
}
if let Some(p) = eq.durability {
base.durability = p;
}
if let Some(p) = eq.deadline {
base.deadline = p;
}
if let Some(p) = eq.latency_budget {
base.latency_budget = p;
}
if let Some(p) = eq.liveliness {
base.liveliness = p;
}
if let Some(p) = eq.destination_order {
base.destination_order = p;
}
if let Some(p) = eq.ownership {
base.ownership = p;
}
if let Some(p) = eq.partition.clone() {
base.partition = p;
}
if let Some(p) = eq.presentation {
base.presentation = p;
}
if let Some(p) = eq.history {
base.history = p;
}
if let Some(p) = eq.resource_limits {
base.resource_limits = p;
}
if let Some(p) = eq.time_based_filter {
base.time_based_filter = p;
}
if let Some(p) = eq.reader_data_lifecycle {
base.reader_data_lifecycle = p;
}
if let Some(p) = eq.user_data.clone() {
base.user_data = p;
}
if let Some(p) = eq.topic_data.clone() {
base.topic_data = p;
}
if let Some(p) = eq.group_data.clone() {
base.group_data = p;
}
base
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn topic(body: &[u8]) -> Option<EntityQos> {
resolve_entity_qos(body, &MetadataStore::new(), EntityKind::Topic).unwrap()
}
fn publisher(body: &[u8]) -> Option<EntityQos> {
resolve_entity_qos(body, &MetadataStore::new(), EntityKind::Publisher).unwrap()
}
fn participant(body: &[u8]) -> Option<EntityQos> {
resolve_entity_qos(body, &MetadataStore::new(), EntityKind::DomainParticipant).unwrap()
}
fn writer(body: &[u8]) -> Option<EntityQos> {
resolve_entity_qos(body, &MetadataStore::new(), EntityKind::DataWriter).unwrap()
}
fn reader(body: &[u8]) -> Option<EntityQos> {
resolve_entity_qos(body, &MetadataStore::new(), EntityKind::DataReader).unwrap()
}
#[test]
fn empty_body_has_no_qos() {
assert_eq!(topic(b""), None);
}
#[test]
fn a_body_with_no_qos_element_has_no_qos() {
assert_eq!(topic(b"<topic><name>t</name></topic>"), None);
}
#[test]
fn malformed_xml_is_a_bad_request() {
let err = resolve_entity_qos(b"<topic><qos>", &MetadataStore::new(), EntityKind::Topic)
.unwrap_err();
assert!(matches!(err, BackendError::BadRequest(_)));
}
#[test]
fn an_unmodelled_policy_element_is_a_bad_request() {
let err = resolve_entity_qos(
b"<topic><qos><not_a_policy/></qos></topic>",
&MetadataStore::new(),
EntityKind::Topic,
)
.unwrap_err();
assert!(matches!(err, BackendError::BadRequest(_)));
}
#[test]
fn topic_qos_merges_reliability_onto_base_and_leaves_the_rest() {
let body = b"<topic><qos><reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability></qos></topic>";
let eq = topic(body).expect("qos present");
let mut base = TopicQos::default();
base.lifespan.duration = zerodds_qos::Duration::from_secs(42_i32);
let q = topic_qos_from(&eq, base.clone());
assert_eq!(
q.reliability.kind,
zerodds_dcps::qos::ReliabilityKind::Reliable
);
assert_eq!(q.lifespan, base.lifespan);
}
#[test]
fn publisher_qos_reads_partition() {
let body = b"<publisher><qos><partition><name><element>g1</element></name></partition></qos></publisher>";
let eq = publisher(body).expect("qos present");
let q = publisher_qos_from(&eq, PublisherQos::default());
assert_eq!(q.partition.names, vec!["g1".to_string()]);
}
#[test]
fn participant_qos_reads_user_data() {
let body =
b"<domain_participant><qos><user_data><value>aGk=</value></user_data></qos></domain_participant>";
let eq = participant(body).expect("qos present");
let q = participant_qos_from(&eq, DomainParticipantQos::default());
assert_eq!(q.user_data.value, b"hi".to_vec());
}
#[test]
fn writer_qos_merges_history_and_leaves_resource_limits_from_base() {
let body = b"<data_writer><qos><history><kind>KEEP_ALL_HISTORY_QOS</kind></history></qos></data_writer>";
let eq = writer(body).expect("qos present");
let mut base = DataWriterQos::default();
base.resource_limits.max_samples = 42;
let q = writer_qos_from(&eq, base.clone());
assert_eq!(q.history.kind, zerodds_dcps::qos::HistoryKind::KeepAll);
assert_eq!(q.resource_limits, base.resource_limits);
}
#[test]
fn reader_qos_reads_reliability_and_leaves_the_rest_from_base() {
let body = b"<data_reader><qos><reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability></qos></data_reader>";
let eq = reader(body).expect("qos present");
let base = DataReaderQos::default();
let q = reader_qos_from(&eq, base.clone());
assert_eq!(
q.reliability.kind,
zerodds_dcps::qos::ReliabilityKind::Reliable
);
assert_eq!(q.resource_limits, base.resource_limits);
}
fn store_profile(metadata: &MetadataStore, library: &str, profile_xml: &str) {
if metadata.qos_libraries.get(library).is_err() {
metadata
.qos_libraries
.create(
library,
crate::metadata::StoredDocument {
content_type: "application/dds-web+xml".to_string(),
body: format!("<qos_library><name>{library}</name></qos_library>")
.into_bytes(),
},
)
.unwrap();
}
let name_start = profile_xml.find("<name>").expect("profile has a name") + 6;
let name_end = profile_xml.find("</name>").expect("profile has a name");
let profile_name = &profile_xml[name_start..name_end];
metadata
.qos_profiles
.create(
&MetadataStore::profile_key(library, profile_name),
crate::metadata::StoredDocument {
content_type: "application/dds-web+xml".to_string(),
body: profile_xml.as_bytes().to_vec(),
},
)
.unwrap();
}
#[test]
fn qos_profile_reference_resolves_a_stored_profile() {
let metadata = MetadataStore::new();
store_profile(
&metadata,
"Lib",
"<qos_profile><name>Reliable</name><topic_qos><reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability></topic_qos></qos_profile>",
);
let body = b"<topic><qos_profile>Lib::Reliable</qos_profile></topic>";
let eq = resolve_entity_qos(body, &metadata, EntityKind::Topic)
.unwrap()
.expect("profile resolved");
assert_eq!(
eq.reliability.expect("reliability set").kind,
zerodds_dcps::qos::ReliabilityKind::Reliable
);
}
#[test]
fn qos_profile_reference_to_an_unknown_profile_is_a_bad_request() {
let metadata = MetadataStore::new();
let body = b"<topic><qos_profile>NoSuchLib::NoSuchProfile</qos_profile></topic>";
let err = resolve_entity_qos(body, &metadata, EntityKind::Topic).unwrap_err();
assert!(matches!(err, BackendError::BadRequest(_)));
}
#[test]
fn inline_qos_overrides_the_profiles_value_for_the_same_policy() {
let metadata = MetadataStore::new();
store_profile(
&metadata,
"Lib",
"<qos_profile><name>P</name><topic_qos><reliability><kind>BEST_EFFORT_RELIABILITY_QOS</kind></reliability><history><kind>KEEP_ALL_HISTORY_QOS</kind></history></topic_qos></qos_profile>",
);
let body = b"<topic><qos_profile>Lib::P</qos_profile><qos><reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability></qos></topic>";
let eq = resolve_entity_qos(body, &metadata, EntityKind::Topic)
.unwrap()
.expect("resolved");
assert_eq!(
eq.reliability.expect("reliability set").kind,
zerodds_dcps::qos::ReliabilityKind::Reliable
);
assert_eq!(
eq.history.expect("history set").kind,
zerodds_dcps::qos::HistoryKind::KeepAll
);
}
#[test]
fn inline_qos_base_name_inherits_from_a_stored_profile() {
let metadata = MetadataStore::new();
store_profile(
&metadata,
"Lib",
"<qos_profile><name>P</name><topic_qos><reliability><kind>RELIABLE_RELIABILITY_QOS</kind></reliability><history><kind>KEEP_ALL_HISTORY_QOS</kind></history></topic_qos></qos_profile>",
);
let body = b"<topic><qos base_name=\"Lib::P\"><history><kind>KEEP_LAST_HISTORY_QOS</kind><depth>3</depth></history></qos></topic>";
let eq = resolve_entity_qos(body, &metadata, EntityKind::Topic)
.unwrap()
.expect("resolved");
assert_eq!(
eq.reliability.expect("reliability set").kind,
zerodds_dcps::qos::ReliabilityKind::Reliable
);
let history = eq.history.expect("history set");
assert_eq!(history.kind, zerodds_dcps::qos::HistoryKind::KeepLast);
assert_eq!(history.depth, 3);
assert!(eq.base_name.is_none());
}
#[test]
fn inline_qos_base_name_of_an_unknown_profile_is_a_bad_request() {
let body = b"<topic><qos base_name=\"Lib::Missing\"/></topic>";
let err = resolve_entity_qos(body, &MetadataStore::new(), EntityKind::Topic).unwrap_err();
assert!(matches!(err, BackendError::BadRequest(_)));
}
#[test]
fn inline_qos_policy_the_entity_lacks_is_a_bad_request() {
let metadata = MetadataStore::new();
for (kind, body) in [
(
EntityKind::DataReader,
&b"<data_reader><qos><ownership_strength><value>3</value></ownership_strength></qos></data_reader>"[..],
),
(
EntityKind::DataWriter,
&b"<data_writer><qos><time_based_filter/></qos></data_writer>"[..],
),
(
EntityKind::Publisher,
&b"<publisher><qos><reliability/></qos></publisher>"[..],
),
(
EntityKind::Topic,
&b"<topic><qos><partition/></qos></topic>"[..],
),
(
EntityKind::DomainParticipant,
&b"<domain_participant><qos><history/></qos></domain_participant>"[..],
),
] {
let err = resolve_entity_qos(body, &metadata, kind).unwrap_err();
assert!(matches!(err, BackendError::BadRequest(_)), "{kind:?}: {err:?}");
}
let ok = resolve_entity_qos(
b"<data_writer><qos><ownership_strength><value>3</value></ownership_strength></qos></data_writer>",
&metadata,
EntityKind::DataWriter,
)
.unwrap()
.expect("qos");
assert_eq!(ok.ownership_strength.unwrap().value, 3);
}
}