move decimal fn to common and reuse it with client and server
This commit is contained in:
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
prost-types = { workspace = true }
|
||||
rust_decimal = { workspace = true }
|
||||
|
||||
tonic = "0.14.6"
|
||||
prost = "0.14.4"
|
||||
|
||||
112
common/src/decimal.rs
Normal file
112
common/src/decimal.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
// common/src/decimal.rs
|
||||
//!
|
||||
//! The one canonical decimal spelling shared by the client and the server.
|
||||
//!
|
||||
//! Every `NUMERIC` column travels the wire as a string so no value passes
|
||||
//! through `f64`. Both ends parse that string with [`parse_decimal_exact`], so
|
||||
//! the client rejects exactly what the server would reject and a value that
|
||||
//! parses locally is guaranteed to be accepted.
|
||||
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
/// Parses a decimal written in canonical base-10 notation.
|
||||
///
|
||||
/// Rejects anything ambiguous or non-finite: exponents (`1e2`), grouping
|
||||
/// (`1,00`), a leading `+`, surrounding whitespace, `NaN`/`inf`, and values
|
||||
/// outside [`Decimal`]'s range.
|
||||
pub fn parse_decimal_exact(value: &str) -> Result<Decimal, String> {
|
||||
if value.is_empty() || value.len() > 128 {
|
||||
return Err("Decimal must contain between 1 and 128 characters".to_string());
|
||||
}
|
||||
let unsigned = value.strip_prefix('-').unwrap_or(value);
|
||||
if unsigned.is_empty() {
|
||||
return Err("Decimal must contain digits".to_string());
|
||||
}
|
||||
let mut parts = unsigned.split('.');
|
||||
let integer = parts.next().unwrap_or_default();
|
||||
let fraction = parts.next();
|
||||
if parts.next().is_some()
|
||||
|| integer.is_empty()
|
||||
|| !integer.bytes().all(|byte| byte.is_ascii_digit())
|
||||
|| fraction.is_some_and(|fraction| {
|
||||
fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit())
|
||||
})
|
||||
{
|
||||
return Err("Decimal must use canonical base-10 notation".to_string());
|
||||
}
|
||||
Decimal::from_str_exact(value).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
/// True for the `data_type` spellings `GetTableStructure` reports for a decimal
|
||||
/// column: `NUMERIC` (from `numeric` and `money`), `NUMERIC(p)` and
|
||||
/// `NUMERIC(p,s)` (from `decimal(p,s)`).
|
||||
pub fn is_decimal_data_type(data_type: &str) -> bool {
|
||||
data_type
|
||||
.trim()
|
||||
.to_ascii_uppercase()
|
||||
.starts_with(DECIMAL_DATA_TYPE_PREFIX)
|
||||
}
|
||||
|
||||
const DECIMAL_DATA_TYPE_PREFIX: &str = "NUMERIC";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn canonical_parser_rejects_ambiguous_or_non_finite_input() {
|
||||
for value in [
|
||||
"",
|
||||
" 1",
|
||||
"1 ",
|
||||
"1,00",
|
||||
"1_000",
|
||||
"+1",
|
||||
"1e2",
|
||||
"NaN",
|
||||
"inf",
|
||||
"+inf",
|
||||
"--1",
|
||||
"-",
|
||||
".5",
|
||||
"1.",
|
||||
"1.2.3",
|
||||
"79228162514264337593543950336",
|
||||
"8000000000000000000000000000.1",
|
||||
"0.00000000000000000000000000001",
|
||||
] {
|
||||
assert!(
|
||||
parse_decimal_exact(value).is_err(),
|
||||
"unexpectedly accepted {value:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_parser_preserves_the_written_scale() {
|
||||
for value in [
|
||||
"0",
|
||||
"-0.01",
|
||||
"12.50",
|
||||
"12.500",
|
||||
"123456789012345678901.25",
|
||||
"-79228162514264337593543950335",
|
||||
] {
|
||||
assert_eq!(
|
||||
parse_decimal_exact(value).unwrap().to_string(),
|
||||
value,
|
||||
"round trip changed {value:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_data_type_covers_every_numeric_spelling() {
|
||||
for data_type in ["NUMERIC", "NUMERIC(12)", "NUMERIC(12,3)", "numeric(12,3)"] {
|
||||
assert!(is_decimal_data_type(data_type), "missed {data_type}");
|
||||
}
|
||||
for data_type in ["TEXT", "INT8", "TIMESTAMPTZ", "VARCHAR(255)", ""] {
|
||||
assert!(!is_decimal_data_type(data_type), "matched {data_type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// common/src/lib.rs
|
||||
|
||||
pub mod search;
|
||||
pub mod decimal;
|
||||
pub mod grpc_error;
|
||||
pub mod relationship;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user