From 671b70aafabbf203670878431d101f7f5d6499e0 Mon Sep 17 00:00:00 2001 From: rolv Date: Sat, 9 Nov 2024 00:05:08 +0000 Subject: [PATCH 01/14] tests(cli): cli utils --- cli/src/cli/utils.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/cli/src/cli/utils.rs b/cli/src/cli/utils.rs index 2d75064..39fc9eb 100644 --- a/cli/src/cli/utils.rs +++ b/cli/src/cli/utils.rs @@ -10,3 +10,40 @@ pub fn if_supports_colour( comfy_table::Color::Reset } } + +#[cfg(test)] +mod tests { + use super::*; + use comfy_table::Color; + use termcolor::{ColorChoice, StandardStream}; + + #[test] + fn test_if_supports_colour() { + let get_stream = StandardStream::stdout; + + assert_eq!( + comfy_table::Color::Reset, + if_supports_colour(&get_stream(ColorChoice::Never), Color::Red) + ); + assert_eq!( + comfy_table::Color::Reset, + if_supports_colour(&get_stream(ColorChoice::Never), Color::Cyan) + ); + + assert_eq!( + comfy_table::Color::Green, + if_supports_colour(&get_stream(ColorChoice::Always), Color::Green) + ); + assert_eq!( + comfy_table::Color::Black, + if_supports_colour(&get_stream(ColorChoice::AlwaysAnsi), Color::Black) + ); + + assert_eq!( + comfy_table::Color::Reset, + if_supports_colour(&get_stream(ColorChoice::Always), Color::Reset) + ); + + // NOTE: No test for `ColorChoice::Auto` - not sure how that would react in a CI environment + } +} From c68de29e6d76826fc9d34b0f23c456efc1febdd6 Mon Sep 17 00:00:00 2001 From: rolv Date: Sun, 10 Nov 2024 10:34:30 +0000 Subject: [PATCH 02/14] feat: add small method for checking that the API is available --- lib/src/api/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/src/api/mod.rs b/lib/src/api/mod.rs index 7ce00e2..e04be8c 100644 --- a/lib/src/api/mod.rs +++ b/lib/src/api/mod.rs @@ -56,6 +56,17 @@ impl ServerClient { format!("{}{endpoint}", self.url.as_str()) } + /// Makes a basic request to the API and returns true in the event of a successful response. + /// + /// Useful for a simple check that the API is up and successfully responding to requests. + pub async fn is_server_available(&self) -> bool { + self.client + .get(self.url("")) + .send() + .await + .is_ok_and(|r| r.status().is_success()) + } + /// Request exchange rates for a specific date (latest by default). pub async fn convert(&self, req: convert::Request) -> Result { let (url, query_params) = req.setup()?; From be789be04006cea8afd93c5b9f690ac22de60e2d Mon Sep 17 00:00:00 2001 From: rolv Date: Sun, 10 Nov 2024 10:35:00 +0000 Subject: [PATCH 03/14] feat: add some builder-pattern methods to the requests for `convert` and `period` --- lib/src/api/convert.rs | 26 ++++++++++++++++++++++++++ lib/src/api/period.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/lib/src/api/convert.rs b/lib/src/api/convert.rs index 655b53e..7fd7475 100644 --- a/lib/src/api/convert.rs +++ b/lib/src/api/convert.rs @@ -36,6 +36,32 @@ pub struct Request { pub date: Option, } +impl Request { + /// Consumes the [`Request`] and returns a new one with the given base. + pub fn with_base(mut self, base: Currency) -> Self { + self.base = Some(base); + self + } + + /// Consumes the [`Request`] and returns a new one with the given targets. + pub fn with_targets(mut self, targets: Vec) -> Self { + self.targets = Some(targets); + self + } + + /// Consumes the [`Request`] and returns a new one with the given amount. + pub fn with_amount(mut self, amount: CurrencyValue) -> Self { + self.amount = Some(amount); + self + } + + /// Consumes the [`Request`] and returns a new one with the given date. + pub fn with_date(mut self, date: NaiveDate) -> Self { + self.date = Some(date); + self + } +} + impl ServerClientRequest for Request { /// Get the endpoint for fetching exchange rates for a specific date. fn get_url(&self) -> Cow<'static, str> { diff --git a/lib/src/api/period.rs b/lib/src/api/period.rs index 7e7c70b..adfa557 100644 --- a/lib/src/api/period.rs +++ b/lib/src/api/period.rs @@ -34,6 +34,38 @@ pub struct Request { pub end_date: Option, } +impl Request { + /// Consumes the [`Request`] and returns a new one with the given base. + pub fn with_base(mut self, base: Currency) -> Self { + self.base = Some(base); + self + } + + /// Consumes the [`Request`] and returns a new one with the given targets. + pub fn with_targets(mut self, targets: Vec) -> Self { + self.targets = Some(targets); + self + } + + /// Consumes the [`Request`] and returns a new one with the given amount. + pub fn with_amount(mut self, amount: CurrencyValue) -> Self { + self.amount = Some(amount); + self + } + + /// Consumes the [`Request`] and returns a new one with the given start date. + pub fn with_start_date(mut self, date: NaiveDate) -> Self { + self.start_date = date; + self + } + + /// Consumes the [`Request`] and returns a new one with the given end date. + pub fn with_end_date(mut self, date: NaiveDate) -> Self { + self.end_date = Some(date); + self + } +} + impl ServerClientRequest for Request { /// Get the endpoint for fetching exchange rates over a period of time. fn get_url(&self) -> Cow<'static, str> { From 34ab86bd46e104eb67b4066050dd1304fe08980b Mon Sep 17 00:00:00 2001 From: rolv Date: Sun, 10 Nov 2024 12:11:32 +0000 Subject: [PATCH 04/14] fix: check that targets don't include the default currency (EUR) as that will still return an error from the API --- lib/src/api/convert.rs | 9 ++++++--- lib/src/api/period.rs | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/src/api/convert.rs b/lib/src/api/convert.rs index 7fd7475..6144c65 100644 --- a/lib/src/api/convert.rs +++ b/lib/src/api/convert.rs @@ -72,10 +72,13 @@ impl ServerClientRequest for Request { } fn ensure_valid(&self) -> crate::error::Result<()> { - if let (Some(targets), Some(base)) = (&self.targets, &self.base) { - if targets.contains(base) { + if let Some(targets) = &self.targets { + // Check against the default value too as passing targets which include the default (EUR), + // will still cause an error to be returned from the API + let base = self.base.clone().unwrap_or_default(); + if targets.contains(&base) { return Err(Error::RequestTargetsIncludeBase { - base: base.clone(), + base, targets: targets.clone(), }); } diff --git a/lib/src/api/period.rs b/lib/src/api/period.rs index adfa557..0b9ef11 100644 --- a/lib/src/api/period.rs +++ b/lib/src/api/period.rs @@ -76,10 +76,13 @@ impl ServerClientRequest for Request { } fn ensure_valid(&self) -> crate::error::Result<()> { - if let (Some(targets), Some(base)) = (&self.targets, &self.base) { - if targets.contains(base) { + if let Some(targets) = &self.targets { + // Check against the default value too as passing targets which include the default (EUR), + // will still cause an error to be returned from the API + let base = self.base.clone().unwrap_or_default(); + if targets.contains(&base) { return Err(Error::RequestTargetsIncludeBase { - base: base.clone(), + base, targets: targets.clone(), }); } From 54adc672542617300e69fd7030ae368e94fe259a Mon Sep 17 00:00:00 2001 From: rolv Date: Sun, 10 Nov 2024 22:06:42 +0000 Subject: [PATCH 05/14] refactor: move some common functionality between `convert` and `period` to a separate module --- lib/src/api/convert.rs | 23 ++----- lib/src/api/mod.rs | 43 ++---------- lib/src/api/period.rs | 16 +---- lib/src/api/shared.rs | 153 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 69 deletions(-) create mode 100644 lib/src/api/shared.rs diff --git a/lib/src/api/convert.rs b/lib/src/api/convert.rs index 6144c65..d260ed2 100644 --- a/lib/src/api/convert.rs +++ b/lib/src/api/convert.rs @@ -3,11 +3,8 @@ use std::borrow::Cow; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; -use super::{build_base_query_params, ServerClientRequest}; -use crate::{ - data::{Currency, CurrencyValue, CurrencyValueMap}, - error::Error, -}; +use super::{base_build_query_params, base_ensure_valid, ServerClientRequest}; +use crate::data::{Currency, CurrencyValue, CurrencyValueMap}; /// Response for fetching the latest exchange rates. #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] @@ -72,22 +69,10 @@ impl ServerClientRequest for Request { } fn ensure_valid(&self) -> crate::error::Result<()> { - if let Some(targets) = &self.targets { - // Check against the default value too as passing targets which include the default (EUR), - // will still cause an error to be returned from the API - let base = self.base.clone().unwrap_or_default(); - if targets.contains(&base) { - return Err(Error::RequestTargetsIncludeBase { - base, - targets: targets.clone(), - }); - } - } - - Ok(()) + base_ensure_valid(&self.base, &self.targets) } fn build_query_params(&self) -> super::QueryParams { - build_base_query_params(&self.amount, &self.base, &self.targets) + base_build_query_params(&self.amount, &self.base, &self.targets) } } diff --git a/lib/src/api/mod.rs b/lib/src/api/mod.rs index e04be8c..e3d7066 100644 --- a/lib/src/api/mod.rs +++ b/lib/src/api/mod.rs @@ -6,15 +6,14 @@ pub mod convert; pub mod currencies; pub mod period; +mod shared; use std::borrow::Cow; +use shared::*; use url::Url; -use crate::{ - data::{Currency, CurrencyValue}, - error::Result, -}; +use crate::error::Result; /// A HTTP client for making requests to a Frankfurter API. #[derive(Debug)] @@ -109,11 +108,11 @@ impl ServerClient { } } -type EndpointUrl = Cow<'static, str>; -type QueryParams = Vec<(&'static str, String)>; +pub type EndpointUrl = Cow<'static, str>; +pub type QueryParams = Vec<(&'static str, String)>; /// Utility trait to provide a common interface for server client requests. -trait ServerClientRequest { +pub trait ServerClientRequest { fn get_url(&self) -> EndpointUrl; fn ensure_valid(&self) -> Result<()>; @@ -129,35 +128,5 @@ trait ServerClientRequest { } } -/// Common query parameters between [`convert::Request`] and [`period::Request`]. -pub(super) fn build_base_query_params( - amount: &Option, - base: &Option, - targets: &Option>, -) -> Vec<(&'static str, String)> { - let mut query_params = vec![]; - - if let Some(a) = amount { - query_params.push(("amount", format!("{a:.2}"))); - }; - - if let Some(b) = base { - query_params.push(("base", b.to_string())); - }; - - if let Some(t) = targets { - // An empty string for the target/symbol would lead to an error being returned from the API, - // so skip if targets is empty - if !t.is_empty() { - query_params.push(( - "symbols", - t.iter() - .map(|s| s.to_string()) - .collect::>() - .join(","), - )) - } - }; - query_params } diff --git a/lib/src/api/period.rs b/lib/src/api/period.rs index 0b9ef11..4a56c2a 100644 --- a/lib/src/api/period.rs +++ b/lib/src/api/period.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap}; use chrono::{Datelike, NaiveDate, TimeDelta, Timelike, Utc, Weekday}; use serde::{Deserialize, Serialize}; -use super::{build_base_query_params, ServerClientRequest}; +use super::{base_build_query_params, base_ensure_valid, ServerClientRequest}; use crate::{ data::{Currency, CurrencyValue, CurrencyValueMap}, error::Error, @@ -76,17 +76,7 @@ impl ServerClientRequest for Request { } fn ensure_valid(&self) -> crate::error::Result<()> { - if let Some(targets) = &self.targets { - // Check against the default value too as passing targets which include the default (EUR), - // will still cause an error to be returned from the API - let base = self.base.clone().unwrap_or_default(); - if targets.contains(&base) { - return Err(Error::RequestTargetsIncludeBase { - base, - targets: targets.clone(), - }); - } - } + base_ensure_valid(&self.base, &self.targets)?; let mut latest = Utc::now(); // Reduce day by 1 if it is still earlier than the earliest exchange rate fetch at 15:00 @@ -127,6 +117,6 @@ impl ServerClientRequest for Request { } fn build_query_params(&self) -> super::QueryParams { - build_base_query_params(&self.amount, &self.base, &self.targets) + base_build_query_params(&self.amount, &self.base, &self.targets) } } diff --git a/lib/src/api/shared.rs b/lib/src/api/shared.rs new file mode 100644 index 0000000..9c151ee --- /dev/null +++ b/lib/src/api/shared.rs @@ -0,0 +1,153 @@ +use crate::{ + data::{Currency, CurrencyValue}, + error::{Error, Result}, +}; + +/// Shared validation checks between [`super::convert::Request`] and [`super::period::Request`]. +pub(super) fn base_ensure_valid( + base: &Option, + targets: &Option>, +) -> Result<()> { + if let Some(targets) = targets { + // Check against the default value too as passing targets which include the default (EUR), + // will still cause an error to be returned from the API + let base = base.clone().unwrap_or_default(); + if targets.contains(&base) { + return Err(Error::RequestTargetsIncludeBase { + base, + targets: targets.clone(), + }); + } + } + + Ok(()) +} + +/// Shared query parameters between [`super::convert::Request`] and [`super::period::Request`]. +pub(super) fn base_build_query_params( + amount: &Option, + base: &Option, + targets: &Option>, +) -> Vec<(&'static str, String)> { + let mut query_params = vec![]; + + if let Some(a) = amount { + query_params.push(("amount", format!("{a:.2}"))); + }; + + if let Some(b) = base { + query_params.push(("base", b.to_string())); + }; + + if let Some(t) = targets { + // An empty string for the target/symbol would lead to an error being returned from the API, + // so skip if targets is empty + if !t.is_empty() { + query_params.push(( + "symbols", + t.iter() + .map(|s| s.to_string()) + .collect::>() + .join(","), + )) + } + }; + + query_params +} + +#[cfg(test)] +mod tests_shared { + use super::*; + use crate::api::test_utils::dbg_err; + + #[test] + fn test_base_ensure_valid() { + // DEFAULT + assert!(base_ensure_valid(&None, &None).inspect_err(dbg_err).is_ok()); + + // VALID TARGETS + assert!( + base_ensure_valid(&None, &Some(vec![Currency::USD, Currency::AUD])) + .inspect_err(dbg_err) + .is_ok() + ); + + // INVALID TARGETS + assert!(base_ensure_valid( + &Some(Currency::USD), + &Some(vec![Currency::USD, Currency::AUD]) + ) + .is_err()); + + // Check against default (EUR) + assert!(base_ensure_valid(&None, &Some(vec![Currency::EUR, Currency::AUD])).is_err()); + } + + #[test] + fn test_base_build_query_params() { + // DEFAULT + assert_eq!(base_build_query_params(&None, &None, &None), vec![]); + + // INDIVIDUAL + assert_eq!( + base_build_query_params(&Some(CurrencyValue::from(10.0)), &None, &None), + vec![("amount", String::from("10.00"))] + ); + assert_eq!( + base_build_query_params(&None, &Some(Currency::AUD), &None), + vec![("base", String::from("AUD"))] + ); + assert_eq!( + base_build_query_params(&None, &None, &Some(vec![Currency::CAD, Currency::ZAR])), + vec![("symbols", String::from("CAD,ZAR"))] + ); + + // COMBOS + assert_eq!( + base_build_query_params( + &Some(CurrencyValue::from(1000000.0)), + &Some(Currency::USD), + &Some(vec![Currency::CNY, Currency::CZK, Currency::IDR]) + ), + vec![ + ("amount", String::from("1000000.00")), + ("base", String::from("USD")), + ("symbols", String::from("CNY,CZK,IDR")), + ] + ); + assert_eq!( + base_build_query_params( + &None, + &Some(Currency::USD), + &Some(vec![Currency::CNY, Currency::CZK, Currency::IDR]) + ), + vec![ + ("base", String::from("USD")), + ("symbols", String::from("CNY,CZK,IDR")), + ] + ); + assert_eq!( + base_build_query_params( + &Some(CurrencyValue::from(1000000.0)), + &None, + &Some(vec![Currency::CNY, Currency::CZK, Currency::IDR]) + ), + vec![ + ("amount", String::from("1000000.00")), + ("symbols", String::from("CNY,CZK,IDR")), + ] + ); + assert_eq!( + base_build_query_params( + &Some(CurrencyValue::from(1000000.0)), + &Some(Currency::USD), + &None + ), + vec![ + ("amount", String::from("1000000.00")), + ("base", String::from("USD")), + ] + ); + } +} From de5de3569d296a0709b92021efac928140b489a1 Mon Sep 17 00:00:00 2001 From: rolv Date: Sun, 10 Nov 2024 22:07:24 +0000 Subject: [PATCH 06/14] tests: add `pretty_assertions` for testing --- Cargo.lock | 23 +++++++++++++++++++++++ lib/Cargo.toml | 1 + 2 files changed, 24 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f105f7e..0be20d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "displaydoc" version = "0.2.5" @@ -793,6 +799,7 @@ version = "0.0.0" dependencies = [ "chrono", "enum_dispatch", + "pretty_assertions", "reqwest", "serde", "serde_json", @@ -1001,6 +1008,16 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -1822,6 +1839,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.4" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index b33ee6b..5ad1287 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -21,3 +21,4 @@ thiserror = { version = "1.0.64" } [dev-dependencies] tokio = { workspace = true } +pretty_assertions = { version = "1.4.1" } From 08c6f634636aa3acb27890ac9192f820c1db4ba1 Mon Sep 17 00:00:00 2001 From: rolv Date: Tue, 12 Nov 2024 20:17:35 +0000 Subject: [PATCH 07/14] tests(lib): add basic unit tests --- cli/src/cli/utils.rs | 3 +- lib/src/api/convert.rs | 54 +++++++++++++++++++++ lib/src/api/mod.rs | 6 +++ lib/src/api/period.rs | 104 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 1 deletion(-) diff --git a/cli/src/cli/utils.rs b/cli/src/cli/utils.rs index 39fc9eb..72859b2 100644 --- a/cli/src/cli/utils.rs +++ b/cli/src/cli/utils.rs @@ -13,10 +13,11 @@ pub fn if_supports_colour( #[cfg(test)] mod tests { - use super::*; use comfy_table::Color; use termcolor::{ColorChoice, StandardStream}; + use super::*; + #[test] fn test_if_supports_colour() { let get_stream = StandardStream::stdout; diff --git a/lib/src/api/convert.rs b/lib/src/api/convert.rs index d260ed2..9b52903 100644 --- a/lib/src/api/convert.rs +++ b/lib/src/api/convert.rs @@ -76,3 +76,57 @@ impl ServerClientRequest for Request { base_build_query_params(&self.amount, &self.base, &self.targets) } } + +#[cfg(test)] +mod tests_convert { + use chrono::{Days, Utc}; + use pretty_assertions::assert_eq; + + use super::*; + use crate::api::test_utils::dbg_err; + + #[test] + fn get_url() { + assert_eq!(Request::default().get_url(), "latest"); + + let date = NaiveDate::from_ymd_opt(2000, 7, 2).unwrap(); + assert_eq!( + Request::default().with_date(date).get_url(), + format!("{date}") + ); + } + + #[test] + fn ensure_valid() { + // Check that [`super::base_ensure_valid`] is being called + assert!(Request::default() + .with_base(Currency::EUR) + .with_targets(vec![Currency::EUR, Currency::USD]) + .ensure_valid() + .is_err()); + + // VALID DATE + assert!(Request::default() + .with_date(NaiveDate::default()) + .ensure_valid() + .inspect_err(dbg_err) + .is_ok()); + + // Will just use an earlier date + let tomorrow = Utc::now() + .checked_add_days(Days::new(1)) + .unwrap() + .date_naive(); + assert!(Request::default() + .with_date(tomorrow) + .ensure_valid() + .inspect_err(dbg_err) + .is_ok()); + // Weekend - will just use the closest date with data + assert!(Request::default() + .with_date(NaiveDate::from_ymd_opt(2024, 2, 3).unwrap()) + .ensure_valid() + .inspect_err(dbg_err) + .is_ok()); + } +} diff --git a/lib/src/api/mod.rs b/lib/src/api/mod.rs index e3d7066..a8adb21 100644 --- a/lib/src/api/mod.rs +++ b/lib/src/api/mod.rs @@ -128,5 +128,11 @@ pub trait ServerClientRequest { } } +#[cfg(test)] +mod test_utils { + use crate::error::Error; + pub fn dbg_err(e: &Error) { + dbg!(e); + } } diff --git a/lib/src/api/period.rs b/lib/src/api/period.rs index 4a56c2a..af9a7fb 100644 --- a/lib/src/api/period.rs +++ b/lib/src/api/period.rs @@ -120,3 +120,107 @@ impl ServerClientRequest for Request { base_build_query_params(&self.amount, &self.base, &self.targets) } } + +#[cfg(test)] +mod tests_period { + use chrono::Days; + use pretty_assertions::assert_eq; + + use super::*; + use crate::api::test_utils::dbg_err; + + #[test] + fn get_url() { + assert_eq!( + Request::default().get_url(), + format!("{}..", NaiveDate::default()) + ); + + let date = NaiveDate::from_ymd_opt(2000, 7, 2).unwrap(); + assert_eq!( + Request::default().with_start_date(date).get_url(), + format!("{date}..") + ); + + let date = NaiveDate::from_ymd_opt(2020, 8, 9).unwrap(); + assert_eq!( + Request::default().with_end_date(date).get_url(), + format!("{}..{date}", NaiveDate::default()) + ); + + let start_date = NaiveDate::from_ymd_opt(2020, 8, 9).unwrap(); + let end_date = NaiveDate::from_ymd_opt(2020, 10, 9).unwrap(); + assert_eq!( + Request::default() + .with_start_date(start_date) + .with_end_date(end_date) + .get_url(), + format!("{start_date}..{end_date}") + ); + } + + #[test] + fn ensure_valid() { + // Check that [`super::base_ensure_valid`] is being called + assert!(Request::default() + .with_base(Currency::EUR) + .with_targets(vec![Currency::EUR, Currency::USD]) + .ensure_valid() + .is_err()); + + // VALID START DATE + assert!(Request::default() + .with_start_date(NaiveDate::default()) + .ensure_valid() + .inspect_err(dbg_err) + .is_ok()); + + // INVALID START DATE + let tomorrow = Utc::now() + .checked_add_days(Days::new(1)) + .unwrap() + .date_naive(); + assert!(Request::default() + .with_start_date(tomorrow) + .ensure_valid() + .is_err()); + + // VALID END DATE + assert!(Request::default() + .with_end_date(NaiveDate::from_ymd_opt(2000, 2, 4).unwrap()) + .ensure_valid() + .inspect_err(dbg_err) + .is_ok()); + // Not quite weekend only - Friday-Sun + assert!(Request::default() + .with_start_date(NaiveDate::from_ymd_opt(2024, 8, 2).unwrap()) + .with_end_date(NaiveDate::from_ymd_opt(2024, 8, 4).unwrap()) + .ensure_valid() + .inspect_err(dbg_err) + .is_ok()); + + // INVALID END DATE + assert!(Request::default() + .with_end_date(NaiveDate::default().checked_sub_days(Days::new(1)).unwrap()) + .ensure_valid() + .is_err()); + // Weekend only - Sat-Sun + assert!(Request::default() + .with_start_date(NaiveDate::from_ymd_opt(2024, 8, 3).unwrap()) + .with_end_date(NaiveDate::from_ymd_opt(2024, 8, 4).unwrap()) + .ensure_valid() + .is_err()); + // Weekend only - Sat-Sat + assert!(Request::default() + .with_start_date(NaiveDate::from_ymd_opt(2024, 1, 13).unwrap()) + .with_end_date(NaiveDate::from_ymd_opt(2024, 1, 13).unwrap()) + .ensure_valid() + .is_err()); + // Weekend only - Sun-Sun + assert!(Request::default() + .with_start_date(NaiveDate::from_ymd_opt(2024, 6, 23).unwrap()) + .with_end_date(NaiveDate::from_ymd_opt(2024, 6, 23).unwrap()) + .ensure_valid() + .is_err()); + } +} From 2a27f2c249457c52774e108edf6eb9c2b579137f Mon Sep 17 00:00:00 2001 From: rolv Date: Tue, 12 Nov 2024 20:19:51 +0000 Subject: [PATCH 08/14] tests(lib): add basic integration tests --- justfile | 16 ++++----- lib/tests/base.rs | 17 +++++++++ lib/tests/endpoint_convert.rs | 42 ++++++++++++++++++++++ lib/tests/endpoint_period.rs | 66 +++++++++++++++++++++++++++++++++++ lib/tests/shared/mod.rs | 11 ++++++ 5 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 lib/tests/base.rs create mode 100644 lib/tests/endpoint_convert.rs create mode 100644 lib/tests/endpoint_period.rs create mode 100644 lib/tests/shared/mod.rs diff --git a/justfile b/justfile index 552404b..cf837c3 100644 --- a/justfile +++ b/justfile @@ -7,8 +7,6 @@ alias d := develop alias du := docker_up alias dd := docker_down -# COMMANDS ----------------------------------------------------------------------------------------- - # List commands default: @just --list @@ -22,21 +20,21 @@ format: check cargo +nightly fmt # Test -test: format - cargo test --all +test: docker_up && docker_down + cargo test --all -- --nocapture + +# Run test suite whenever any change is made +develop: format docker_up + cargo watch -w cli -w lib -s "cargo test --all -- --nocapture" # Build -build: test +build: format cargo build --release # Run an example example EXAMPLE=("basic"): docker_up && docker_down -cargo run --package lib_frankfurter --example {{ EXAMPLE }} -# Run test suite whenever any change is made -develop: - cargo watch -w cli -w lib -s "just test" - # Start up the local Frankfurter API docker_up: docker compose up -d --wait diff --git a/lib/tests/base.rs b/lib/tests/base.rs new file mode 100644 index 0000000..2cdc84c --- /dev/null +++ b/lib/tests/base.rs @@ -0,0 +1,17 @@ +mod shared; +use shared::get_server; + +#[tokio::test] +async fn local_api_is_available() { + assert!( + get_server().is_server_available().await, + "\x1b[1m \nIMPORTANT: Please ensure that there is a local Frankfurter API running.\n \x1b[0m" + ); +} + +#[tokio::test] +async fn endpoint_currencies() { + let server = get_server(); + let res = server.currencies(Default::default()).await.unwrap(); + assert!(res.0.len() > 10); +} diff --git a/lib/tests/endpoint_convert.rs b/lib/tests/endpoint_convert.rs new file mode 100644 index 0000000..39a16eb --- /dev/null +++ b/lib/tests/endpoint_convert.rs @@ -0,0 +1,42 @@ +mod shared; +use chrono::NaiveDate; +use lib_frankfurter::{ + api::convert, + data::{Currency, CurrencyValue}, +}; +use pretty_assertions::assert_eq; +use shared::get_server; + +#[tokio::test] +async fn endpoint_convert() { + let server = get_server(); + let make_request = |request: convert::Request| async { server.convert(request).await.unwrap() }; + + // BASIC + let res = make_request(Default::default()).await; + assert_eq!(res.base, Currency::EUR); + assert!(res.rates.len() > 10); + assert_eq!(res.amount, CurrencyValue::from(1.0)); + + // BASE CURRENCY AND AMOUNT + let base = Currency::USD; + let amount = CurrencyValue::from(4.0); + let res = make_request( + convert::Request::default() + .with_base(base.clone()) + .with_amount(amount), + ) + .await; + assert_eq!(res.base, base); + assert_eq!(res.amount, amount); + + // TARGETS + let targets = vec![Currency::AUD, Currency::DKK, Currency::ZAR]; + let res = make_request(convert::Request::default().with_targets(targets.clone())).await; + assert_eq!(res.rates.len(), targets.len()); + + // DATE + let date = NaiveDate::from_ymd_opt(2024, 8, 20).unwrap(); + let res = make_request(convert::Request::default().with_date(date)).await; + assert_eq!(res.date, date); +} diff --git a/lib/tests/endpoint_period.rs b/lib/tests/endpoint_period.rs new file mode 100644 index 0000000..9f63fae --- /dev/null +++ b/lib/tests/endpoint_period.rs @@ -0,0 +1,66 @@ +mod shared; +use chrono::{Datelike, NaiveDate}; +use lib_frankfurter::{ + api::period, + data::{Currency, CurrencyValue}, +}; +use pretty_assertions::assert_eq; +use shared::get_server; + +#[tokio::test] +async fn endpoint_period() { + let server = get_server(); + let make_request = |request: period::Request| async { server.period(request).await.unwrap() }; + // The latest date with available data + let earliest_data = NaiveDate::from_ymd_opt(1999, 1, 4).unwrap(); + + // BASIC + // Note that this request will use the default `NaiveDate` of 1st Jan, 1970 + let res = make_request(Default::default()).await; + assert_eq!(res.start_date, earliest_data); + assert_eq!(res.base, Currency::EUR); + // Shouldn't include the base currency and the fallback currency + assert!(res.rates.last_key_value().unwrap().1.len() > 10); + assert_eq!(res.amount, CurrencyValue::from(1.0)); + assert!(res.rates.len() > 1000); + + // BASE CURRENCY AND AMOUNT + let base = Currency::KRW; + let amount = CurrencyValue::from(10.0); + let res = make_request( + period::Request::default() + .with_base(base.clone()) + .with_amount(amount), + ) + .await; + assert_eq!(res.base, base); + assert_eq!(res.amount, amount); + + // TARGETS + let targets = vec![Currency::CHF, Currency::CAD, Currency::CNY]; + let res = make_request(period::Request::default().with_targets(targets.clone())).await; + assert_eq!(res.rates.last_key_value().unwrap().1.len(), targets.len()); + + // STARTING DATE + let start_date = NaiveDate::from_ymd_opt(2020, 10, 5).unwrap(); + let res = make_request(period::Request::default().with_start_date(start_date)).await; + assert_eq!(res.start_date, start_date); + assert!(res.rates.len() > 200); + + // STARTING AND END DATES + let start_date = NaiveDate::from_ymd_opt(2024, 10, 7).unwrap(); + let end_date = NaiveDate::from_ymd_opt(2024, 10, 11).unwrap(); + let res = make_request( + period::Request::default() + .with_start_date(start_date) + .with_end_date(end_date), + ) + .await; + assert_eq!(res.start_date, start_date); + assert_eq!(res.end_date.unwrap(), end_date); + assert_eq!( + res.rates.len(), + // Start -> end date (inclusive) + (end_date.num_days_from_ce() - start_date.num_days_from_ce() + 1) as usize + ); +} diff --git a/lib/tests/shared/mod.rs b/lib/tests/shared/mod.rs new file mode 100644 index 0000000..fb0f711 --- /dev/null +++ b/lib/tests/shared/mod.rs @@ -0,0 +1,11 @@ +use std::sync::LazyLock; + +use lib_frankfurter::api; +use url::Url; + +/// URL for locally hosted API +static URL: LazyLock = LazyLock::new(|| Url::parse("http://localhost:8080").unwrap()); + +pub fn get_server() -> api::ServerClient { + api::ServerClient::new(URL.clone()) +} From 22ceefd82e81ad51543a3385ac84ed9b3b111084 Mon Sep 17 00:00:00 2001 From: rolv Date: Tue, 12 Nov 2024 20:24:50 +0000 Subject: [PATCH 09/14] fix: `Value` -> `CurrencyValue` --- lib/src/data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/data.rs b/lib/src/data.rs index 5f30ef7..f7e5557 100644 --- a/lib/src/data.rs +++ b/lib/src/data.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, fmt::Display, num::ParseFloatError, ops::Deref, use serde::{Deserialize, Serialize}; use strum::{Display, EnumString, VariantNames}; -/// A map of [`Currency`] to their respective [`Value`], sorted by the currency code keys. +/// A map of [`Currency`] to their respective [`CurrencyValue`], sorted by the currency code keys. /// /// This represents a JSON response from the server outlining exchange rates for different currency /// ISO 4217 codes. From 0303841d23d815e72e36d685ee6d65f2721b4a04 Mon Sep 17 00:00:00 2001 From: rolv Date: Tue, 12 Nov 2024 22:51:36 +0000 Subject: [PATCH 10/14] ci: add `cargo doc` step to the `check` workflow --- .github/workflows/check.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 17b0490..d2c46ac 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -32,11 +32,14 @@ jobs: - name: Check run: cargo check --all + - name: Format + run: cargo fmt --all --check + - name: Build run: cargo build --all --verbose - - name: Format - run: cargo fmt --all --check + - name: Docs + run: cargo doc --all --verbose - name: Lint run: cargo clippy --all -- -D warnings From 87b6efefb92de77515a534ea36dffb2bb725bf2c Mon Sep 17 00:00:00 2001 From: rolv Date: Tue, 12 Nov 2024 23:00:00 +0000 Subject: [PATCH 11/14] ci: run `docker-compose` stack so that the tests can actually be run in CI --- .github/workflows/check.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d2c46ac..487714d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -45,4 +45,16 @@ jobs: run: cargo clippy --all -- -D warnings - name: Tests - run: cargo test --all --verbose + run: | + # Full tests for Linux only, due to requirement of running local API hosted through docker + if [ "$RUNNER_OS" == "Linux" ]; then + docker compose up -d --wait + # Wait for Frankfurter container to fetch data + sleep 60 + cargo test --all --verbose + docker compose down + else + cargo test --package lib_frankfurter --lib + cargo test --package frankfurter_cli --bin frs + fi + shell: bash From 9a5196102f90f4beda0d9ff30c6b3c99aa95c839 Mon Sep 17 00:00:00 2001 From: rolv Date: Wed, 13 Nov 2024 13:04:40 +0000 Subject: [PATCH 12/14] fix: exit with error code on error --- cli/src/cli/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/cli/mod.rs b/cli/src/cli/mod.rs index 3db2f96..fdc45f5 100644 --- a/cli/src/cli/mod.rs +++ b/cli/src/cli/mod.rs @@ -1,4 +1,4 @@ -use std::{env::var, io::Write}; +use std::{env::var, io::Write, process::exit}; use clap::{Parser, Subcommand}; use enum_dispatch::enum_dispatch; @@ -110,6 +110,7 @@ impl Cli { } writeln!(&mut stderr, "Error: {e}").expect("Failed to write to stderr"); + exit(1); } } } From 84570ff57f5f2652771567f610fe61a03a7acae2 Mon Sep 17 00:00:00 2001 From: rolv Date: Wed, 13 Nov 2024 13:07:12 +0000 Subject: [PATCH 13/14] tests(cli): add basic integration tests for the CLI --- Cargo.lock | 139 +++++++++++++++++++++++++++++++++++++++++++++++ cli/Cargo.toml | 4 ++ cli/tests/cli.rs | 117 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 cli/tests/cli.rs diff --git a/Cargo.lock b/Cargo.lock index 0be20d9..138decc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -87,6 +96,22 @@ version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -126,6 +151,17 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -270,6 +306,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "displaydoc" version = "0.2.5" @@ -281,6 +323,12 @@ dependencies = [ "syn", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -324,6 +372,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -359,12 +416,14 @@ name = "frankfurter_cli" version = "0.0.0" dependencies = [ "anyhow", + "assert_cmd", "chrono", "clap", "comfy-table", "enum_dispatch", "is-terminal", "lib_frankfurter", + "predicates", "serde_json", "strum", "termcolor", @@ -893,6 +952,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-traits" version = "0.2.19" @@ -1008,6 +1073,36 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -1045,6 +1140,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "reqwest" version = "0.12.9" @@ -1407,6 +1531,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "thiserror" version = "1.0.65" @@ -1583,6 +1713,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2c22ea8..5026828 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,3 +26,7 @@ termcolor = { version = "1.4.1" } is-terminal = { version = "0.4.13" } comfy-table = "7.1.1" anyhow = "1.0.91" + +[dev-dependencies] +assert_cmd = "2.0.16" +predicates = "3.1.2" diff --git a/cli/tests/cli.rs b/cli/tests/cli.rs new file mode 100644 index 0000000..a7dae5e --- /dev/null +++ b/cli/tests/cli.rs @@ -0,0 +1,117 @@ +use assert_cmd::Command; +use predicates::{ + prelude::PredicateBooleanExt, + str::{contains, ends_with, is_match, starts_with}, +}; + +fn get_cmd() -> Command { + let mut cmd = Command::cargo_bin("frs").unwrap(); + cmd.arg("--url=http://localhost:8080"); + cmd +} + +#[test] +fn test_currencies_basic() { + get_cmd() + .arg("currencies") + .assert() + .stdout( + contains("AUD") + .and(contains("USD")) + .and(contains("GBP")) + .and(contains("United States Dollar")) + .and(is_match("\\d").unwrap().not()), + ) + .success(); +} + +#[test] +fn test_currencies_json() { + get_cmd() + .arg("currencies") + .arg("--json") + .assert() + .stdout( + starts_with("{") + .and(ends_with("}\n")) + .and(contains("\"EUR\": \"Euro\"")), + ) + .success(); +} + +#[test] +fn test_currencies_raw() { + get_cmd() + .arg("currencies") + .arg("--raw") + .assert() + .stdout(starts_with("AUD\tAustralian Dollar").and(contains("EUR\tEuro"))) + .success(); +} + +#[test] +fn test_convert_basic() { + get_cmd() + .arg("convert") + .assert() + .stdout( + contains("AUD") + .and(contains("USD")) + .and(contains("GBP")) + .and(is_match("\\d").unwrap()), + ) + .success(); +} + +#[test] +fn test_convert_targets() { + get_cmd() + .args(["convert", "USD", "EUR,GBP"]) + .assert() + .stdout( + contains("EUR") + .and(contains("GBP")) + .and(is_match("\\d").unwrap()) + .and(contains("USD").not()), + ) + .success(); +} + +#[test] +fn test_convert_amount() { + get_cmd() + .args(["convert", "-a", "1000", "--json"]) + .assert() + .stdout(contains("1000").and(is_match("\\d").unwrap())) + .success(); +} + +#[test] +fn test_period_basic() { + get_cmd() + .args(["period", "EUR", "2024-10-10"]) + .assert() + .stdout( + contains("2024-10-10") + .and(contains("2024-10-11")) + .and(contains("2024-11-05")) + .and(contains("AUD")) + .and(contains("USD")) + .and(contains("GBP")), + ) + .success(); +} + +#[test] +fn test_period_end_date() { + get_cmd() + .args(["period", "EUR", "2020-5-12", "2020-5-13"]) + .assert() + .stdout( + contains("2020-05-12") + .and(contains("2020-05-13")) + .and(contains("2024-05-11").not()) + .and(contains("2024-05-14").not()), + ) + .success(); +} From 6365bd572982b352fa75529a3bca994d6286775d Mon Sep 17 00:00:00 2001 From: rolv Date: Wed, 13 Nov 2024 15:36:20 +0000 Subject: [PATCH 14/14] ci: add workflow to verify minimum Rust version --- .github/workflows/msrv.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/msrv.yml diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml new file mode 100644 index 0000000..fb3bc4c --- /dev/null +++ b/.github/workflows/msrv.yml @@ -0,0 +1,30 @@ +on: + workflow_dispatch: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +name: MSRV check + +jobs: + msrv_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install stable toolchain + run: rustup toolchain install stable + + - name: Install Cargo MSRV + uses: actions-rs/install@v0.1 + with: + crate: cargo-msrv + version: latest + use-tool-cache: true + + - name: Verify minimum Rust version for the library and CLI + run: | + cargo msrv verify --path ./lib + cargo msrv verify --path ./cli +