diff options
author | rtkay123 <dev@kanjala.com> | 2025-08-09 10:36:07 +0200 |
---|---|---|
committer | rtkay123 <dev@kanjala.com> | 2025-08-09 10:36:07 +0200 |
commit | affa986bf1f84b725bd23309986250ff04cf2c93 (patch) | |
tree | 00faafcbdf1962793793e7581984078ce3466085 | |
parent | 0f663ccb94581264e839bab9ae386114e8bd9973 (diff) | |
download | warden-affa986bf1f84b725bd23309986250ff04cf2c93.tar.bz2 warden-affa986bf1f84b725bd23309986250ff04cf2c93.zip |
feat: data cache
-rw-r--r-- | Cargo.lock | 55 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | crates/warden/Cargo.toml | 5 | ||||
-rw-r--r-- | crates/warden/src/server/routes/processor/pacs008.rs | 113 | ||||
-rw-r--r-- | lib/warden-core/Cargo.toml | 13 | ||||
-rw-r--r-- | lib/warden-core/build.rs | 13 | ||||
-rw-r--r-- | lib/warden-core/src/google.rs | 2 | ||||
-rw-r--r-- | lib/warden-core/src/google/parser.rs | 4 | ||||
-rw-r--r-- | lib/warden-core/src/google/parser/dt.rs | 233 | ||||
-rw-r--r-- | lib/warden-core/src/google/parser/money.rs | 293 | ||||
-rw-r--r-- | lib/warden-core/src/lib.rs | 5 | ||||
-rw-r--r-- | lib/warden-core/src/message.rs | 1 | ||||
-rw-r--r-- | proto/warden_message.proto | 27 |
13 files changed, 759 insertions, 6 deletions
@@ -385,6 +385,16 @@ dependencies = [ ] [[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] name = "derive_arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -944,6 +954,12 @@ dependencies = [ ] [[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1040,6 +1056,12 @@ dependencies = [ ] [[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] name = "prettyplease" version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1481,6 +1503,37 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] name = "tinystr" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1922,6 +1975,7 @@ dependencies = [ "serde", "serde_json", "stack-up", + "time", "tokio", "tower", "tracing", @@ -1941,6 +1995,7 @@ dependencies = [ "prost", "serde", "serde_json", + "time", "tonic", "tonic-prost-build", "tonic-types", @@ -17,6 +17,7 @@ prost = "0.14.1" serde = "1.0.219" serde_json = "1.0.142" stack-up = { git = "https://github.com/rtkay123/stack-up" } +time = "0.3.41" tokio = "1.47.1" tonic = "0.14.0" tower = "0.5.2" diff --git a/crates/warden/Cargo.toml b/crates/warden/Cargo.toml index 20c646b..231ccae 100644 --- a/crates/warden/Cargo.toml +++ b/crates/warden/Cargo.toml @@ -14,6 +14,7 @@ clap = { workspace = true, features = ["derive"] } config = { workspace = true, features = ["convert-case", "toml"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +time.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } tracing.workspace = true utoipa = { workspace = true, features = ["axum_extras"] } @@ -22,10 +23,10 @@ utoipa-rapidoc = { workspace = true, optional = true } utoipa-redoc = { workspace = true, optional = true } utoipa-scalar = { workspace = true, optional = true } utoipa-swagger-ui = { workspace = true, optional = true } -warden-core = { workspace = true, features = ["iso20022", "openapi"] } +warden-core = { workspace = true, features = ["iso20022", "serde", "openapi"] } [features] -# default = [] +default = [] swagger = ["dep:utoipa-swagger-ui", "utoipa-swagger-ui/axum"] redoc = ["dep:utoipa-redoc", "utoipa-redoc/axum"] rapidoc = ["dep:utoipa-rapidoc", "utoipa-rapidoc/axum"] diff --git a/crates/warden/src/server/routes/processor/pacs008.rs b/crates/warden/src/server/routes/processor/pacs008.rs index cde5c07..66df598 100644 --- a/crates/warden/src/server/routes/processor/pacs008.rs +++ b/crates/warden/src/server/routes/processor/pacs008.rs @@ -1,5 +1,9 @@ use axum::{extract::State, response::IntoResponse}; -use warden_core::iso20022::pacs008::Pacs008Document; +use tracing::trace; +use warden_core::{ + iso20022::{TransactionType, pacs008::Pacs008Document}, + message::DataCache, +}; use crate::{error::AppError, server::routes::PACS008_001_12, state::AppHandle, version::Version}; @@ -31,5 +35,112 @@ pub(super) async fn post_pacs008( State(state): State<AppHandle>, axum::Json(transaction): axum::Json<Pacs008Document>, ) -> Result<impl IntoResponse, AppError> { + let tx_tp = TransactionType::PACS008; Ok(String::default()) } + +pub fn build_data_cache(transaction: &Pacs008Document) -> anyhow::Result<DataCache> { + trace!("building data cache object"); + let cdt_trf_tx_inf = transaction.f_i_to_f_i_cstmr_cdt_trf.cdt_trf_tx_inf.first(); + + let instd_amt = cdt_trf_tx_inf.and_then(|value| value.instd_amt.clone()); + + let intr_bk_sttlm_amt = cdt_trf_tx_inf.and_then(|value| value.intr_bk_sttlm_amt.clone()); + + let xchg_rate = cdt_trf_tx_inf.and_then(|value| value.xchg_rate); + let cre_dt_tm = transaction.f_i_to_f_i_cstmr_cdt_trf.grp_hdr.cre_dt_tm; + + let dbtr_othr = cdt_trf_tx_inf.and_then(|value| { + value + .dbtr + .id + .as_ref() + .and_then(|value| value.prvt_id.othr.first()) + }); + + let debtor_id = dbtr_othr + .and_then(|value| { + value + .schme_nm + .as_ref() + .map(|schme_nm| format!("{}{}", value.id, schme_nm.prtry)) + }) + .ok_or_else(|| anyhow::anyhow!("missing debtor id"))?; + + let cdtr_othr = cdt_trf_tx_inf.and_then(|value| { + value.cdtr.as_ref().and_then(|value| { + value + .id + .as_ref() + .and_then(|value| value.prvt_id.othr.first()) + }) + }); + + let creditor_id = cdtr_othr + .and_then(|value| { + value + .schme_nm + .as_ref() + .map(|schme_nm| format!("{}{}", value.id, schme_nm.prtry)) + }) + .ok_or_else(|| anyhow::anyhow!("missing creditor id"))?; + + let dbtr_acct_othr = cdt_trf_tx_inf.and_then(|value| { + value + .dbtr_acct + .as_ref() + .and_then(|value| value.id.as_ref().map(|value| value.othr.clone())) + }); + let dbtr_mmb_id = cdt_trf_tx_inf.and_then(|value| { + value.dbtr_agt.as_ref().and_then(|value| { + value + .fin_instn_id + .clr_sys_mmb_id + .as_ref() + .map(|value| value.mmb_id.as_str()) + }) + }); + + let debtor_acct_id = if let (Some(a), Some(b)) = (dbtr_acct_othr, dbtr_mmb_id) { + Some(format!("{}{b}", a.id)) + } else { + None + } + .ok_or_else(|| anyhow::anyhow!("missing debtor_acct_id"))?; + + let cdtr_acct_othr = cdt_trf_tx_inf.and_then(|value| { + value + .cdtr_acct + .as_ref() + .and_then(|value| value.id.as_ref().map(|value| value.othr.clone())) + }); + let cdtr_mmb_id = cdt_trf_tx_inf.and_then(|value| { + value.cdtr_agt.as_ref().and_then(|value| { + value + .fin_instn_id + .clr_sys_mmb_id + .as_ref() + .map(|value| value.mmb_id.as_str()) + }) + }); + + let creditor_acct_id = if let (Some(a), Some(b)) = (cdtr_acct_othr, cdtr_mmb_id) { + Some(format!("{}{b}", a.id)) + } else { + None + } + .ok_or_else(|| anyhow::anyhow!("missing creditor_acct_id"))?; + + let data_cache = DataCache { + cdtr_id: creditor_id.to_string(), + dbtr_id: debtor_id.to_string(), + dbtr_acct_id: debtor_acct_id.to_string(), + cdtr_acct_id: creditor_acct_id.to_string(), + cre_dt_tm: Some(cre_dt_tm), + instd_amt, + intr_bk_sttlm_amt, + xchg_rate, + }; + + Ok(data_cache) +} diff --git a/lib/warden-core/Cargo.toml b/lib/warden-core/Cargo.toml index 9cd4617..bdf0af0 100644 --- a/lib/warden-core/Cargo.toml +++ b/lib/warden-core/Cargo.toml @@ -15,6 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"] prost = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } +time = { workspace = true, optional = true } tonic = { workspace = true, optional = true } tonic-types = { version = "0.14.0", optional = true } utoipa = { workspace = true, optional = true } @@ -23,7 +24,17 @@ utoipa = { workspace = true, optional = true } default = [] iso20022 = ["dep:prost", "dep:tonic", "dep:tonic-types"] serde = ["dep:serde", "serde/derive", "dep:serde_json"] -openapi = ["dep:utoipa", "serde"] +serde-time = [ + "time", + "serde", + "time/serde", +] +time = [ + "time/parsing", + "time/formatting", + "time/macros", +] +openapi = ["dep:utoipa", "serde-time", "utoipa/time"] [build-dependencies] tonic-prost-build = { version = "0.14.0", features = ["cleanup-markdown"] } diff --git a/lib/warden-core/build.rs b/lib/warden-core/build.rs index 7ae6b6a..1998137 100644 --- a/lib/warden-core/build.rs +++ b/lib/warden-core/build.rs @@ -14,6 +14,7 @@ impl Entity { vec![ "proto/iso20022/pacs_008_001_12.proto", "proto/iso20022/pacs_002_001_12.proto", + "proto/warden_message.proto", ] } @@ -78,10 +79,18 @@ fn build_proto(package: &str, entity: Entity) -> Result<(), Box<dyn std::error:: #[cfg(all(feature = "serde", feature = "iso20022"))] fn add_serde(config: tonic_prost_build::Builder) -> tonic_prost_build::Builder { - config.type_attribute( + let config = config.type_attribute( ".", "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"snake_case\")]", - ) + ); + + #[cfg(feature = "time")] + let config = config.type_attribute( + ".google.protobuf.Timestamp", + "#[serde(try_from = \"time::OffsetDateTime\")] #[serde(into = \"String\")]", + ); + + config } #[cfg(all(feature = "openapi", feature = "iso20022"))] diff --git a/lib/warden-core/src/google.rs b/lib/warden-core/src/google.rs index 365766c..0e9487d 100644 --- a/lib/warden-core/src/google.rs +++ b/lib/warden-core/src/google.rs @@ -1,3 +1,5 @@ +mod parser; + /// Well known types pub mod protobuf { include!(concat!(env!("OUT_DIR"), "/google.protobuf.rs")); diff --git a/lib/warden-core/src/google/parser.rs b/lib/warden-core/src/google/parser.rs new file mode 100644 index 0000000..7405077 --- /dev/null +++ b/lib/warden-core/src/google/parser.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "time")] +mod dt; + +//mod money; diff --git a/lib/warden-core/src/google/parser/dt.rs b/lib/warden-core/src/google/parser/dt.rs new file mode 100644 index 0000000..ced6f12 --- /dev/null +++ b/lib/warden-core/src/google/parser/dt.rs @@ -0,0 +1,233 @@ +use crate::google::{protobuf::Timestamp, r#type::Date}; + +impl From<time::OffsetDateTime> for Date { + fn from(dt: time::OffsetDateTime) -> Self { + Self { + year: dt.year(), + month: dt.month() as i32, + day: dt.day() as i32, + } + } +} + +impl From<time::Date> for Date { + fn from(value: time::Date) -> Self { + Self { + year: value.year(), + month: value.month() as i32, + day: value.day() as i32, + } + } +} + +impl TryFrom<Date> for time::Date { + type Error = time::Error; + + fn try_from(value: Date) -> Result<Self, Self::Error> { + Ok(Self::from_calendar_date( + value.year, + time::Month::try_from(value.month as u8)?, + value.day as u8, + )?) + } +} + +impl std::str::FromStr for Date { + type Err = time::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let date = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339) + .map(Date::from); + + match date { + Ok(dt) => Ok(dt), + Err(_e) => { + let my_format = time::macros::format_description!("[year]-[month]-[day]"); + let date = time::Date::parse(s, &my_format)?; + Ok(Date::from(date)) + } + } + } +} + +impl TryFrom<String> for Date { + type Error = time::Error; + + fn try_from(value: String) -> Result<Self, Self::Error> { + <Date as std::str::FromStr>::from_str(&value) + } +} + +impl TryFrom<DateItem> for Date { + type Error = time::Error; + + fn try_from(value: DateItem) -> Result<Self, Self::Error> { + match value { + DateItem::String(ref string) => <Date as std::str::FromStr>::from_str(string), + #[cfg(feature = "iso20022")] + DateItem::Date { year, month, day } => Ok(Date { year, month, day }), + DateItem::Timestamp { seconds, nanos } => { + let odt = time::OffsetDateTime::try_from(crate::google::protobuf::Timestamp { + seconds, + nanos, + })?; + Ok(Self { + year: odt.year(), + month: odt.month() as i32, + day: odt.day() as i32, + }) + } + } + } +} + +impl From<Date> for String { + fn from(value: Date) -> Self { + let prepend = |value: i32| -> String { + match value.lt(&10) { + true => format!("0{}", value), + false => value.to_string(), + } + }; + format!( + "{}-{}-{}", + value.year, + prepend(value.month), + prepend(value.day), + ) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] +/// Date utility +#[derive(Clone, Debug)] +pub enum DateItem { + /// string + String(String), + /// ts + Timestamp { seconds: i64, nanos: i32 }, + /// date + #[cfg(feature = "iso20022")] + Date { year: i32, month: i32, day: i32 }, +} + +impl TryFrom<DateItem> for Timestamp { + type Error = time::Error; + + fn try_from(value: DateItem) -> Result<Self, Self::Error> { + match value { + DateItem::String(ref string) => <Timestamp as std::str::FromStr>::from_str(string), + #[cfg(feature = "iso20022")] + DateItem::Date { year, month, day } => { + let date = time::Date::try_from(crate::google::r#type::Date { year, month, day })?; + let time = time::Time::MIDNIGHT; + let offset = time::UtcOffset::UTC; + Ok(date.with_time(time).assume_offset(offset).into()) + } + DateItem::Timestamp { seconds, nanos } => Ok(Self { seconds, nanos }), + } + } +} + +impl From<time::OffsetDateTime> for Timestamp { + fn from(dt: time::OffsetDateTime) -> Self { + Timestamp { + seconds: dt.unix_timestamp(), + nanos: dt.nanosecond() as i32, + } + } +} + +impl From<Timestamp> for String { + fn from(value: Timestamp) -> Self { + let odt = time::OffsetDateTime::try_from(value).expect("invalid date"); + odt.format(&time::format_description::well_known::Rfc3339) + .expect("format is not rfc3339") + } +} + +impl std::str::FromStr for Timestamp { + type Err = time::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let timestamp = + time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)?; + + Ok(Timestamp::from(timestamp)) + } +} + +impl TryFrom<String> for Timestamp { + type Error = time::Error; + + fn try_from(value: String) -> Result<Self, Self::Error> { + <Timestamp as std::str::FromStr>::from_str(&value) + } +} + +impl TryFrom<Timestamp> for time::OffsetDateTime { + type Error = time::Error; + + fn try_from(value: Timestamp) -> Result<Self, Self::Error> { + let dt = time::OffsetDateTime::from_unix_timestamp(value.seconds)?; + + Ok(dt.replace_nanosecond(value.nanos as u32)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use time::{Duration, OffsetDateTime}; + + #[test] + fn test_offsetdatetime_to_timestamp() { + let now = OffsetDateTime::now_utc(); + let timestamp: Timestamp = now.into(); + + assert_eq!(timestamp.seconds, now.unix_timestamp()); + assert_eq!(timestamp.nanos, now.nanosecond() as i32); + } + + #[test] + fn test_timestamp_to_offsetdatetime() { + let now = OffsetDateTime::now_utc(); + let timestamp: Timestamp = now.into(); + let dt: OffsetDateTime = timestamp.try_into().unwrap(); + + assert_eq!(dt, now); + } + + #[test] + fn test_timestamp_to_offsetdatetime_with_nanos() { + let now = OffsetDateTime::now_utc(); + let nanos = 123456789; + let dt = now + Duration::nanoseconds(nanos); + let timestamp: Timestamp = dt.into(); + let dt_from_timestamp: OffsetDateTime = timestamp.try_into().unwrap(); + + assert_eq!(dt_from_timestamp, dt); + } + + #[test] + fn test_timestamp_to_offsetdatetime_with_negative_nanos() { + let now = OffsetDateTime::now_utc(); + let nanos = -123456789; + let dt = now + Duration::nanoseconds(nanos); + let timestamp: Timestamp = dt.into(); + let dt_from_timestamp: OffsetDateTime = timestamp.try_into().unwrap(); + + assert_eq!(dt_from_timestamp, dt); + } + + #[test] + fn test_timestamp_to_offsetdatetime_invalid_seconds() { + let timestamp = Timestamp { + seconds: i64::MIN, + nanos: 0, + }; + let result: Result<OffsetDateTime, time::Error> = timestamp.try_into(); + assert!(result.is_err()); + } +} diff --git a/lib/warden-core/src/google/parser/money.rs b/lib/warden-core/src/google/parser/money.rs new file mode 100644 index 0000000..54ed5fa --- /dev/null +++ b/lib/warden-core/src/google/parser/money.rs @@ -0,0 +1,293 @@ +use crate::google::r#type::Date; + +/// If money cannot be created +#[derive(Debug, PartialEq)] +pub enum MoneyError { + /// Invalid currency code + InvalidCurrencyCode, +} + +impl<T> TryFrom<(f64, T)> for Money +where + T: AsRef<str>, +{ + type Error = MoneyError; + + fn try_from((value, ccy): (f64, T)) -> Result<Self, Self::Error> { + let currency_code = ccy.as_ref(); + + let is_valid = + currency_code.len() == 3 && currency_code.chars().all(|c| c.is_ascii_uppercase()); + + if !is_valid { + return Err(MoneyError::InvalidCurrencyCode); + } + + let units = value.trunc() as i64; + let nanos_raw = ((value - units as f64) * 1_000_000_000.0).round() as i32; + + let (units, nanos) = if units > 0 && nanos_raw < 0 { + (units - 1, nanos_raw + 1_000_000_000) + } else if units < 0 && nanos_raw > 0 { + (units + 1, nanos_raw - 1_000_000_000) + } else { + (units, nanos_raw) + }; + + Ok(Money { + currency_code: currency_code.to_string(), + units, + nanos, + }) + } +} + +impl From<Money> for f64 { + fn from(m: Money) -> Self { + m.units as f64 + (m.nanos as f64) / 1_000_000_000.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::TryFrom; + + #[test] + fn test_valid_money_conversion() { + let money = Money::try_from((1.75, "USD")).unwrap(); + assert_eq!( + money, + Money { + currency_code: "USD".to_string(), + units: 1, + nanos: 750_000_000 + } + ); + + let money = Money::try_from((-1.75, "EUR")).unwrap(); + assert_eq!( + money, + Money { + currency_code: "EUR".to_string(), + units: -1, + nanos: -750_000_000 + } + ); + + let money = Money::try_from((0.333_333_333, "JPY")).unwrap(); + assert_eq!( + money, + Money { + currency_code: "JPY".to_string(), + units: 0, + nanos: 333_333_333 + } + ); + } + + #[test] + fn test_sign_correction() { + let m = Money::try_from((1.000_000_001, "USD")).unwrap(); + assert_eq!(m.units, 1); + assert_eq!(m.nanos, 1); + + let m = Money::try_from((-1.000_000_001, "USD")).unwrap(); + assert_eq!(m.units, -1); + assert_eq!(m.nanos, -1); + + let m = Money::try_from((0.000_000_001, "USD")).unwrap(); + assert_eq!(m.units, 0); + assert_eq!(m.nanos, 1); + + let m = Money::try_from((-0.000_000_001, "USD")).unwrap(); + assert_eq!(m.units, 0); + assert_eq!(m.nanos, -1); + } + + #[test] + fn test_invalid_currency_code() { + let cases = ["usd", "US", "USDD", "12A", "€€€", ""]; + + for &code in &cases { + let result = Money::try_from((1.0, code)); + assert_eq!(result, Err(MoneyError::InvalidCurrencyCode)); + } + } + + #[test] + fn test_money_to_f64_positive() { + let money = Money { + currency_code: "USD".to_string(), + units: 1, + nanos: 750_000_000, + }; + let value: f64 = money.into(); + assert_eq!(value, 1.75); + } + + #[test] + fn test_money_to_f64_negative() { + let money = Money { + currency_code: "EUR".to_string(), + units: -1, + nanos: -250_000_000, + }; + let value: f64 = money.into(); + assert_eq!(value, -1.25); + } + + #[test] + fn test_money_to_f64_zero() { + let money = Money { + currency_code: "JPY".to_string(), + units: 0, + nanos: 0, + }; + let value: f64 = money.into(); + assert_eq!(value, 0.0); + } + + #[test] + fn test_money_to_f64_fractional_positive() { + let money = Money { + currency_code: "USD".to_string(), + units: 0, + nanos: 1, + }; + let value: f64 = money.into(); + assert_eq!(value, 1.0e-9); + } + + #[test] + fn test_money_to_f64_fractional_negative() { + let money = Money { + currency_code: "USD".to_string(), + units: 0, + nanos: -1, + }; + let value: f64 = money.into(); + assert_eq!(value, -1.0e-9); + } + + #[test] + fn test_round_trip_conversion() { + let original = 1234.567_890_123; + let money = Money::try_from((original, "USD")).unwrap(); + let back: f64 = money.into(); + assert!( + (original - back).abs() < 1e-9, + "got {}, expected {}", + back, + original + ); + } +} + +#[cfg(feature = "time")] +impl From<time::OffsetDateTime> for Date { + fn from(dt: time::OffsetDateTime) -> Self { + Self { + year: dt.year(), + month: dt.month() as i32, + day: dt.day() as i32, + } + } +} + +#[cfg(feature = "time")] +impl From<time::Date> for Date { + fn from(value: time::Date) -> Self { + Self { + year: value.year(), + month: value.month() as i32, + day: value.day() as i32, + } + } +} + +#[cfg(feature = "time")] +impl TryFrom<Date> for time::Date { + type Error = time::Error; + + fn try_from(value: Date) -> Result<Self, Self::Error> { + Ok(Self::from_calendar_date( + value.year, + time::Month::try_from(value.month as u8)?, + value.day as u8, + )?) + } +} + +#[cfg(feature = "time")] +impl std::str::FromStr for Date { + type Err = time::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let date = time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339) + .map(Date::from); + + match date { + Ok(dt) => Ok(dt), + Err(_e) => { + let my_format = time::macros::format_description!("[year]-[month]-[day]"); + let date = time::Date::parse(&s, &my_format)?; + Ok(Date::from(date)) + } + } + } +} + +#[cfg(feature = "time")] +impl TryFrom<String> for Date { + type Error = time::Error; + + fn try_from(value: String) -> Result<Self, Self::Error> { + <Date as std::str::FromStr>::from_str(&value) + } +} + +#[cfg(feature = "time")] +impl TryFrom<super::helpers::time_util::DateItem> for Date { + type Error = time::Error; + + fn try_from(value: super::helpers::time_util::DateItem) -> Result<Self, Self::Error> { + match value { + super::helpers::time_util::DateItem::String(ref string) => { + <Date as std::str::FromStr>::from_str(string) + } + #[cfg(feature = "iso20022")] + super::helpers::time_util::DateItem::Date { year, month, day } => { + Ok(Date { year, month, day }) + } + super::helpers::time_util::DateItem::Timestamp { seconds, nanos } => { + let odt = time::OffsetDateTime::try_from(crate::google::protobuf::Timestamp { + seconds, + nanos, + })?; + Ok(Self { + year: odt.year(), + month: odt.month() as i32, + day: odt.day() as i32, + }) + } + } + } +} + +impl From<Date> for String { + fn from(value: Date) -> Self { + let prepend = |value: i32| -> String { + match value.lt(&10) { + true => format!("0{}", value), + false => value.to_string(), + } + }; + format!( + "{}-{}-{}", + value.year, + prepend(value.month), + prepend(value.day), + ) + } +} diff --git a/lib/warden-core/src/lib.rs b/lib/warden-core/src/lib.rs index 111ce5d..19d02f3 100644 --- a/lib/warden-core/src/lib.rs +++ b/lib/warden-core/src/lib.rs @@ -15,3 +15,8 @@ pub mod google; #[allow(missing_docs)] #[cfg(feature = "iso20022")] pub mod iso20022; + +/// Message in transit +#[allow(missing_docs)] +#[cfg(feature = "iso20022")] +pub mod message; diff --git a/lib/warden-core/src/message.rs b/lib/warden-core/src/message.rs new file mode 100644 index 0000000..4b8142b --- /dev/null +++ b/lib/warden-core/src/message.rs @@ -0,0 +1 @@ +tonic::include_proto!("message"); diff --git a/proto/warden_message.proto b/proto/warden_message.proto new file mode 100644 index 0000000..d9c0cd4 --- /dev/null +++ b/proto/warden_message.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package message; + +import "proto/iso20022/pacs_008_001_12.proto"; +import "proto/iso20022/pacs_002_001_12.proto"; +import "google/protobuf/timestamp.proto"; + +message Payload { + oneof transaction { + iso20022.pacs008.Pacs008Document pacs008 = 1; + iso20022.pacs002.Pacs002Document pacs002 = 2; + } + DataCache data_cache = 3; + string tx_tp = 4; +} + +message DataCache { + string cdtr_id = 1; + string dbtr_id = 2; + string dbtr_acct_id = 3; + string cdtr_acct_id = 4; + google.protobuf.Timestamp cre_dt_tm = 5; + iso20022.pacs008.ActiveOrHistoricCurrencyAndAmount instd_amt = 6; + iso20022.pacs008.ActiveCurrencyAndAmount intr_bk_sttlm_amt = 7; + optional double xchg_rate = 8; +} |