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}