5 Commits

Author SHA1 Message Date
75f73e5d5e bump to version 1.1.0 2025-12-17 22:35:09 +01:00
980ae6f8cb breakout async_main 2025-12-17 22:33:31 +01:00
364875a84e Bump MSRV and cleanup with clippy 2025-12-17 22:20:31 +01:00
f388c31594 Don't specify the license twice 2025-12-17 22:14:20 +01:00
Love
837a8e9851 Merge pull request #3 from lov3b/feature/smol-str
Use smolstr since we only expect small strings
2025-12-17 22:12:35 +01:00
15 changed files with 73 additions and 78 deletions

14
Cargo.lock generated
View File

@@ -232,7 +232,7 @@ dependencies = [
[[package]] [[package]]
name = "ecb-rates" name = "ecb-rates"
version = "1.0.1" version = "1.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1236,21 +1236,9 @@ dependencies = [
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-native-tls"
version = "0.3.1" version = "0.3.1"

View File

@@ -1,11 +1,10 @@
[package] [package]
name = "ecb-rates" name = "ecb-rates"
description = "Query exchange rates from the European Central Bank (ECB)" description = "Query exchange rates from the European Central Bank (ECB)"
version = "1.0.1" version = "1.1.0"
edition = "2021" edition = "2024"
authors = ["Love Billenius <lovebillenius@disroot.org>"] authors = ["Love Billenius <lovebillenius@disroot.org>"]
license = "Zlib" license = "Zlib"
license-file = "LICENSE"
keywords = [ keywords = [
"ECB", "ECB",
"Bank", "Bank",
@@ -14,7 +13,7 @@ keywords = [
"rates", "rates",
] ]
repository = "https://github.com/lov3b/ecb-rates" repository = "https://github.com/lov3b/ecb-rates"
rust-version = "1.83" rust-version = "1.92"
categories = ["finance", "command-line-utilities"] categories = ["finance", "command-line-utilities"]
[profile.release] [profile.release]
@@ -35,4 +34,4 @@ reqwest = "0.12"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
smol_str = { version = "0.3", features = ["serde"] } smol_str = { version = "0.3", features = ["serde"] }
tokio = { version = "1.48", features = ["macros"] } tokio = "1.48"

View File

@@ -3,8 +3,8 @@ use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::CacheLine; use super::CacheLine;
use crate::os::Os;
use crate::View; use crate::View;
use crate::os::Os;
#[derive(Debug)] #[derive(Debug)]
pub struct Cache { pub struct Cache {

View File

@@ -4,8 +4,8 @@ use chrono::serde::ts_seconds;
use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeDelta, Utc, Weekday}; use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeDelta, Utc, Weekday};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::ExchangeRateResult;
use crate::Hollidays; use crate::Hollidays;
use crate::models::ExchangeRateResult;
const CET: FixedOffset = unsafe { FixedOffset::east_opt(3600).unwrap_unchecked() }; const CET: FixedOffset = unsafe { FixedOffset::east_opt(3600).unwrap_unchecked() };

View File

@@ -1,11 +1,10 @@
use clap::{arg, Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use smol_str::SmolStr; use smol_str::SmolStr;
use super::{ShowDays, SortBy}; use super::{ShowDays, SortBy};
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(author, version, about)] #[command(author, version, about)]
pub struct Cli { pub struct Cli {
/// Which currencies do you want to fetch rates for? /// Which currencies do you want to fetch rates for?
#[arg(long = "currencies", short = 'c')] #[arg(long = "currencies", short = 'c')]
@@ -42,7 +41,7 @@ pub struct Cli {
#[arg(long = "invert", short = 'i')] #[arg(long = "invert", short = 'i')]
pub should_invert: bool, pub should_invert: bool,
//// Max decimals to keep in price. /// Max decimals to keep in price.
#[arg(long = "max-decimals", short = 'd', default_value_t = 5)] #[arg(long = "max-decimals", short = 'd', default_value_t = 5)]
pub max_decimals: u8, pub max_decimals: u8,

View File

@@ -9,7 +9,7 @@ pub enum SortBy {
impl SortBy { impl SortBy {
pub fn get_comparer(&self) -> fn(&(&str, f64), &(&str, f64)) -> std::cmp::Ordering { pub fn get_comparer(&self) -> fn(&(&str, f64), &(&str, f64)) -> std::cmp::Ordering {
match self { match self {
Self::Currency => |a, b| a.0.cmp(&b.0), Self::Currency => |a, b| a.0.cmp(b.0),
Self::Rate => |a, b| a.1.total_cmp(&b.1), Self::Rate => |a, b| a.1.total_cmp(&b.1),
} }
} }

View File

@@ -7,6 +7,12 @@ pub struct HeaderDescription<'a> {
header_description: [&'a str; 2], header_description: [&'a str; 2],
} }
impl<'a> Default for HeaderDescription<'a> {
fn default() -> Self {
Self::new()
}
}
impl<'a> HeaderDescription<'a> { impl<'a> HeaderDescription<'a> {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {

View File

@@ -134,14 +134,14 @@ mod tests {
#[should_panic] #[should_panic]
fn test_year_too_low() { fn test_year_too_low() {
disable_panic_stack_trace(); disable_panic_stack_trace();
let _ = Hollidays::new(1000); let _ = Hollidays::new(1000);
} }
#[test] #[test]
#[should_panic] #[should_panic]
fn test_year_too_high() { fn test_year_too_high() {
disable_panic_stack_trace(); disable_panic_stack_trace();
let _ = Hollidays::new(9999); let _ = Hollidays::new(9999);
} }
fn disable_panic_stack_trace() { fn disable_panic_stack_trace() {

View File

@@ -1,4 +1,4 @@
pub mod cache; pub mod caching;
pub mod cli; pub mod cli;
mod header_description; mod header_description;
mod holiday; mod holiday;
@@ -13,16 +13,15 @@ pub use header_description::HeaderDescription;
pub use holiday::Hollidays; pub use holiday::Hollidays;
pub use view::View; pub use view::View;
const APP_NAME: &'static str = "ECB-rates"; const APP_NAME: &str = "ECB-rates";
const DEFAULT_WIDTH: usize = 20; const DEFAULT_WIDTH: usize = 20;
pub mod ecb_url { pub mod ecb_url {
pub const TODAY: &'static str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; pub const TODAY: &str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml";
pub mod hist { pub mod hist {
pub const DAYS_ALL: &'static str = pub const DAYS_ALL: &str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml";
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml"; pub const DAYS_90: &str =
pub const DAYS_90: &'static str =
"https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml"; "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml";
} }
} }

View File

@@ -1,5 +1,6 @@
use anyhow::Context;
use clap::Parser as _; use clap::Parser as _;
use ecb_rates::cache::{Cache, CacheLine}; use ecb_rates::caching::{Cache, CacheLine};
use ecb_rates::HeaderDescription; use ecb_rates::HeaderDescription;
use reqwest::{Client, IntoUrl}; use reqwest::{Client, IntoUrl};
use smol_str::StrExt; use smol_str::StrExt;
@@ -17,22 +18,40 @@ async fn get_and_parse(url: impl IntoUrl) -> anyhow::Result<Vec<ExchangeRateResu
parse(&xml_content) parse(&xml_content)
} }
#[tokio::main(flavor = "current_thread")] fn main() -> ExitCode {
async fn main() -> ExitCode { let cli = Cli::parse();
let mut cli = Cli::parse();
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(e) => {
eprintln!("Failed to initialize asynchronous runtime: {:?}", e);
return ExitCode::FAILURE;
}
};
match runtime.block_on(async_main(cli)) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("Fatal: {:?}", e);
ExitCode::FAILURE
}
}
}
async fn async_main(mut cli: Cli) -> anyhow::Result<()> {
if cli.force_color { if cli.force_color {
colored::control::set_override(true); colored::control::set_override(true);
} }
let mut header_description = HeaderDescription::new(); let mut header_description = HeaderDescription::new();
let use_cache = !cli.no_cache; let use_cache = !cli.no_cache;
let view = match cli.show_days.to_view() { let view = cli
Some(v) => v, .show_days
None => { .to_view()
eprintln!("It doesn't make any sence to fetch 0 days right?"); .context("It doesn't make any sence to fetch 0 days right?")?;
return ExitCode::SUCCESS;
}
};
let mut cache = if use_cache { Cache::load(&view) } else { None }; let mut cache = if use_cache { Cache::load(&view) } else { None };
let cache_ok = cache.as_ref().map_or_else( let cache_ok = cache.as_ref().map_or_else(
|| false, || false,
@@ -48,14 +67,9 @@ async fn main() -> ExitCode {
.exchange_rate_results .exchange_rate_results
.clone() .clone()
} else { } else {
let parsed = match get_and_parse(view.to_ecb_url()).await { let parsed = get_and_parse(view.to_ecb_url())
Ok(k) => k, .await
Err(e) => { .context("Failed to get/parse data from ECB")?;
eprintln!("Failed to get/parse data from ECB: {}", e);
return ExitCode::FAILURE;
}
};
if !cache_ok { if !cache_ok {
let not_equal_cache = cache.as_ref().map_or_else( let not_equal_cache = cache.as_ref().map_or_else(
|| true, || true,
@@ -66,14 +80,10 @@ async fn main() -> ExitCode {
}, },
); );
if not_equal_cache { if not_equal_cache && let Some(cache_safe) = cache.as_mut() {
if let Some(cache_safe) = cache.as_mut() { let cache_line = CacheLine::new(parsed.clone());
let cache_line = CacheLine::new(parsed.clone()); cache_safe.set_cache_line(cache_line);
cache_safe.set_cache_line(cache_line); cache_safe.save()?;
if let Err(e) = cache_safe.save() {
eprintln!("Failed to save to cache with: {:?}", e);
}
}
} }
} }
parsed parsed
@@ -81,12 +91,9 @@ async fn main() -> ExitCode {
cli.perspective = cli.perspective.map(|s| s.to_uppercase_smolstr()); cli.perspective = cli.perspective.map(|s| s.to_uppercase_smolstr());
if let Some(currency) = cli.perspective.as_ref() { if let Some(currency) = cli.perspective.as_ref() {
header_description.replace_eur(&currency); header_description.replace_eur(currency);
let error_occured = change_perspective(&mut parsed, &currency).is_none(); change_perspective(&mut parsed, currency)
if error_occured { .context("The currency wasn't in the data from the ECB!")?;
eprintln!("The currency wasn't in the data from the ECB!");
return ExitCode::FAILURE;
}
} }
if cli.should_invert { if cli.should_invert {
@@ -161,5 +168,5 @@ async fn main() -> ExitCode {
}; };
println!("{}", &output); println!("{}", &output);
ExitCode::SUCCESS Ok(())
} }

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use quick_xml::events::Event;
use quick_xml::Reader; use quick_xml::Reader;
use quick_xml::events::Event;
use smol_str::SmolStr; use smol_str::SmolStr;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
@@ -68,11 +68,9 @@ pub fn parse(xml: &str) -> anyhow::Result<Vec<ExchangeRateResult>> {
*inside_cube_time = true; *inside_cube_time = true;
} }
if *inside_cube_time { if *inside_cube_time && let (Some(c), Some(r_str)) = (currency_attr, rate_attr) {
if let (Some(c), Some(r_str)) = (currency_attr, rate_attr) { let r = r_str.parse::<f64>()?;
let r = r_str.parse::<f64>()?; current_rates.insert(c, r);
current_rates.insert(c, r);
}
} }
Ok(()) Ok(())

View File

@@ -1,11 +1,10 @@
mod table_display;
mod table_getter; mod table_getter;
mod table_owned; mod table_owned;
mod table_ref; mod table_ref;
mod table_trait; mod table_trait;
mod table_display;
pub use table_getter::TableGet; pub use table_getter::TableGet;
pub use table_owned::Table; pub use table_owned::Table;
pub use table_ref::TableRef; pub use table_ref::TableRef;
pub use table_trait::TableTrait; pub use table_trait::TableTrait;

View File

@@ -2,9 +2,9 @@ use std::fmt::Display;
use smol_str::SmolStr; use smol_str::SmolStr;
use crate::DEFAULT_WIDTH;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
use crate::DEFAULT_WIDTH;
use super::table_display::helper_table_print; use super::table_display::helper_table_print;
use super::{TableGet, TableTrait}; use super::{TableGet, TableTrait};

View File

@@ -1,13 +1,13 @@
use std::fmt::Display; use std::fmt::Display;
use crate::DEFAULT_WIDTH;
use crate::cli::SortBy; use crate::cli::SortBy;
use crate::models::ExchangeRateResult; use crate::models::ExchangeRateResult;
use crate::DEFAULT_WIDTH;
use super::Table;
use super::table_display::helper_table_print; use super::table_display::helper_table_print;
use super::table_getter::TableGet; use super::table_getter::TableGet;
use super::table_trait::TableTrait; use super::table_trait::TableTrait;
use super::Table;
pub struct TableRef<'a> { pub struct TableRef<'a> {
header: Option<&'a str>, header: Option<&'a str>,
@@ -78,7 +78,7 @@ impl<'a> TableGet for TableRef<'a> {
fn get_width(&self) -> usize { fn get_width(&self) -> usize {
self.width self.width
} }
fn get_left_offset(&self) -> usize { fn get_left_offset(&self) -> usize {
self.left_offset self.left_offset
} }