use zerodds_xml_wire::parser::{Event, XmlParser};
#[derive(Debug, Clone, Default)]
pub struct FlatFields {
scalars: std::collections::BTreeMap<String, String>,
lists: std::collections::BTreeMap<String, Vec<String>>,
}
impl FlatFields {
#[must_use]
pub fn get(&self, tag: &str) -> Option<&str> {
self.scalars.get(tag).map(String::as_str)
}
#[must_use]
pub fn list(&self, tag: &str) -> Vec<String> {
self.lists.get(tag).cloned().unwrap_or_default()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError(pub String);
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "malformed object body: {}", self.0)
}
}
impl std::error::Error for ParseError {}
pub fn parse(xml: &str) -> Result<FlatFields, ParseError> {
let mut out = FlatFields::default();
let mut depth: u32 = 0;
let mut current_tag: Option<String> = None;
let mut current_text = String::new();
let mut current_is_list = false;
for event in XmlParser::new(xml) {
let event = event.map_err(|e| ParseError(e.to_string()))?;
match event {
Event::Declaration(_) | Event::CData(_) => {}
Event::StartElement { name, .. } => {
depth += 1;
if depth == 2 {
current_tag = Some(name);
current_text.clear();
current_is_list = false;
} else if depth == 3 && current_tag.is_some() {
current_is_list = true;
current_text.clear();
}
}
Event::Text(text) => {
if depth >= 2 {
current_text.push_str(text.trim());
}
}
Event::EndElement(name) => {
if depth == 3 {
if let Some(tag) = ¤t_tag {
out.lists
.entry(tag.clone())
.or_default()
.push(current_text.trim().to_string());
}
current_text.clear();
} else if depth == 2 {
if !current_is_list {
if let Some(tag) = current_tag.take() {
out.scalars.insert(tag, current_text.trim().to_string());
}
}
current_tag = None;
current_text.clear();
current_is_list = false;
}
let _ = &name;
depth = depth.saturating_sub(1);
}
}
}
Ok(out)
}
pub fn child_xml(xml: &str, tag: &str) -> Result<Option<String>, ParseError> {
use zerodds_xml_wire::emitter::XmlEmitter;
let mut out = XmlEmitter::new();
let mut depth: u32 = 0;
let mut inside = false;
for event in XmlParser::new(xml) {
let event = event.map_err(|e| ParseError(e.to_string()))?;
match event {
Event::Declaration(_) => {}
Event::StartElement { name, attrs } => {
depth += 1;
if depth == 2 && name == tag {
inside = true;
}
if inside {
let attrs: Vec<(&str, &str)> = attrs
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
out.start_element(&name, &attrs)
.map_err(|e| ParseError(format!("{e:?}")))?;
}
}
Event::Text(text) if inside => out.text(&text),
Event::CData(text) if inside => out.cdata(&text),
Event::Text(_) | Event::CData(_) => {}
Event::EndElement(_) => {
if inside {
out.end_element()
.map_err(|e| ParseError(format!("{e:?}")))?;
if depth == 2 {
return Ok(Some(out.finish()));
}
}
depth = depth.saturating_sub(1);
}
}
}
Ok(None)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn scalar_fields_are_read() {
let f =
parse("<topic><name>Sensor</name><type_name>M::Sensor</type_name></topic>").unwrap();
assert_eq!(f.get("name"), Some("Sensor"));
assert_eq!(f.get("type_name"), Some("M::Sensor"));
}
#[test]
fn list_field_collects_items() {
let f = parse(
"<waitset><name>ws1</name><readers><item>a</item><item>b</item></readers></waitset>",
)
.unwrap();
assert_eq!(f.get("name"), Some("ws1"));
assert_eq!(f.list("readers"), vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn missing_field_is_none() {
let f = parse("<topic><name>Sensor</name></topic>").unwrap();
assert_eq!(f.get("type_name"), None);
}
#[test]
fn a_child_subtree_comes_back_as_xml() {
let body = "<WriteSample><writer>dw1</writer>\
<data><sensor_id>6</sensor_id><name>a & b</name></data></WriteSample>";
assert_eq!(
child_xml(body, "data").unwrap().as_deref(),
Some("<data><sensor_id>6</sensor_id><name>a & b</name></data>")
);
assert_eq!(
child_xml(body, "sensor_id").unwrap(),
None,
"not a direct child"
);
assert_eq!(child_xml(body, "missing").unwrap(), None);
}
#[test]
fn malformed_xml_is_an_error() {
assert!(parse("<topic><name>Sensor</topic>").is_err());
}
}