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