From 0ee4621fa3f00b3b5283056f82ab89b40f75873d Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 5 Apr 2026 14:50:35 -0600 Subject: [PATCH] Added config parsing and refactored parsing --- Cargo.toml | 5 +- data/config.xml | 195 ++++++++++++++++++++++++++++++ src/lib.rs | 79 +++++------- src/model/config.rs | 162 +++++++++++++++++++++++++ src/model/mod.rs | 71 +++++++++++ src/{model.rs => model/status.rs} | 147 +++++++++++----------- 6 files changed, 538 insertions(+), 121 deletions(-) create mode 100644 data/config.xml create mode 100644 src/model/config.rs create mode 100644 src/model/mod.rs rename src/{model.rs => model/status.rs} (53%) diff --git a/Cargo.toml b/Cargo.toml index 894a28f..f7af37a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "rust-dsn-parser" -version = "0.3.0" +version = "1.0.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -xml-rs = "1.0.0" \ No newline at end of file +thiserror = "2.0.18" +xml-rs = "1.0.0" diff --git a/data/config.xml b/data/config.xml new file mode 100644 index 0000000..2b3f60e --- /dev/null +++ b/data/config.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib.rs b/src/lib.rs index c06ca6c..11a1485 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,75 +3,56 @@ pub mod model; pub mod prelude; -use std::num::{ParseFloatError, ParseIntError}; -use std::str::ParseBoolError; +use thiserror::Error; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum DsnRespParseError { - AttrMissing(String), - ParseIntErr(ParseIntError), - ParseFloatErr(ParseFloatError), - ParseBoolErr(ParseBoolError), + #[error("Failed to read XML: {0}")] XMLReaderError(xml::reader::Error), + #[error("Failed to parse element '{element}': {err}")] + ElementParsingError { element: String, err: DsnFieldError }, } -impl From for DsnRespParseError { - fn from(error: ParseIntError) -> Self { - DsnRespParseError::ParseIntErr(error) - } -} - -impl From for DsnRespParseError { - fn from(error: ParseFloatError) -> Self { - DsnRespParseError::ParseFloatErr(error) - } -} - -impl From for DsnRespParseError { - fn from(error: ParseBoolError) -> Self { - DsnRespParseError::ParseBoolErr(error) - } -} - -impl From for DsnRespParseError { - fn from(error: xml::reader::Error) -> Self { - DsnRespParseError::XMLReaderError(error) - } -} - -impl std::fmt::Display for DsnRespParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DsnRespParseError::AttrMissing(attr) => write!(f, "Attr missing: {}", attr), - DsnRespParseError::ParseIntErr(e) => write!(f, "Int parse error: {}", e), - DsnRespParseError::ParseFloatErr(e) => write!(f, "Int parse error: {}", e), - DsnRespParseError::ParseBoolErr(e) => write!(f, "Bool parse error: {}", e), - DsnRespParseError::XMLReaderError(e) => write!(f, "XML Parse error: {}", e), - } - } +#[derive(Error, Debug)] +pub enum DsnFieldError { + #[error("Attribute '{0}' is missing")] + AttrMissing(String), + #[error("Failed to parse attribute '{attribute}': {err}")] + AttributeParseFailed { err: String, attribute: String }, } #[cfg(test)] mod tests { - use crate::model::DsnResponse; + use crate::DsnRespParseError; + use crate::model::config::DsnConfig; + use crate::model::status::DsnStatus; + use crate::prelude::DsnModel; use std::fs::File; use std::io::BufReader; - fn parse_test_file() -> DsnResponse { + fn parse_test_file() -> Result { let example_file = File::open("data/dsn.xml").unwrap(); let buf_reader = BufReader::new(example_file); - DsnResponse::from_xml_response(buf_reader).unwrap() + DsnStatus::from_xml_response(buf_reader) + } + + fn parse_cfg_file() -> Result { + let example_file = File::open("data/config.xml").unwrap(); + let buf_reader = BufReader::new(example_file); + DsnConfig::from_xml_response(buf_reader) } #[test] fn test_parse() { - let response = parse_test_file(); - println!("{:?}", response.stations) + let response = parse_test_file().unwrap(); + + assert_eq!(response.stations.len(), 3); } #[test] - fn test_get_active_targets() { - let response = parse_test_file(); - println!("{:?}", response.get_active_targets()); + fn test_parse_config() { + let config = parse_cfg_file().unwrap(); + assert_eq!(config.sites.len(), 3); + assert_eq!(config.spacecraft_map.len(), 168) } } diff --git a/src/model/config.rs b/src/model/config.rs new file mode 100644 index 0000000..422170a --- /dev/null +++ b/src/model/config.rs @@ -0,0 +1,162 @@ +use crate::model::{DsnField, DsnModel}; +use crate::{DsnFieldError, DsnRespParseError}; +use std::collections::HashMap; +use std::io::{BufReader, Read}; +use xml::EventReader; +use xml::attribute::OwnedAttribute; +use xml::reader::XmlEvent; + +#[derive(Debug)] +pub struct DsnConfig { + pub sites: Vec, + pub spacecraft_map: HashMap, +} + +impl DsnModel for DsnConfig { + fn from_xml_response(reader: BufReader) -> Result + where + T: Read, + { + let mut sites = Vec::new(); + let mut site_cfg: Option = None; + + let mut spacecraft_map = HashMap::new(); + let mut spacecraft_cfg: Option = None; + + let parser = EventReader::new(reader).into_iter(); + for e in parser { + match e { + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + if name.local_name == "site" { + site_cfg = Some(SiteCfg::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + element: "site".to_string(), + err, + } + })?) + } else if let Some(site_cfg) = &mut site_cfg + && name.local_name == "dish" + { + site_cfg.add_dish(DishCfg::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "dish".to_string(), + } + })?) + } else if name.local_name == "spacecraft" { + spacecraft_cfg = + Some(SpacecraftMap::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "spacecraft".to_string(), + } + })?); + } + } + Ok(XmlEvent::EndElement { name }) => { + if name.local_name == "site" { + if let Some(site_cfg) = &site_cfg { + sites.push(site_cfg.clone()); + } + site_cfg = None; + } else if name.local_name == "spacecraft" { + if let Some(spacecraft_cfg) = &spacecraft_cfg { + spacecraft_map + .insert(spacecraft_cfg.name.clone(), spacecraft_cfg.clone()); + } + spacecraft_cfg = None; + } + } + Ok(XmlEvent::EndDocument) => {} + Err(e) => { + return Err(DsnRespParseError::XMLReaderError(e)); + } + _ => {} + } + } + + Ok(Self { + sites, + spacecraft_map, + }) + } +} + +#[derive(Debug, Clone)] +pub struct SiteCfg { + pub name: String, + pub friendly_name: String, + pub longitude: f32, + pub latitude: f32, + pub dishes: Vec, +} + +impl SiteCfg { + pub fn add_dish(&mut self, dish: DishCfg) { + self.dishes.push(dish); + } +} + +impl DsnField for SiteCfg { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let name = Self::get_attr(attrs, "name")?; + let friendly_name = Self::get_attr(attrs, "friendlyName")?; + let longitude = Self::parse_attribute::(attrs, "longitude")?; + let latitude = Self::parse_attribute::(attrs, "latitude")?; + + Ok(Self { + name, + friendly_name, + longitude, + latitude, + dishes: vec![], + }) + } +} + +#[derive(Debug, Clone)] +pub struct DishCfg { + pub name: String, + pub friendly_name: String, + pub dish_type: String, +} + +impl DsnField for DishCfg { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let name = Self::get_attr(attrs, "name")?; + let friendly_name = Self::get_attr(attrs, "friendlyName")?; + let dish_type = Self::get_attr(attrs, "type")?; + + Ok(Self { + name, + friendly_name, + dish_type, + }) + } +} + +#[derive(Debug, Clone)] +pub struct SpacecraftMap { + pub name: String, + pub explorer_name: String, + pub friendly_name: String, + pub thumbnail: Option, +} + +impl DsnField for SpacecraftMap { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let name = Self::get_attr(attrs, "name")?; + let explorer_name = Self::get_attr(attrs, "explorerName")?; + let friendly_name = Self::get_attr(attrs, "friendlyName")?; + let thumbnail = Self::parse_optional_attribute::(attrs, "thumbnail")?; + + Ok(Self { + name, + explorer_name, + friendly_name, + thumbnail, + }) + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..d695f3b --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,71 @@ +pub mod config; +pub mod status; + +use crate::{DsnFieldError, DsnRespParseError}; +use std::fmt::Debug; +use std::io::BufReader; +use std::str::FromStr; +use xml::attribute::OwnedAttribute; +pub trait DsnModel: Sized { + fn from_xml_string(resp_str: &str) -> Result { + Self::from_xml_response(BufReader::new(resp_str.as_bytes())) + } + + fn from_xml_response(reader: BufReader) -> Result + where + T: std::io::Read; +} + +pub trait DsnField: Sized + Clone { + fn get_attr(attrs: &[OwnedAttribute], name: &str) -> Result { + attrs + .iter() + .find_map(|o| { + if o.name.local_name.eq(name) { + Some(o.value.clone()) + } else { + None + } + }) + .ok_or_else(|| DsnFieldError::AttrMissing(name.to_string())) + } + + fn parse(name: &str, value: &str) -> Result + where + ::Err: Debug, + { + value + .parse::() + .map_err(|err| DsnFieldError::AttributeParseFailed { + err: format!("{err:?}"), + attribute: name.to_string(), + }) + } + + fn parse_attribute(attrs: &[OwnedAttribute], name: &str) -> Result + where + ::Err: Debug, + { + Self::parse::(name, &Self::get_attr(attrs, name)?) + } + + fn parse_optional_attribute( + attrs: &[OwnedAttribute], + name: &str, + ) -> Result, DsnFieldError> + where + ::Err: Debug, + { + let attr = if let Ok(attr) = Self::get_attr(attrs, name) + && !attr.is_empty() + { + Some(Self::parse::(name, &attr)?) + } else { + None + }; + + Ok(attr) + } + + fn parse_field(attrs: &[OwnedAttribute]) -> Result; +} diff --git a/src/model.rs b/src/model/status.rs similarity index 53% rename from src/model.rs rename to src/model/status.rs index 8b99c6c..ad6c950 100644 --- a/src/model.rs +++ b/src/model/status.rs @@ -1,30 +1,16 @@ -use std::convert::TryFrom; +use crate::model::{DsnField, DsnModel}; +use crate::{DsnFieldError, DsnRespParseError}; use std::io::BufReader; - -use crate::DsnRespParseError; use xml::EventReader; use xml::attribute::OwnedAttribute; use xml::reader::XmlEvent; -fn get_attr(attrs: &[OwnedAttribute], name: &str) -> Result { - attrs - .iter() - .find_map(|o| { - if o.name.local_name.eq(name) { - Some(o.value.clone()) - } else { - None - } - }) - .ok_or_else(|| DsnRespParseError::AttrMissing(name.to_string())) -} - #[derive(Clone, Debug)] -pub struct DsnResponse { +pub struct DsnStatus { pub stations: Vec, } -impl DsnResponse { +impl DsnStatus { pub fn get_active_targets(&self) -> Vec { let mut targets = Vec::new(); @@ -36,12 +22,10 @@ impl DsnResponse { targets } +} - pub fn from_xml_string(resp_str: &str) -> Result { - Self::from_xml_response(BufReader::new(resp_str.as_bytes())) - } - - pub fn from_xml_response(reader: BufReader) -> Result +impl DsnModel for DsnStatus { + fn from_xml_response(reader: BufReader) -> Result where T: std::io::Read, { @@ -60,15 +44,46 @@ impl DsnResponse { if let Some(station) = station { stations.push(station); } - station = Some(Station::try_from(attributes)?); + station = Some(Station::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "station".to_string(), + } + })?); } else if name.local_name.contains("dish") { - dish = Some(Dish::try_from(attributes)?); + dish = Some(Dish::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "dish".to_string(), + } + })?); } else if name.local_name.contains("downSignal") { - dish = Some(dish.unwrap().add_down_signal(Signal::try_from(attributes)?)); + dish = Some(dish.unwrap().add_down_signal( + Signal::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "downSignal".to_string(), + } + })?, + )); } else if name.local_name.contains("upSignal") { - dish = Some(dish.unwrap().add_up_signal(Signal::try_from(attributes)?)); + dish = Some(dish.unwrap().add_up_signal( + Signal::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "upSignal".to_string(), + } + })?, + )); } else if name.local_name.contains("target") { - dish = Some(dish.unwrap().add_target(Target::try_from(attributes)?)); + dish = Some(dish.unwrap().add_target( + Target::parse_field(&attributes).map_err(|err| { + DsnRespParseError::ElementParsingError { + err, + element: "target".to_string(), + } + })?, + )); } } Ok(XmlEvent::EndElement { name }) => { @@ -84,13 +99,13 @@ impl DsnResponse { } } Err(e) => { - return Err(DsnRespParseError::from(e)); + return Err(DsnRespParseError::XMLReaderError(e)); } _ => {} } } - Ok(DsnResponse { stations }) + Ok(DsnStatus { stations }) } } @@ -110,14 +125,12 @@ impl Station { } } -impl TryFrom> for Station { - type Error = DsnRespParseError; - - fn try_from(attrs: Vec) -> Result { - let name = get_attr(&attrs, "name")?; - let friendly_name = get_attr(&attrs, "friendlyName")?; - let time_utc = get_attr(&attrs, "timeUTC")?.parse::()?; - let tz_offset = get_attr(&attrs, "timeZoneOffset")?.parse::()?; +impl DsnField for Station { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let name = Self::get_attr(attrs, "name")?; + let friendly_name = Self::get_attr(attrs, "friendlyName")?; + let time_utc = Self::parse_attribute::(attrs, "timeUTC")?; + let tz_offset = Self::parse_attribute::(attrs, "timeZoneOffset")?; Ok(Self { name, @@ -135,7 +148,7 @@ pub enum SignalType { Carrier, #[default] None, - Unkown(String), + Unknown(String), } impl From for SignalType { @@ -144,7 +157,7 @@ impl From for SignalType { "data" => SignalType::Data, "carrier" => SignalType::Carrier, "none" => SignalType::None, - _ => SignalType::Unkown(s), + _ => SignalType::Unknown(s), } } } @@ -159,16 +172,14 @@ pub struct Signal { pub spacecraft_id: Option, } -impl TryFrom> for Signal { - type Error = DsnRespParseError; - - fn try_from(attrs: Vec) -> Result { - let signal_type = SignalType::from(get_attr(&attrs, "signalType")?); - let data_rate = get_attr(&attrs, "dataRate")?.parse::().ok(); - let frequency = get_attr(&attrs, "frequency")?.parse::().ok(); - let power = get_attr(&attrs, "power")?.parse::().ok(); - let spacecraft = get_attr(&attrs, "spacecraft")?; - let spacecraft_id = get_attr(&attrs, "spacecraftID")?.parse::().ok(); +impl DsnField for Signal { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let signal_type = SignalType::from(Self::get_attr(attrs, "signalType")?); + let data_rate = Self::parse_optional_attribute::(attrs, "dataRate")?; + let frequency = Self::parse_optional_attribute::(attrs, "frequency")?; + let power = Self::parse_optional_attribute::(attrs, "power")?; + let spacecraft = Self::get_attr(attrs, "spacecraft")?; + let spacecraft_id = Self::parse_optional_attribute::(attrs, "spacecraftID")?; Ok(Self { signal_type, @@ -193,15 +204,13 @@ pub struct Target { pub rtlt: f64, } -impl TryFrom> for Target { - type Error = DsnRespParseError; - - fn try_from(attrs: Vec) -> Result { - let name = get_attr(&attrs, "name")?; - let id = get_attr(&attrs, "id")?.parse::()?; - let upleg_range = get_attr(&attrs, "uplegRange")?.parse::()?; - let downleg_range = get_attr(&attrs, "downlegRange")?.parse::()?; - let rtlt = get_attr(&attrs, "rtlt")?.parse::()?; +impl DsnField for Target { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let name = Self::get_attr(attrs, "name")?; + let id = Self::parse_attribute::(attrs, "id")?; + let upleg_range = Self::parse_attribute::(attrs, "uplegRange")?; + let downleg_range = Self::parse_attribute::(attrs, "downlegRange")?; + let rtlt = Self::parse_attribute::(attrs, "rtlt")?; Ok(Self { name, @@ -244,17 +253,15 @@ impl Dish { } } -impl TryFrom> for Dish { - type Error = DsnRespParseError; - - fn try_from(attrs: Vec) -> Result { - let name = get_attr(&attrs, "name")?; - let azimuth_angle = get_attr(&attrs, "azimuthAngle")?.parse::().ok(); - let elevation_angle = get_attr(&attrs, "elevationAngle")?.parse::().ok(); - let wind_speed = get_attr(&attrs, "windSpeed")?.parse::().ok(); - let is_mspa = get_attr(&attrs, "isMSPA")?.parse::()?; - let is_array = get_attr(&attrs, "isArray")?.parse::()?; - let is_ddor = get_attr(&attrs, "isDDOR")?.parse::()?; +impl DsnField for Dish { + fn parse_field(attrs: &[OwnedAttribute]) -> Result { + let name = Self::get_attr(attrs, "name")?; + let azimuth_angle = Self::parse_optional_attribute::(attrs, "azimuthAngle")?; + let elevation_angle = Self::parse_optional_attribute::(attrs, "elevationAngle")?; + let wind_speed = Self::parse_optional_attribute::(attrs, "windSpeed")?; + let is_mspa = Self::parse_attribute::(attrs, "isMSPA")?; + let is_array = Self::parse_attribute::(attrs, "isArray")?; + let is_ddor = Self::parse_attribute::(attrs, "isDDOR")?; Ok(Self { name,