Skip to main content

icydb_model/base/normalizer/
num.rs

1//! Module: base::normalizer::num
2//!
3//! Responsibility: base normalizer definitions.
4//! Does not own: validation policy, persistence, or schema mutation semantics.
5//! Boundary: mutates schema field values through facade normalizer traits.
6
7use crate::{
8    base::helper::try_cast_decimal, prelude::*, schema::NumericValue, visitor::Normalizer,
9};
10use std::any::type_name;
11
12///
13/// Clamp
14///
15
16#[normalizer]
17pub struct Clamp {
18    min: Decimal,
19    max: Decimal,
20    invalid_configuration: bool,
21}
22
23impl Clamp {
24    /// Configure bounds; unrepresentable bounds make normalization fail without mutation.
25    pub fn new<N: NumericValue>(min: N, max: N) -> Self {
26        let min = try_cast_decimal(&min);
27        let max = try_cast_decimal(&max);
28
29        // Keep conversion failure distinct from a real zero bound. The derived
30        // Default remains the valid zero interval used by existing callers.
31        Self {
32            min: min.unwrap_or_default(),
33            max: max.unwrap_or_default(),
34            invalid_configuration: min.is_none() || max.is_none(),
35        }
36    }
37}
38
39impl<T: NumericValue> Normalizer<T> for Clamp {
40    fn normalize(&self, value: &mut T) -> Result<(), String> {
41        if self.invalid_configuration {
42            return Err("Clamp bounds cannot be represented as Decimal".to_string());
43        }
44        if self.min > self.max {
45            return Err(format!(
46                "Clamp requires min <= max (got {}..={})",
47                self.min, self.max
48            ));
49        }
50
51        let v = try_cast_decimal(value).ok_or_else(|| {
52            format!(
53                "value of type {} cannot be represented as Decimal",
54                type_name::<T>()
55            )
56        })?;
57
58        let clamped = if v < self.min {
59            self.min
60        } else if v > self.max {
61            self.max
62        } else {
63            v
64        };
65
66        *value = T::try_from_decimal(clamped).ok_or_else(|| {
67            format!(
68                "clamped value cannot be represented as {}",
69                type_name::<T>()
70            )
71        })?;
72
73        Ok(())
74    }
75}
76
77///
78/// RoundDecimalPlaces
79///
80
81#[normalizer]
82pub struct RoundDecimalPlaces {
83    scale: u32,
84    invalid_configuration: bool,
85}
86
87impl RoundDecimalPlaces {
88    /// Configure a scale; failed conversion makes normalization fail without mutation.
89    #[must_use]
90    pub fn new(scale: impl TryInto<u32>) -> Self {
91        let scale = scale.try_into().ok();
92        Self {
93            scale: scale.unwrap_or_default(),
94            invalid_configuration: scale.is_none(),
95        }
96    }
97}
98
99impl Normalizer<Decimal> for RoundDecimalPlaces {
100    fn normalize(&self, value: &mut Decimal) -> Result<(), String> {
101        if self.invalid_configuration {
102            return Err("decimal rounding scale cannot be represented as u32".to_string());
103        }
104        *value = value.round_dp(self.scale);
105
106        Ok(())
107    }
108}
109
110///
111/// TESTS
112///
113
114#[cfg(test)]
115mod tests;