From b9843d65a1ed7363d91fece0a192401b104eb8f9 Mon Sep 17 00:00:00 2001 From: Love Billenius Date: Sat, 4 Jan 2025 17:01:42 +0100 Subject: [PATCH] Break out modules --- src/cli.rs | 31 ++++++++++++ src/lib.rs | 14 ++++++ src/main.rs | 131 ++----------------------------------------------- src/models.rs | 8 +++ src/parsing.rs | 109 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 128 deletions(-) create mode 100644 src/cli.rs create mode 100644 src/lib.rs create mode 100644 src/models.rs create mode 100644 src/parsing.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..0ce8dd2 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,31 @@ +use clap::{arg, Parser, Subcommand}; + +#[derive(Debug, Parser)] +#[command(author, version, about)] + +pub struct Cli { + /// Which currencies do you want to fetch rates for? + #[arg(long = "currencies", short = 'c')] + pub currencies: Vec, + /// Which subcommand (output format) we are using + #[command(subcommand)] + pub command: FormatCommand, +} + +/// Subcommand enum for output format +#[derive(Debug, Subcommand)] +pub enum FormatCommand { + /// Minimal JSON output + JSONMin, + /// Pretty-printed JSON output + JSONPretty, + /// Plain line-by-line output (with extra flags) + Plain { + /// Show the time in the output + #[arg(long = "display-time")] + display_time: bool, + /// Print currencies in a compact single line + #[arg(long = "compact")] + compact: bool, + }, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c078ed1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod cli; +pub mod parsing; +pub mod models; + +pub mod ecb_url { + pub const TODAY: &'static str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; + + pub mod hist { + pub const DAILY: &'static str = + "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml"; + pub const DAYS_90: &'static str = + "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"; + } +} diff --git a/src/main.rs b/src/main.rs index 0694060..f053735 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,139 +1,14 @@ -use clap::{Parser, Subcommand}; -use quick_xml::events::Event; -use quick_xml::Reader; +use ecb_exchange::ecb_url; use reqwest::Client; -use serde::Serialize; -use std::collections::HashMap; use std::error::Error; -use std::fs; -#[derive(Debug, Serialize)] -pub struct ExchangeRateResult { - pub time: String, - pub rates: HashMap, -} - -const ECB_DAILY: &'static str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; -const ECB_HIST_DAILY: &'static str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml"; -const ECB_HIST_90: &'static str = - "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"; +use ecb_exchange::parsing::parse; #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), Box> { let client = Client::new(); - let xml_content = client.get(ECB_DAILY).send().await?.text().await?; + let xml_content = client.get(ecb_url::TODAY).send().await?.text().await?; let parsed = parse(&xml_content).unwrap(); println!("{}", serde_json::to_string_pretty(&parsed).unwrap()); Ok(()) } - -fn parse(xml: &str) -> Result, Box> { - let mut reader = Reader::from_str(xml); - reader.config_mut().trim_text(true); - - let mut results = Vec::new(); - let mut current_time: Option = None; - let mut inside_cube_time = false; - let mut current_rates = HashMap::new(); - - fn handle_cube_element( - e: &quick_xml::events::BytesStart, - current_time: &mut Option, - inside_cube_time: &mut bool, - current_rates: &mut HashMap, - results: &mut Vec, - ) -> Result<(), Box> { - if e.name().local_name().as_ref() != b"Cube" { - return Ok(()); - } - - let mut time_attr: Option = None; - let mut currency_attr: Option = None; - let mut rate_attr: Option = None; - - // Check attributes to see if it's a time-labeled Cube or a currency-labeled Cube - for attr_result in e.attributes() { - let attr = attr_result?; - let key = attr.key.as_ref(); - let val = String::from_utf8_lossy(attr.value.as_ref()).to_string(); - - match key { - b"time" => { - time_attr = Some(val); - } - b"currency" => { - currency_attr = Some(val); - } - b"rate" => { - rate_attr = Some(val); - } - _ => {} - } - } - - // If we found a time attribute, it means we're at "Cube time='...'" - if let Some(t) = time_attr { - // If we already had a current_time, that means we finished one block - if current_time.is_some() { - let previous_time = current_time.take().unwrap(); - results.push(ExchangeRateResult { - time: previous_time, - rates: current_rates.clone(), - }); - current_rates.clear(); - } - // Now set the new time - *current_time = Some(t); - *inside_cube_time = true; - } - - // If we're inside an existing time block and we see currency/rate, store it - if *inside_cube_time { - if let (Some(c), Some(r_str)) = (currency_attr, rate_attr) { - let r = r_str.parse::()?; - current_rates.insert(c, r); - } - } - - Ok(()) - } - - // Main parsing loop - while let Ok(event) = reader.read_event() { - match event { - // For normal (non-self-closing) tags - Event::Start(e) => { - handle_cube_element( - &e, - &mut current_time, - &mut inside_cube_time, - &mut current_rates, - &mut results, - )?; - } - - // For self-closing tags (like currency lines!) - Event::Empty(e) => { - handle_cube_element( - &e, - &mut current_time, - &mut inside_cube_time, - &mut current_rates, - &mut results, - )?; - } - Event::Eof => break, - _ => {} // Event::End is here aswell - } - } - - // If the document ended and we still have one block in memory - if let Some(last_time) = current_time { - results.push(ExchangeRateResult { - time: last_time, - rates: current_rates, - }); - } - - Ok(results) -} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..98abe83 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExchangeRateResult { + pub time: String, + pub rates: HashMap, +} diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..bb88a84 --- /dev/null +++ b/src/parsing.rs @@ -0,0 +1,109 @@ +use std::collections::HashMap; +use std::error::Error; + +use quick_xml::events::Event; +use quick_xml::Reader; + +use crate::models::ExchangeRateResult; + +pub fn parse(xml: &str) -> Result, Box> { + let mut reader = Reader::from_str(xml); + reader.config_mut().trim_text(true); + + let mut results = Vec::new(); + let mut current_time: Option = None; + let mut inside_cube_time = false; + let mut current_rates = HashMap::new(); + + fn handle_cube_element( + e: &quick_xml::events::BytesStart, + current_time: &mut Option, + inside_cube_time: &mut bool, + current_rates: &mut HashMap, + results: &mut Vec, + ) -> Result<(), Box> { + if e.name().local_name().as_ref() != b"Cube" { + return Ok(()); + } + + let mut time_attr: Option = None; + let mut currency_attr: Option = None; + let mut rate_attr: Option = None; + + for attr_result in e.attributes() { + let attr = attr_result?; + let key = attr.key.as_ref(); + let val = String::from_utf8_lossy(attr.value.as_ref()).to_string(); + + match key { + b"time" => { + time_attr = Some(val); + } + b"currency" => { + currency_attr = Some(val); + } + b"rate" => { + rate_attr = Some(val); + } + _ => {} + } + } + + if let Some(t) = time_attr { + if current_time.is_some() { + let previous_time = current_time.take().unwrap(); + results.push(ExchangeRateResult { + time: previous_time, + rates: current_rates.clone(), + }); + current_rates.clear(); + } + *current_time = Some(t); + *inside_cube_time = true; + } + + if *inside_cube_time { + if let (Some(c), Some(r_str)) = (currency_attr, rate_attr) { + let r = r_str.parse::()?; + current_rates.insert(c, r); + } + } + + Ok(()) + } + + while let Ok(event) = reader.read_event() { + match event { + Event::Start(e) => { + handle_cube_element( + &e, + &mut current_time, + &mut inside_cube_time, + &mut current_rates, + &mut results, + )?; + } + + Event::Empty(e) => { + handle_cube_element( + &e, + &mut current_time, + &mut inside_cube_time, + &mut current_rates, + &mut results, + )?; + } + Event::Eof => break, + _ => {} // Event::End is here aswell + } + } + + if let Some(last_time) = current_time { + results.push(ExchangeRateResult { + time: last_time, + rates: current_rates, + }); + } + + Ok(results) +}