ocpi_tariffs/
number.rs

1//! We represent the OCPI spec Number as a `Decimal` and serialize and deserialize to the precision defined in the OCPI spec.
2//!
3//! <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
4
5use std::{borrow::Cow, fmt};
6
7use rust_decimal::Decimal;
8
9use crate::{
10    into_caveat, json,
11    warning::{self, IntoCaveat},
12};
13
14/// The scale for numerical values as defined in the OCPI spec.
15///
16/// See: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
17pub const SCALE: u32 = 4;
18
19/// The warnings that can happen when parsing or linting a numerical value.
20#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
21pub enum WarningKind {
22    /// Numerical strings do not need escape codes.
23    ContainsEscapeCodes,
24
25    /// The value provided exceeds `Decimal::MAX`.
26    ExceedsMaximumPossibleValue,
27
28    /// The JSON value given is not a number.
29    InvalidType,
30
31    /// The value provided is less than `Decimal::MIN`.
32    LessThanMinimumPossibleValue,
33
34    /// An underflow is when there are more fractional digits than can be represented within `Decimal`.
35    Underflow,
36}
37
38impl fmt::Display for WarningKind {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            WarningKind::ContainsEscapeCodes => write!(
42                f,
43                "The value contains escape codes but it does not need them"
44            ),
45            WarningKind::InvalidType => write!(f, "The value should be a number."),
46            WarningKind::ExceedsMaximumPossibleValue => {
47                write!(
48                    f,
49                    "The value provided exceeds `79,228,162,514,264,337,593,543,950,335`."
50                )
51            }
52            WarningKind::LessThanMinimumPossibleValue => write!(
53                f,
54                "The value provided is less than `-79,228,162,514,264,337,593,543,950,335`."
55            ),
56            WarningKind::Underflow => write!(
57                f,
58                "An underflow is when there are more than 28 fractional digits"
59            ),
60        }
61    }
62}
63
64impl warning::Kind for WarningKind {
65    fn id(&self) -> Cow<'static, str> {
66        match self {
67            WarningKind::ContainsEscapeCodes => "contains_escape_codes".into(),
68            WarningKind::InvalidType => "invalid_type".into(),
69            WarningKind::ExceedsMaximumPossibleValue => "exceeds_maximum_possible_value".into(),
70            WarningKind::LessThanMinimumPossibleValue => "less_than_minimum_possible_value".into(),
71            WarningKind::Underflow => "underflow".into(),
72        }
73    }
74}
75
76into_caveat!(Decimal);
77
78impl json::FromJson<'_, '_> for Decimal {
79    type WarningKind = WarningKind;
80
81    fn from_json(elem: &json::Element<'_>) -> crate::Verdict<Self, Self::WarningKind> {
82        let mut warnings = warning::Set::new();
83        let value = elem.as_value();
84
85        let Some(s) = value.as_number() else {
86            warnings.with_elem(WarningKind::InvalidType, elem);
87            return Err(warnings);
88        };
89
90        let mut decimal = match Decimal::from_str_exact(s) {
91            Ok(v) => v,
92            Err(err) => {
93                let kind = match err {
94                    rust_decimal::Error::ExceedsMaximumPossibleValue => {
95                        WarningKind::ExceedsMaximumPossibleValue
96                    }
97                    rust_decimal::Error::LessThanMinimumPossibleValue => {
98                        WarningKind::LessThanMinimumPossibleValue
99                    }
100                    rust_decimal::Error::Underflow => WarningKind::Underflow,
101                    rust_decimal::Error::ConversionTo(_) => unreachable!("This is only triggered when converting to numerical types"),
102                    rust_decimal::Error::ErrorString(_) => unreachable!("rust_decimal docs state: This is a legacy/deprecated error type retained for backwards compatibility."),
103                    rust_decimal::Error::ScaleExceedsMaximumPrecision(_) => unreachable!("`Decimal::from_str_exact` uses a scale of zero")
104                };
105
106                warnings.with_elem(kind, elem);
107                return Err(warnings);
108            }
109        };
110
111        decimal.rescale(SCALE);
112        Ok(decimal.into_caveat(warnings))
113    }
114}
115
116/// Deserialize bytes into a `Decimal` applying the scale defined in the OCPI spec.
117///
118/// Called from the `impl Deserialize` for a `Decimal` newtype.
119pub(crate) fn deser_as_dec<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
120where
121    D: serde::Deserializer<'de>,
122{
123    use serde::Deserialize;
124
125    let mut d = <Decimal as Deserialize>::deserialize(deserializer)?;
126    d.rescale(SCALE);
127    Ok(d)
128}
129
130/// Called from the `impl Deserialize` for a `Decimal` newtype.
131pub(crate) fn deser_as_opt_dec<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>
132where
133    D: serde::Deserializer<'de>,
134{
135    use serde::Deserialize;
136
137    let d = Option::<Decimal>::deserialize(deserializer)?;
138    let d = d.map(|mut d| {
139        d.rescale(SCALE);
140        d
141    });
142    Ok(d)
143}
144
145/// Called from the `impl Serialize` for a `Decimal` newtype.
146pub(crate) fn ser_to_dec<S>(mut dec: Decimal, serializer: S) -> Result<S::Ok, S::Error>
147where
148    S: serde::Serializer,
149{
150    dec.rescale(SCALE);
151    dec.normalize_assign();
152
153    serde::Serialize::serialize(&dec, serializer)
154}
155
156/// Impl a `Decimal` based newtype.
157///
158/// All `Decimal` newtypes impl `serde::Serialize` and `serde::Deserialize` which apply the precision
159/// defined in the OCPI spec.
160///
161/// <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/types.asciidoc#14-number-type>
162#[doc(hidden)]
163#[macro_export]
164macro_rules! impl_dec_newtype {
165    ($kind:ident) => {
166        impl $kind {
167            #[allow(dead_code, reason = "auto generated")]
168            pub(crate) fn from_decimal(number: Decimal) -> Self {
169                Self(number)
170            }
171
172            #[must_use]
173            pub fn zero() -> Self {
174                use num_traits::Zero as _;
175
176                Self(Decimal::zero())
177            }
178
179            pub fn is_zero(&self) -> bool {
180                self.0.is_zero()
181            }
182
183            /// Round this number to the OCPI specified amount of decimals.
184            #[must_use]
185            pub fn rescale(mut self) -> Self {
186                self.0.rescale(number::SCALE);
187                Self(self.0)
188            }
189
190            #[must_use]
191            pub fn round_dp(self, digits: u32) -> Self {
192                Self(self.0.round_dp(digits))
193            }
194        }
195
196        impl std::fmt::Display for $kind {
197            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198                write!(f, "{:.4}", self.0)
199            }
200        }
201
202        /// The user can convert a `Decimal` newtype to a `Decimal` But cannot create
203        /// a `Decimal` newtype from a `Decimal`.
204        impl From<$kind> for rust_decimal::Decimal {
205            fn from(value: $kind) -> Self {
206                value.0
207            }
208        }
209
210        #[cfg(test)]
211        impl From<u64> for $kind {
212            fn from(value: u64) -> Self {
213                Self(value.into())
214            }
215        }
216
217        #[cfg(test)]
218        impl From<rust_decimal::Decimal> for $kind {
219            fn from(value: rust_decimal::Decimal) -> Self {
220                Self(value)
221            }
222        }
223
224        impl $crate::SaturatingAdd for $kind {
225            fn saturating_add(self, other: Self) -> Self {
226                Self(self.0.saturating_add(other.0))
227            }
228        }
229
230        impl $crate::SaturatingSub for $kind {
231            fn saturating_sub(self, other: Self) -> Self {
232                Self(self.0.saturating_sub(other.0))
233            }
234        }
235
236        impl $crate::json::FromJson<'_, '_> for $kind {
237            type WarningKind = $crate::number::WarningKind;
238
239            fn from_json(elem: &json::Element<'_>) -> $crate::Verdict<Self, Self::WarningKind> {
240                rust_decimal::Decimal::from_json(elem).map(|v| v.map(Self))
241            }
242        }
243
244        impl<'de> serde::Deserialize<'de> for $kind {
245            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
246            where
247                D: serde::Deserializer<'de>,
248            {
249                $crate::number::deser_as_dec(deserializer).map(Self)
250            }
251        }
252
253        impl serde::Serialize for $kind {
254            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255            where
256                S: serde::Serializer,
257            {
258                $crate::number::ser_to_dec(self.0, serializer)
259            }
260        }
261
262        #[cfg(test)]
263        impl $crate::test::ApproxEq for $kind {
264            fn approx_eq(&self, other: &Self) -> bool {
265                const TOLERANCE: Decimal = rust_decimal_macros::dec!(0.1);
266                const PRECISION: u32 = 2;
267
268                $crate::test::approx_eq_dec(self.0, other.0, TOLERANCE, PRECISION)
269            }
270        }
271    };
272}
273
274#[cfg(test)]
275mod test {
276    use rust_decimal::Decimal;
277    use rust_decimal_macros::dec;
278
279    use crate::test::{approx_eq_dec, ApproxEq};
280
281    impl ApproxEq for Decimal {
282        fn approx_eq(&self, other: &Self) -> bool {
283            const TOLERANCE: Decimal = dec!(0.1);
284            const PRECISION: u32 = 2;
285
286            approx_eq_dec(*self, *other, TOLERANCE, PRECISION)
287        }
288    }
289}