ocpi_tariffs/
lib.rs

1//! # OCPI Tariffs library
2//!
3//! Calculate the (sub)totals of a [charge session](https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc)
4//! using the [`cdr::price`] function and use the generated [`price::Report`] to review and compare the calculated
5//! totals versus the sources from the `CDR`.
6//!
7//! See the [`price::Report`] for a detailed list of all the fields that help analyze and validate the pricing of a `CDR`.
8//!
9//! - Use the [`cdr::parse`] and [`tariff::parse`] function to parse and guess which OCPI version of a CDR or tariff you have.
10//! - Use the [`cdr::parse_with_version`] and [`tariff::parse_with_version`] functions to parse a CDR of tariff as the given version.
11//! - Use the [`tariff::lint`] to lint a tariff: flag common errors, bugs, dangerous constructs and stylistic flaws in the tariff.
12//!
13//! # Examples
14//!
15//! ## Price a CDR with embedded tariff
16//!
17//! If you have a CDR JSON with an embedded tariff you can price the CDR with the following code:
18//!
19//! ```rust
20//! # use ocpi_tariffs::{cdr, price, Version};
21//! #
22//! # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
23//!
24//! let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
25//! let cdr::ParseReport {
26//!     cdr,
27//!     unexpected_fields,
28//! } = report;
29//!
30//! # if !unexpected_fields.is_empty() {
31//! #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
32//! #
33//! #     for path in &unexpected_fields {
34//! #         eprintln!("{path}");
35//! #     }
36//! # }
37//!
38//! let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam)?;
39//!
40//! if !report.warnings.is_empty() {
41//!     eprintln!("Pricing the CDR resulted in `{}` warnings", report.warnings.len());
42//!
43//!     for (elem_path, warnings) in report.warnings {
44//!         eprintln!("  {elem_path}");
45//!
46//!         for warning in warnings {
47//!             eprintln!("    - {warning}");
48//!         }
49//!     }
50//! }
51//!
52//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
53//! ```
54//!
55//! ## Price a CDR using tariff in separate JSON file
56//!
57//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
58//! following code:
59//!
60//! ```rust
61//! # use ocpi_tariffs::{cdr, price, tariff, Version};
62//! #
63//! # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/cdr.json");
64//! # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
65//!
66//! let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
67//! let cdr::ParseReport {
68//!     cdr,
69//!     unexpected_fields,
70//! } = report;
71//!
72//! # if !unexpected_fields.is_empty() {
73//! #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
74//! #
75//! #     for path in &unexpected_fields {
76//! #         eprintln!("{path}");
77//! #     }
78//! # }
79//!
80//! let tariff::ParseReport {
81//!     tariff,
82//!     unexpected_fields,
83//! } = tariff::parse_with_version(TARIFF_JSON, Version::V211).unwrap();
84//! let report = cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), chrono_tz::Tz::Europe__Amsterdam)?;
85//!
86//! if !report.warnings.is_empty() {
87//!     eprintln!("Pricing the CDR resulted in `{}` warnings", report.warnings.len());
88//!
89//!     for (elem_path, warnings) in report.warnings {
90//!         eprintln!("  {elem_path}");
91//!
92//!         for warning in warnings {
93//!             eprintln!("    - {warning}");
94//!         }
95//!     }
96//! }
97//!
98//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
99//! ```
100//!
101//! ## Lint a tariff
102//!
103//! ```rust
104//! # use ocpi_tariffs::{guess, tariff, warning};
105//! #
106//! # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
107//!
108//! let report = tariff::parse_and_report(TARIFF_JSON)?;
109//! let guess::Report {
110//!     unexpected_fields,
111//!     version,
112//! } = report;
113//!
114//! if !unexpected_fields.is_empty() {
115//!     eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
116//!
117//!     for path in &unexpected_fields {
118//!         eprintln!("  * {path}");
119//!     }
120//!
121//!     eprintln!();
122//! }
123//!
124//! let guess::Version::Certain(tariff) = version else {
125//!     return Err("Unable to guess the version of given CDR JSON.".into());
126//! };
127//!
128//! let report = tariff::lint(&tariff)?;
129//!
130//! eprintln!("`{}` lint warnings found", report.warnings.len());
131//!
132//! for warning::Group { element, warnings } in report.warnings.group_by_elem(tariff.as_element()) {
133//!     eprintln!(
134//!         "Warnings reported for `json::Element` at path: `{}`",
135//!         element.path()
136//!     );
137//!
138//!     for warning in warnings {
139//!         eprintln!("  * {warning}");
140//!     }
141//!
142//!     eprintln!();
143//! }
144//!
145//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
146//! ```
147
148pub mod cdr;
149pub mod country;
150pub mod currency;
151pub mod datetime;
152pub mod duration;
153mod energy;
154pub mod generate;
155pub mod guess;
156pub mod json;
157pub mod lint;
158pub mod money;
159pub mod number;
160pub mod price;
161pub mod string;
162pub mod tariff;
163pub mod timezone;
164mod v211;
165mod v221;
166pub mod warning;
167pub mod weekday;
168
169use std::{collections::BTreeSet, fmt};
170
171use warning::IntoCaveat;
172use weekday::Weekday;
173
174#[doc(inline)]
175pub use duration::{ToDuration, ToHoursDecimal};
176#[doc(inline)]
177pub use energy::{Ampere, Kw, Kwh};
178#[doc(inline)]
179pub use money::{Cost, Money, Price, Vat, VatApplicable};
180#[doc(inline)]
181pub use warning::{Caveat, Verdict, VerdictExt, Warning};
182
183/// Set of unexpected fields encountered while parsing a CDR or tariff.
184pub type UnexpectedFields = BTreeSet<String>;
185
186/// The Id for a tariff used in the pricing of a CDR.
187pub type TariffId = String;
188
189/// The OCPI versions supported by this crate
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub enum Version {
192    V221,
193    V211,
194}
195
196impl Versioned for Version {
197    fn version(&self) -> Version {
198        *self
199    }
200}
201
202impl fmt::Display for Version {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            Version::V221 => f.write_str("v221"),
206            Version::V211 => f.write_str("v211"),
207        }
208    }
209}
210
211/// An object for a specific OCPI [`Version`].
212pub trait Versioned: fmt::Debug {
213    /// Return the OCPI `Version` of this object.
214    fn version(&self) -> Version;
215}
216
217/// An object with an uncertain [`Version`].
218pub trait Unversioned: fmt::Debug {
219    /// The concrete [`Versioned`] type.
220    type Versioned: Versioned;
221
222    /// Forced an [`Unversioned`] object to be the given [`Version`].
223    ///
224    /// This does not change the structure of the OCPI object.
225    /// It simply relabels the object as a different OCPI Version.
226    ///
227    /// Use this with care.
228    fn force_into_versioned(self, version: Version) -> Self::Versioned;
229}
230
231/// Out of range error type used in various converting APIs
232#[derive(Clone, Copy, Hash, PartialEq, Eq)]
233pub struct OutOfRange(());
234
235impl std::error::Error for OutOfRange {}
236
237impl OutOfRange {
238    const fn new() -> OutOfRange {
239        OutOfRange(())
240    }
241}
242
243impl fmt::Display for OutOfRange {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "out of range")
246    }
247}
248
249impl fmt::Debug for OutOfRange {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        write!(f, "out of range")
252    }
253}
254
255/// Errors that can happen if a JSON str is parsed.
256pub struct ParseError {
257    /// The type of object we were trying to deserialize.
258    object: ObjectType,
259
260    /// The error that occurred while deserializing.
261    kind: ParseErrorKind,
262}
263
264/// The kind of Error that occurred.
265pub enum ParseErrorKind {
266    /// Some Error types are erased to avoid leaking dependencies.
267    Erased(Box<dyn std::error::Error + Send + Sync + 'static>),
268
269    /// The integrated JSON parser was unable to parse a JSON str.
270    Json(json::Error),
271
272    /// The OCPI object should be a JSON object.
273    ShouldBeAnObject,
274}
275
276impl std::error::Error for ParseError {
277    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
278        match &self.kind {
279            ParseErrorKind::Erased(err) => Some(&**err),
280            ParseErrorKind::Json(err) => Some(err),
281            ParseErrorKind::ShouldBeAnObject => None,
282        }
283    }
284}
285
286impl fmt::Debug for ParseError {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        fmt::Display::fmt(self, f)
289    }
290}
291
292impl fmt::Display for ParseError {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        write!(f, "while deserializing {:?}: ", self.object)?;
295
296        match &self.kind {
297            ParseErrorKind::Erased(err) => write!(f, "{err}"),
298            ParseErrorKind::Json(err) => write!(f, "{err}"),
299            ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
300        }
301    }
302}
303
304impl ParseError {
305    /// Create a [`ParseError`] from a generic std Error for a CDR object.
306    fn from_cdr_err(err: json::Error) -> Self {
307        Self {
308            object: ObjectType::Tariff,
309            kind: ParseErrorKind::Json(err),
310        }
311    }
312
313    /// Create a [`ParseError`] from a generic std Error for a tariff object.
314    fn from_tariff_err(err: json::Error) -> Self {
315        Self {
316            object: ObjectType::Tariff,
317            kind: ParseErrorKind::Json(err),
318        }
319    }
320
321    fn cdr_should_be_object() -> ParseError {
322        Self {
323            object: ObjectType::Cdr,
324            kind: ParseErrorKind::ShouldBeAnObject,
325        }
326    }
327
328    fn tariff_should_be_object() -> ParseError {
329        Self {
330            object: ObjectType::Tariff,
331            kind: ParseErrorKind::ShouldBeAnObject,
332        }
333    }
334
335    /// Deconstruct the error.
336    pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
337        (self.object, self.kind)
338    }
339}
340
341/// The type of OCPI objects that can be parsed.
342#[derive(Copy, Clone, Debug, Eq, PartialEq)]
343pub enum ObjectType {
344    /// An OCPI Charge Detail Record.
345    ///
346    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
347    Cdr,
348
349    /// An OCPI tariff.
350    ///
351    /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>)
352    Tariff,
353}
354
355/// Add two types together and saturate to max if the addition operation overflows.
356///
357/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
358pub(crate) trait SaturatingAdd {
359    /// Add two types together and saturate to max if the addition operation overflows.
360    #[must_use]
361    fn saturating_add(self, other: Self) -> Self;
362}
363
364/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
365///
366/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
367pub(crate) trait SaturatingSub {
368    /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
369    #[must_use]
370    fn saturating_sub(self, other: Self) -> Self;
371}
372
373/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
374pub(crate) struct DisplayOption<T>(Option<T>)
375where
376    T: fmt::Display;
377
378impl<T> fmt::Display for DisplayOption<T>
379where
380    T: fmt::Display,
381{
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        match &self.0 {
384            Some(v) => fmt::Display::fmt(v, f),
385            None => f.write_str("∅"),
386        }
387    }
388}
389
390/// A type used to deserialize a JSON string value into a structured Rust enum.
391///
392/// The deserialized value may not map to a `Known` variant in the enum and therefore be `Unknown`.
393/// The caller can then decide what to do with the `Unknown` variant.
394#[derive(Clone, Debug)]
395pub(crate) enum Enum<T> {
396    Known(T),
397    Unknown(String),
398}
399
400/// Create an `Enum<T>` from a `&str`.
401///
402/// This is used in conjunction with `FromJson`
403pub(crate) trait IntoEnum: Sized {
404    fn enum_from_str(s: &str) -> Enum<Self>;
405}
406
407impl<T> IntoCaveat for Enum<T>
408where
409    T: IntoCaveat + IntoEnum,
410{
411    fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
412        Caveat::new(self, warnings)
413    }
414}
415
416#[cfg(test)]
417mod test {
418    #![allow(
419        clippy::unwrap_in_result,
420        reason = "unwraps are allowed anywhere in tests"
421    )]
422
423    use std::{env, fmt, io::IsTerminal as _, path::Path, sync::Once};
424
425    use chrono::{DateTime, Utc};
426    use rust_decimal::Decimal;
427    use serde::{
428        de::{value::StrDeserializer, IntoDeserializer as _},
429        Deserialize,
430    };
431    use tracing::debug;
432    use tracing_subscriber::util::SubscriberInitExt as _;
433
434    use crate::{datetime, json, number};
435
436    /// Creates and sets the default tracing subscriber if not already done.
437    #[track_caller]
438    pub fn setup() {
439        static INITIALIZED: Once = Once::new();
440
441        INITIALIZED.call_once_force(|state| {
442            if state.is_poisoned() {
443                return;
444            }
445
446            let is_tty = std::io::stderr().is_terminal();
447
448            let level = match env::var("RUST_LOG") {
449                Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
450                Err(err) => match err {
451                    env::VarError::NotPresent => tracing::Level::INFO,
452                    env::VarError::NotUnicode(_) => {
453                        panic!("`RUST_LOG` is not unicode");
454                    }
455                },
456            };
457
458            let subscriber = tracing_subscriber::fmt()
459                .with_ansi(is_tty)
460                .with_file(true)
461                .with_level(false)
462                .with_line_number(true)
463                .with_max_level(level)
464                .with_target(false)
465                .with_test_writer()
466                .without_time()
467                .finish();
468
469            subscriber
470                .try_init()
471                .expect("Init tracing_subscriber::Subscriber");
472        });
473    }
474
475    /// Approximately compares two objects in tests.
476    ///
477    /// We need to approximately compare values in tests as we are not concerned with bitwise
478    /// accuracy. Only that the values are equal within an object specific tolerance.
479    ///
480    /// # Examples
481    ///
482    /// - A `Money` object considers an amount equal if there is only 2 cent difference.
483    /// - A `HoursDecimal` object considers a duration equal if there is only 3 second difference.
484    pub trait ApproxEq<Rhs = Self> {
485        #[must_use]
486        fn approx_eq(&self, other: &Rhs) -> bool;
487    }
488
489    impl<T> ApproxEq for Option<T>
490    where
491        T: ApproxEq,
492    {
493        fn approx_eq(&self, other: &Self) -> bool {
494            match (self, other) {
495                (Some(a), Some(b)) => a.approx_eq(b),
496                (None, None) => true,
497                _ => false,
498            }
499        }
500    }
501
502    /// Approximately compare two `Decimal` values.
503    pub fn approx_eq_dec(a: Decimal, mut b: Decimal, tolerance: Decimal, precision: u32) -> bool {
504        let a = a.round_dp(precision);
505        b.rescale(number::SCALE);
506        let b = b.round_dp(precision);
507        (a - b).abs() <= tolerance
508    }
509
510    #[track_caller]
511    pub fn assert_no_unexpected_fields(unexpected_fields: &json::UnexpectedFields<'_>) {
512        if !unexpected_fields.is_empty() {
513            const MAX_FIELD_DISPLAY: usize = 20;
514
515            if unexpected_fields.len() > MAX_FIELD_DISPLAY {
516                let truncated_fields = unexpected_fields
517                    .iter()
518                    .take(MAX_FIELD_DISPLAY)
519                    .map(|path| path.to_string())
520                    .collect::<Vec<_>>();
521
522                panic!(
523                    "Didn't expect `{}` unexpected fields;\n\
524                    displaying the first ({}):\n{}\n... and {} more",
525                    unexpected_fields.len(),
526                    truncated_fields.len(),
527                    truncated_fields.join(",\n"),
528                    unexpected_fields.len() - truncated_fields.len(),
529                )
530            } else {
531                panic!(
532                    "Didn't expect `{}` unexpected fields:\n{}",
533                    unexpected_fields.len(),
534                    unexpected_fields.to_strings().join(",\n")
535                )
536            };
537        }
538    }
539
540    /// A Field in the expect JSON.
541    ///
542    /// We need to distinguish between a field that's present and null and absent.
543    #[derive(Debug, Default)]
544    pub(crate) enum Expectation<T> {
545        /// The field is present.
546        Present(ExpectValue<T>),
547
548        /// The field is not present.
549        #[default]
550        Absent,
551    }
552
553    /// The value of an expectation field.
554    #[derive(Debug)]
555    pub(crate) enum ExpectValue<T> {
556        /// The field has a value.
557        Some(T),
558
559        /// The field is set to `null`.
560        Null,
561    }
562
563    impl<T> ExpectValue<T>
564    where
565        T: fmt::Debug,
566    {
567        /// Convert the expectation into an `Option`.
568        pub fn into_option(self) -> Option<T> {
569            match self {
570                Self::Some(v) => Some(v),
571                Self::Null => None,
572            }
573        }
574
575        /// Consume the expectation and return the inner value of type `T`.
576        ///
577        /// # Panics
578        ///
579        /// Panics if the `FieldValue` is `Null`.
580        #[track_caller]
581        pub fn expect_value(self) -> T {
582            match self {
583                ExpectValue::Some(v) => v,
584                ExpectValue::Null => panic!("the field expects a value"),
585            }
586        }
587    }
588
589    impl<'de, T> Deserialize<'de> for Expectation<T>
590    where
591        T: Deserialize<'de>,
592    {
593        #[expect(clippy::unwrap_in_result, reason = "This is test util code")]
594        #[track_caller]
595        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
596        where
597            D: serde::Deserializer<'de>,
598        {
599            let value = serde_json::Value::deserialize(deserializer)?;
600
601            if value.is_null() {
602                return Ok(Expectation::Present(ExpectValue::Null));
603            }
604
605            let v = T::deserialize(value).unwrap();
606            Ok(Expectation::Present(ExpectValue::Some(v)))
607        }
608    }
609
610    /// The content and name of an `expect` file.
611    ///
612    /// An `expect` file contains expectations for tests.
613    pub(crate) struct ExpectFile<T> {
614        // The value of the `expect` file.
615        //
616        // When the file is read from disk, the value will be a `String`.
617        // This `String` will then be parsed into structured data ready for use in a test.
618        pub value: Option<T>,
619
620        // The name of the `expect` file.
621        //
622        // This is written into panic messages.
623        pub expect_file_name: String,
624    }
625
626    impl ExpectFile<String> {
627        pub fn as_deref(&self) -> ExpectFile<&str> {
628            ExpectFile {
629                value: self.value.as_deref(),
630                expect_file_name: self.expect_file_name.clone(),
631            }
632        }
633    }
634
635    impl<T> ExpectFile<T> {
636        pub fn with_value(value: Option<T>, file_name: &str) -> Self {
637            Self {
638                value,
639                expect_file_name: file_name.to_owned(),
640            }
641        }
642
643        pub fn only_file_name(file_name: &str) -> Self {
644            Self {
645                value: None,
646                expect_file_name: file_name.to_owned(),
647            }
648        }
649    }
650
651    /// Create a `DateTime` from an RFC 3339 formatted string.
652    #[track_caller]
653    pub fn datetime_from_str(s: &str) -> DateTime<Utc> {
654        let de: StrDeserializer<'_, serde::de::value::Error> = s.into_deserializer();
655        datetime::test::deser_to_utc(de).unwrap()
656    }
657
658    /// Try to read an expectation JSON file based on the name of the given object JSON file.
659    ///
660    /// If the JSON object file is called `cdr.json` with a feature of `price` an expectation file
661    /// called `output_price__cdr.json` is searched for.
662    #[track_caller]
663    pub fn read_expect_json(json_file_path: &Path, feature: &str) -> ExpectFile<String> {
664        let json_dir = json_file_path
665            .parent()
666            .expect("The given file should live in a dir");
667
668        let json_file_name = json_file_path
669            .file_stem()
670            .expect("The `json_file_path` should be a file")
671            .to_str()
672            .expect("The `json_file_path` should have a valid name");
673
674        // An underscore is prefixed to the filename to exclude the file from being included
675        // as input for a `test_each` glob driven test.
676        let expect_file_name = format!("output_{feature}__{json_file_name}.json");
677
678        debug!("Try to read expectation file: `{expect_file_name}`");
679
680        let json = std::fs::read_to_string(json_dir.join(&expect_file_name))
681            .ok()
682            .map(|mut json| {
683                json_strip_comments::strip(&mut json).ok();
684                json
685            });
686
687        debug!("Successfully Read expectation file: `{expect_file_name}`");
688        ExpectFile {
689            value: json,
690            expect_file_name,
691        }
692    }
693
694    /// Parse the JSON from disk into structured data ready for use in a test.
695    ///
696    /// The input and output have an `ExpectFile` wrapper so the `expect_file_name` can
697    /// potentially be used in panic messages;
698    #[track_caller]
699    pub fn parse_expect_json<'de, T>(json: ExpectFile<&'de str>) -> ExpectFile<T>
700    where
701        T: Deserialize<'de>,
702    {
703        let ExpectFile {
704            value,
705            expect_file_name,
706        } = json;
707        let value = value.map(|json| {
708            serde_json::from_str(json)
709                .unwrap_or_else(|_| panic!("Unable to parse expect JSON `{expect_file_name}`"))
710        });
711        ExpectFile {
712            value,
713            expect_file_name: expect_file_name.clone(),
714        }
715    }
716
717    #[track_caller]
718    pub fn assert_approx_eq_failed(
719        left: &dyn fmt::Debug,
720        right: &dyn fmt::Debug,
721        args: Option<fmt::Arguments<'_>>,
722    ) -> ! {
723        match args {
724            Some(args) => panic!(
725                "assertion `left == right` failed: {args}
726left: {left:?}
727right: {right:?}"
728            ),
729            None => panic!(
730                "assertion `left == right` failed
731left: {left:?}
732right: {right:?}"
733            ),
734        }
735    }
736
737    /// This code is copied from the std lib `assert_eq!` and modified for use with `ApproxEq`.
738    #[macro_export]
739    macro_rules! assert_approx_eq {
740        ($left:expr, $right:expr $(,)?) => ({
741            use $crate::test::ApproxEq;
742
743            match (&$left, &$right) {
744                (left_val, right_val) => {
745                    if !((*left_val).approx_eq(&*right_val)) {
746                        // The reborrows below are intentional. Without them, the stack slot for the
747                        // borrow is initialized even before the values are compared, leading to a
748                        // noticeable slow down.
749                        $crate::test::assert_approx_eq_failed(
750                            &*left_val,
751                            &*right_val,
752                            std::option::Option::None
753                        );
754                    }
755                }
756            }
757        });
758        ($left:expr, $right:expr, $($arg:tt)+) => ({
759            use $crate::test::ApproxEq;
760
761            match (&$left, &$right) {
762                (left_val, right_val) => {
763                    if !((*left_val).approx_eq(&*right_val)) {
764                        // The reborrows below are intentional. Without them, the stack slot for the
765                        // borrow is initialized even before the values are compared, leading to a
766                        // noticeable slow down.
767                        $crate::test::assert_approx_eq_failed(
768                            &*left_val,
769                            &*right_val,
770                            std::option::Option::Some(std::format_args!($($arg)+))
771                        );
772                    }
773                }
774            }
775        });
776    }
777}
778
779#[cfg(test)]
780mod test_rust_decimal_arbitrary_precision {
781    use rust_decimal_macros::dec;
782
783    #[test]
784    fn should_serialize_decimal_with_12_fraction_digits() {
785        let dec = dec!(0.123456789012);
786        let actual = serde_json::to_string(&dec).unwrap();
787        assert_eq!(actual, r#""0.123456789012""#.to_owned());
788    }
789
790    #[test]
791    fn should_serialize_decimal_with_8_fraction_digits() {
792        let dec = dec!(37.12345678);
793        let actual = serde_json::to_string(&dec).unwrap();
794        assert_eq!(actual, r#""37.12345678""#.to_owned());
795    }
796
797    #[test]
798    fn should_serialize_0_decimal_without_fraction_digits() {
799        let dec = dec!(0);
800        let actual = serde_json::to_string(&dec).unwrap();
801        assert_eq!(actual, r#""0""#.to_owned());
802    }
803
804    #[test]
805    fn should_serialize_12_num_with_4_fraction_digits() {
806        let num = dec!(0.1234);
807        let actual = serde_json::to_string(&num).unwrap();
808        assert_eq!(actual, r#""0.1234""#.to_owned());
809    }
810
811    #[test]
812    fn should_serialize_8_num_with_4_fraction_digits() {
813        let num = dec!(37.1234);
814        let actual = serde_json::to_string(&num).unwrap();
815        assert_eq!(actual, r#""37.1234""#.to_owned());
816    }
817
818    #[test]
819    fn should_serialize_0_num_without_fraction_digits() {
820        let num = dec!(0);
821        let actual = serde_json::to_string(&num).unwrap();
822        assert_eq!(actual, r#""0""#.to_owned());
823    }
824}