diff --git a/Cargo.lock b/Cargo.lock index 5fd0e3f..409a857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -209,6 +215,7 @@ dependencies = [ name = "ecb-exchange" version = "0.1.0" dependencies = [ + "anyhow", "clap", "quick-xml", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 73a1496..1035270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.95" clap = { version = "4.5.23", features = ["derive"] } quick-xml = { version = "0.37.2", features = ["async-tokio", "tokio"] } reqwest = "0.12.12" diff --git a/src/cli.rs b/src/cli.rs index 0ce8dd2..362f2b4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,6 @@ -use clap::{arg, Parser, Subcommand}; +use clap::{arg, Parser, ValueEnum}; + +use crate::ecb_url; #[derive(Debug, Parser)] #[command(author, version, about)] @@ -7,25 +9,44 @@ 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, + + #[arg(value_enum, default_value_t = FormatOption::Plain)] + pub command: FormatOption, + + /// Show the time in the output + #[arg(long = "display-time")] + pub display_time: bool, + + /// Print currencies in a compact single line + #[arg(long = "compact")] + pub compact: bool, + + /// Amount of data + #[arg(value_enum, default_value_t = Resolution::TODAY, long="resolution", short='r')] + pub resolution: Resolution, } -/// 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, - }, +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum Resolution { + TODAY, + HistDays90, + HistDay, +} + +impl Resolution { + pub fn to_ecb_url(&self) -> &'static str { + match self { + Resolution::TODAY => ecb_url::TODAY, + Resolution::HistDays90 => ecb_url::hist::DAYS_90, + Resolution::HistDay => ecb_url::hist::DAILY, + } + } +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum FormatOption { + /// JSON output + Json, + /// Plain line-by-line output (with extra flags) + Plain, } diff --git a/src/main.rs b/src/main.rs index f053735..84d7f4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,106 @@ -use ecb_exchange::ecb_url; -use reqwest::Client; -use std::error::Error; +use clap::Parser as _; +use ecb_exchange::{cli::Cli, models::ExchangeRateResult}; +use reqwest::{Client, IntoUrl}; +use std::{borrow::BorrowMut, collections::HashMap, error::Error, process::ExitCode}; use ecb_exchange::parsing::parse; -#[tokio::main(flavor = "current_thread")] -async fn main() -> Result<(), Box> { +async fn get_and_parse(url: impl IntoUrl) -> Result, Box> { let client = Client::new(); - 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(()) + let xml_content = client.get(url).send().await?.text().await?; + parse(&xml_content) +} + +fn filter_currencies(exchange_rate_results: &mut [ExchangeRateResult], currencies: &[String]) { + for exchange_rate in exchange_rate_results { + let rates_ptr: *mut HashMap = &mut exchange_rate.rates; + exchange_rate + .rates + .keys() + .filter(|x| !currencies.contains(x)) + .for_each(|key_to_remove| { + let rates = unsafe { (*rates_ptr).borrow_mut() }; + rates.remove_entry(key_to_remove); + }); + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> ExitCode { + let cli = Cli::parse(); + + let mut parsed = match get_and_parse(cli.resolution.to_ecb_url()).await { + Ok(k) => k, + Err(e) => { + eprintln!("Failed to get/parse data from ECB: {}", e); + return ExitCode::FAILURE; + } + }; + + if !cli.currencies.is_empty() { + filter_currencies(&mut parsed, &cli.currencies); + } + + let output = match cli.command { + ecb_exchange::cli::FormatOption::Json => { + let mut json_values = parsed + .iter() + .map(|x| serde_json::to_value(x).expect("Failed to parse content as JSON value")) + .collect::>(); + + if !cli.display_time { + for json_value in json_values.iter_mut() { + if let Some(map) = json_value.as_object_mut() { + map.remove_entry("time"); + } + } + } + + if cli.compact { + serde_json::to_string(&json_values) + } else { + serde_json::to_string_pretty(&json_values) + } + .expect("Failed to parse content as JSON") + } + ecb_exchange::cli::FormatOption::Plain => { + struct StringCur<'a> { + time: &'a String, + cur: String, + } + + let separator = if cli.compact { ", " } else { "\n" }; + + let string_curred = parsed.iter().map(|entry| { + let s = entry + .rates + .iter() + .map(|(cur, rate)| format!("{}: {}", cur, rate)) + .collect::>() + .join(&separator); + + StringCur { + time: &entry.time, + cur: s, + } + }); + + let time_sep = if cli.compact { ": " } else { "\n" }; + let mut buf = String::new(); + for sc in string_curred { + if cli.display_time { + buf.push_str(&sc.time); + buf.push_str(time_sep); + } + buf.push_str(&sc.cur); + buf.push_str(&separator); + buf.push('\n'); + } + + buf + } + }; + + println!("{}", &output); + ExitCode::SUCCESS }