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}