mirror of
https://github.com/lov3b/ecb-rates.git
synced 2025-12-19 19:00:39 +01:00
Compare commits
5 Commits
feature/sm
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 75f73e5d5e | |||
| 980ae6f8cb | |||
| 364875a84e | |||
| f388c31594 | |||
|
|
837a8e9851 |
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -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() };
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
11
src/lib.rs
11
src/lib.rs
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/main.rs
75
src/main.rs
@@ -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(¤cy);
|
header_description.replace_eur(currency);
|
||||||
let error_occured = change_perspective(&mut parsed, ¤cy).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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user