Skip to main content

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