diff --git a/Cargo.lock b/Cargo.lock index a910a08c..53e223f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,7 @@ dependencies = [ "prost-build", "prost-types", "rust_decimal", + "rusty-money", "serde", "serde_json", "tantivy", diff --git a/Cargo.toml b/Cargo.toml index 506932ab..94ec1b9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ tantivy = "0.26.1" # Steel_decimal crate rust_decimal = { version = "1.42.0", features = ["maths", "serde"] } rust_decimal_macros = "1.40.0" +# One version across the workspace: `&'static iso::Currency` only crosses crate +# boundaries if every crate resolves rusty-money to the same instance. +rusty-money = "0.5.0" thiserror = "2.0.18" regex = "1.12.4" diff --git a/client b/client index e3b5d57c..520fb563 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit e3b5d57c905eb8b2a74d02d0332106e1ab990bad +Subproject commit 520fb563f4d90f411b6144a03cf6da230babfb30 diff --git a/common/Cargo.toml b/common/Cargo.toml index a6c2f4e1..cb94cdd9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] prost-types = { workspace = true } rust_decimal = { workspace = true } +rusty-money = { workspace = true } tonic = "0.14.6" prost = "0.14.4" diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index 5009fca0..2be621c3 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -131,7 +131,8 @@ message PostTableDataRequest { // - BOOLEAN: bool value // - TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) // - INTEGER: number with no fractional part and within i32 range - // - BIGINT: number with no fractional part and within i64 range + // - BIGINT: canonical integer string for the full i64 range; a number is + // accepted only within protobuf NumberValue's exact integer range // - NUMERIC(p,s): string representation only; empty string becomes NULL // (numbers for NUMERIC are rejected to avoid precision loss) // diff --git a/common/src/lib.rs b/common/src/lib.rs index 07f6b4ea..e6427035 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,6 +3,7 @@ pub mod search; pub mod decimal; pub mod grpc_error; +pub mod money; pub mod relationship; pub mod system_column; diff --git a/common/src/money.rs b/common/src/money.rs new file mode 100644 index 00000000..e092cc95 --- /dev/null +++ b/common/src/money.rs @@ -0,0 +1,53 @@ +// common/src/money.rs +//! +//! The one canonical currency spelling shared by the client and the server. +//! +//! A currency code is stored, sent and compared as its canonical uppercase +//! ISO-4217 alphabetic code. Both ends resolve it with +//! [`require_iso_currency_code`], so the client rejects exactly what the server +//! would reject, and each side renders the shared error as its own error type. + +pub use rusty_money::iso; + +/// Resolves a canonical uppercase ISO-4217 alphabetic code to its currency. +/// +/// Rejects anything not already canonical: lowercase (`eur`), surrounding +/// whitespace, and any length but three. Normalising here instead would let a +/// value be written in one spelling and compared in another. +pub fn require_iso_currency_code(currency_code: &str) -> Result<&'static iso::Currency, String> { + if currency_code.len() != 3 + || !currency_code + .chars() + .all(|character| character.is_ascii_uppercase()) + { + return Err("Currency must be a canonical uppercase ISO-4217 code".to_string()); + } + + iso::find(currency_code).ok_or_else(|| format!("Unknown ISO-4217 currency: {currency_code}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn canonical_codes_resolve_to_their_currency() { + assert_eq!(require_iso_currency_code("EUR").unwrap(), iso::EUR); + assert_eq!( + require_iso_currency_code("USD").unwrap().iso_alpha_code, + "USD" + ); + } + + #[test] + fn non_canonical_or_unknown_codes_are_rejected() { + for currency_code in [ + "", "E", "EU", "EURO", "eur", "Eur", " EUR", "EUR ", "E R", "E\u{20AC}R", "AAA", "123", + ] { + assert!( + require_iso_currency_code(currency_code).is_err(), + "unexpectedly accepted {currency_code:?}" + ); + } + } +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 2180e340..13a93b58 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index 3b8b3c78..2fd878ab 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -27,7 +27,8 @@ pub struct PostTableDataRequest { /// * BOOLEAN: bool value /// * TIMESTAMPTZ: ISO 8601/RFC 3339 string (parsed to TIMESTAMPTZ) /// * INTEGER: number with no fractional part and within i32 range - /// * BIGINT: number with no fractional part and within i64 range + /// * BIGINT: canonical integer string for the full i64 range; a number is + /// accepted only within protobuf NumberValue's exact integer range /// * NUMERIC(p,s): string representation only; empty string becomes NULL /// (numbers for NUMERIC are rejected to avoid precision loss) /// diff --git a/server b/server index 412af6ea..62be9cbb 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 412af6ea3f147bd79ae4984d0adfa60b37091a5c +Subproject commit 62be9cbbaf0f5a654083967601fc827ab035c7f7 diff --git a/tui-canvas b/tui-canvas index 1251d224..b1761279 160000 --- a/tui-canvas +++ b/tui-canvas @@ -1 +1 @@ -Subproject commit 1251d224067523abcd6d9107adf0d1a062693b6c +Subproject commit b1761279ff760cae62d7bf33f04311d444e5ea83 diff --git a/tui-pages b/tui-pages index 29e1e6a4..46afc191 160000 --- a/tui-pages +++ b/tui-pages @@ -1 +1 @@ -Subproject commit 29e1e6a42e2e9d76fac5a226098a52fd50ca30c0 +Subproject commit 46afc191dbd4cd9d047632401047255b6fc8e24d