From 2dca33109241fb1a3ecd000606c4292689f8e78f Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sat, 8 Oct 2022 14:46:43 -0600 Subject: [PATCH] Initial commit + Parsing working from a test XML file + All parsing is handled by the models + Clippy + fmt --- .gitignore | 3 + Cargo.toml | 11 ++ data/dsn.xml | 50 +++++++++ src/lib.rs | 85 +++++++++++++++ src/model.rs | 286 +++++++++++++++++++++++++++++++++++++++++++++++++ src/prelude.rs | 2 + 6 files changed, 437 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 data/dsn.xml create mode 100644 src/lib.rs create mode 100644 src/model.rs create mode 100644 src/prelude.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab1dd7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8fb07e1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rust-dsn-parser" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = "0.9.15" +chrono = {version = "0.4.22", features=["serde"]} +xml-rs = "0.8.3" \ No newline at end of file diff --git a/data/dsn.xml b/data/dsn.xml new file mode 100644 index 0000000..91f3677 --- /dev/null +++ b/data/dsn.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +1664054963000 + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..de7d407 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,85 @@ +#![allow(dead_code)] + +mod model; +mod prelude; + +use std::num::{ParseFloatError, ParseIntError}; +use std::str::ParseBoolError; + +#[derive(Debug)] +pub enum DsnRespParseError { + AttrMissing(String), + ParseIntErr(ParseIntError), + ParseFloatErr(ParseFloatError), + ParseBoolErr(ParseBoolError), + ParseChronoErr(chrono::ParseError), + XMLReaderError(xml::reader::Error), +} + +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: chrono::ParseError) -> Self { + DsnRespParseError::ParseChronoErr(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::ParseChronoErr(e) => write!(f, "Chrono parse error: {}", e), + DsnRespParseError::XMLReaderError(e) => write!(f, "XML Parse error: {}", e), + } + } +} + +#[cfg(test)] +mod tests { + use crate::model::DsnResponse; + use std::fs::File; + use std::io::BufReader; + + fn parse_test_file() -> DsnResponse { + let example_file = File::open("data/dsn.xml").unwrap(); + let buf_reader = BufReader::new(example_file); + DsnResponse::from_xml_response(buf_reader).unwrap() + } + + #[test] + fn test_parse() { + let response = parse_test_file(); + println!("{:?}", response.stations) + } + + #[test] + fn test_get_active_targets() { + let response = parse_test_file(); + println!("{:?}", response.get_active_targets()); + } +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..91953a3 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,286 @@ +use std::convert::TryFrom; +use std::io::BufReader; + +use crate::DsnRespParseError; +use xml::attribute::OwnedAttribute; +use xml::reader::XmlEvent; +use xml::EventReader; + +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 stations: Vec, +} + +impl DsnResponse { + pub fn get_active_targets(&self) -> Vec { + let mut targets = Vec::new(); + + for station in &self.stations { + for dish in &station.dishes { + targets.append(&mut dish.target.clone()) + } + } + + 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 + where + T: std::io::Read, + { + let mut stations = Vec::new(); + + let mut station: Option = None; + let mut dish: Option = None; + + let parser = EventReader::new(reader).into_iter(); + for e in parser { + match e { + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + if name.local_name.contains("station") { + if let Some(station) = station { + stations.push(station); + } + station = Some(Station::try_from(attributes).unwrap()); + } else if name.local_name.contains("dish") { + dish = Some(Dish::try_from(attributes).unwrap()); + } else if name.local_name.contains("downSignal") { + dish = Some( + dish.unwrap() + .add_down_signal(Signal::try_from(attributes).unwrap()), + ); + } else if name.local_name.contains("upSignal") { + dish = Some( + dish.unwrap() + .add_up_signal(Signal::try_from(attributes).unwrap()), + ); + } else if name.local_name.contains("target") { + dish = Some( + dish.unwrap() + .add_target(Target::try_from(attributes).unwrap()), + ); + } + } + Ok(XmlEvent::EndElement { name }) => { + if name.local_name.contains("dish") && station.is_some() && dish.is_some() { + station = Some(station.unwrap().add_dish(dish.unwrap())); + dish = None; + } + } + Ok(XmlEvent::EndDocument) => { + if let Some(station) = station { + stations.push(station); + break; + } + } + Err(e) => { + return Err(DsnRespParseError::from(e)); + } + _ => {} + } + } + + Ok(DsnResponse { stations }) + } +} + +#[derive(Clone, Debug)] +pub struct Station { + pub name: String, + pub friendly_name: String, + pub time_utc: u64, + pub tz_offset: i64, + pub dishes: Vec, +} + +impl Station { + pub fn add_dish(mut self, dish: Dish) -> Self { + self.dishes.push(dish); + self + } +} + +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::()?; + + Ok(Self { + name, + friendly_name, + time_utc, + tz_offset, + dishes: Vec::new(), + }) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SignalType { + Data, + Carrier, + None, + Unkown(String), +} + +impl From for SignalType { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "data" => SignalType::Data, + "carrier" => SignalType::Carrier, + "none" => SignalType::None, + _ => SignalType::Unkown(s), + } + } +} + +impl Default for SignalType { + fn default() -> Self { + SignalType::None + } +} + +#[derive(Clone, Debug, Default)] +pub struct Signal { + pub signal_type: SignalType, + pub data_rate: Option, + pub frequency: Option, + pub power: Option, + pub spacecraft: String, + 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(); + + Ok(Self { + signal_type, + data_rate, + frequency, + power, + spacecraft, + spacecraft_id, + }) + } +} + +#[derive(Clone, Debug, Default)] +pub struct Target { + pub name: String, + pub id: u32, + /// Up Leg Range (km) + pub upleg_range: f64, + /// Down Leg Leg Range (km) + pub downleg_range: f64, + /// Round trip light time (s) + 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::()?; + + Ok(Self { + name, + id, + upleg_range, + downleg_range, + rtlt, + }) + } +} + +#[derive(Clone, Debug)] +pub struct Dish { + pub name: String, + pub azimuth_angle: Option, + pub elevation_angle: Option, + pub wind_speed: Option, + pub is_mspa: bool, + pub is_array: bool, + pub is_ddor: bool, + pub up_signal: Vec, + pub down_signal: Vec, + pub target: Vec, +} + +impl Dish { + pub fn add_up_signal(mut self, signal: Signal) -> Self { + self.up_signal.push(signal); + self + } + + pub fn add_down_signal(mut self, signal: Signal) -> Self { + self.down_signal.push(signal); + self + } + + pub fn add_target(mut self, target: Target) -> Self { + self.target.push(target); + self + } +} + +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::()?; + + Ok(Self { + name, + azimuth_angle, + elevation_angle, + wind_speed, + is_mspa, + is_array, + is_ddor, + up_signal: Vec::new(), + down_signal: Vec::new(), + target: Vec::new(), + }) + } +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..78b04b0 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,2 @@ +pub use crate::model::*; +pub use crate::DsnRespParseError;