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