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}