use rust_decimal::Decimal;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DecimalError {
#[error("Invalid decimal value {value}: {reason}")]
InvalidValue {
value: f64,
reason: String,
},
#[error("Decimal arithmetic error during {operation}: {reason}")]
ArithmeticError {
operation: String,
reason: String,
},
#[error("Failed to convert decimal from {from_type} to {to_type}: {reason}")]
ConversionError {
from_type: String,
to_type: String,
reason: String,
},
#[error("Decimal value {value} is out of bounds (min: {min}, max: {max})")]
OutOfBounds {
value: f64,
min: f64,
max: f64,
},
#[error("Invalid decimal precision {precision}: {reason}")]
InvalidPrecision {
precision: i32,
reason: String,
},
#[error(transparent)]
ExpirationDate(expiration_date::error::ExpirationDateError),
#[error("Decimal {operation} overflow: {lhs} op {rhs}")]
Overflow {
operation: &'static str,
lhs: Decimal,
rhs: Decimal,
},
}
pub type DecimalResult<T> = Result<T, DecimalError>;
impl DecimalError {
#[must_use]
#[cold]
#[inline(never)]
pub fn invalid_value(value: f64, reason: &str) -> Self {
DecimalError::InvalidValue {
value,
reason: reason.to_string(),
}
}
#[cold]
#[inline(never)]
#[must_use]
pub fn arithmetic_error(operation: &str, reason: &str) -> Self {
DecimalError::ArithmeticError {
operation: operation.to_string(),
reason: reason.to_string(),
}
}
#[cold]
#[inline(never)]
#[must_use]
pub fn conversion_error(from_type: &str, to_type: &str, reason: &str) -> Self {
DecimalError::ConversionError {
from_type: from_type.to_string(),
to_type: to_type.to_string(),
reason: reason.to_string(),
}
}
#[cold]
#[inline(never)]
#[must_use]
pub fn out_of_bounds(value: f64, min: f64, max: f64) -> Self {
DecimalError::OutOfBounds { value, min, max }
}
#[cold]
#[inline(never)]
#[must_use]
pub fn invalid_precision(precision: i32, reason: &str) -> Self {
DecimalError::InvalidPrecision {
precision,
reason: reason.to_string(),
}
}
#[cold]
#[inline(never)]
#[must_use]
pub fn overflow(operation: &'static str, lhs: Decimal, rhs: Decimal) -> Self {
DecimalError::Overflow {
operation,
lhs,
rhs,
}
}
}
impl From<expiration_date::error::ExpirationDateError> for DecimalError {
#[inline]
fn from(err: expiration_date::error::ExpirationDateError) -> Self {
DecimalError::ExpirationDate(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_value_error() {
let error = DecimalError::invalid_value(-1.0, "Value cannot be negative");
assert!(matches!(error, DecimalError::InvalidValue { .. }));
assert!(error.to_string().contains("cannot be negative"));
}
#[test]
fn test_arithmetic_error() {
let error = DecimalError::arithmetic_error("division", "Division by zero");
assert!(matches!(error, DecimalError::ArithmeticError { .. }));
assert!(error.to_string().contains("Division by zero"));
}
#[test]
fn test_conversion_error() {
let error = DecimalError::conversion_error("f64", "Decimal", "Value out of range");
assert!(matches!(error, DecimalError::ConversionError { .. }));
assert!(error.to_string().contains("out of range"));
}
#[test]
fn test_out_of_bounds_error() {
let error = DecimalError::out_of_bounds(150.0, 0.0, 100.0);
assert!(matches!(error, DecimalError::OutOfBounds { .. }));
assert!(error.to_string().contains("150"));
}
#[test]
fn test_invalid_precision_error() {
let error = DecimalError::invalid_precision(-1, "Precision must be non-negative");
assert!(matches!(error, DecimalError::InvalidPrecision { .. }));
assert!(error.to_string().contains("non-negative"));
}
#[test]
fn test_overflow_error() {
let error = DecimalError::overflow("test::site", Decimal::MAX, Decimal::MAX);
assert!(matches!(error, DecimalError::Overflow { .. }));
let rendered = error.to_string();
assert!(rendered.contains("test::site"));
assert!(rendered.contains("overflow"));
}
#[test]
fn test_overflow_fields() {
let lhs = Decimal::MAX;
let rhs = Decimal::from(2);
if let DecimalError::Overflow {
operation,
lhs: captured_lhs,
rhs: captured_rhs,
} = DecimalError::overflow("test::mul", lhs, rhs)
{
assert_eq!(operation, "test::mul");
assert_eq!(captured_lhs, lhs);
assert_eq!(captured_rhs, rhs);
} else {
panic!("expected Overflow variant");
}
}
}