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 valitate 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
13pub mod cdr;
14pub mod country;
15pub mod currency;
16pub mod datetime;
17pub mod duration;
18mod energy;
19pub mod guess;
20pub mod json;
21pub mod lint;
22pub mod money;
23pub mod number;
24pub mod price;
25pub mod string;
26pub mod tariff;
27pub mod timezone;
28mod v211;
29mod v221;
30pub mod warning;
31pub mod weekday;
32
33use std::{collections::BTreeSet, fmt};
34
35use duration::SecondsRound;
36use serde::{Deserialize, Deserializer};
37use warning::IntoCaveat;
38use weekday::Weekday;
39
40#[doc(inline)]
41pub use datetime::{Date, DateTime, Time};
42#[doc(inline)]
43pub use duration::HoursDecimal;
44#[doc(inline)]
45pub use energy::{Ampere, Kw, Kwh};
46#[doc(inline)]
47pub use money::{Cost, Money, Price, Vat, VatApplicable};
48#[doc(inline)]
49pub use number::Number;
50#[doc(inline)]
51pub use string::CiString;
52#[doc(inline)]
53pub use warning::{Caveat, Verdict, VerdictExt, Warning};
54
55/// Set of unexpected fields encountered while parsing a CDR or tariff.
56pub type UnexpectedFields = BTreeSet<String>;
57
58/// The Id for a tariff used in the pricing of a CDR.
59pub type TariffId = String;
60
61/// The OCPI versions supported by this crate
62#[derive(Clone, Copy, Debug, PartialEq)]
63pub enum Version {
64    V221,
65    V211,
66}
67
68impl Versioned for Version {
69    fn version(&self) -> Version {
70        *self
71    }
72}
73
74impl fmt::Display for Version {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Version::V221 => f.write_str("v221"),
78            Version::V211 => f.write_str("v211"),
79        }
80    }
81}
82
83/// An object for a specific OCPI [`Version`].
84pub trait Versioned: fmt::Debug {
85    /// Return the OCPI `Version` of this object.
86    fn version(&self) -> Version;
87}
88
89/// An object with an uncertain [`Version`].
90pub trait Unversioned: fmt::Debug {
91    /// The concrete [`Versioned`] type.
92    type Versioned: Versioned;
93
94    /// Forced an [`Unversioned`] object to be the given [`Version`].
95    ///
96    /// This does not change the structure of the OCPI object.
97    /// It simply relabels the object as a different OCPI Version.
98    ///
99    /// Use this with care.
100    fn force_into_versioned(self, version: Version) -> Self::Versioned;
101}
102
103/// Out of range error type used in various converting APIs
104#[derive(Clone, Copy, Hash, PartialEq, Eq)]
105pub struct OutOfRange(());
106
107impl std::error::Error for OutOfRange {}
108
109impl OutOfRange {
110    const fn new() -> OutOfRange {
111        OutOfRange(())
112    }
113}
114
115impl fmt::Display for OutOfRange {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "out of range")
118    }
119}
120
121impl fmt::Debug for OutOfRange {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "out of range")
124    }
125}
126
127/// Errors that can happen if a JSON str is parsed.
128pub struct ParseError {
129    /// The type of object we were trying to deserialize.
130    object: ObjectType,
131
132    /// The error that occurred while deserializing.
133    kind: ParseErrorKind,
134}
135
136/// The kind of Error that occurred.
137pub enum ParseErrorKind {
138    /// Some Error types are erased to abvoid leaking dependencies.
139    Erased(Box<dyn std::error::Error + Send + Sync + 'static>),
140
141    /// The integrated JSON parser was unable to parse a JSON str.
142    Json(json::Error),
143
144    /// The OCPI object should be a JSON object.
145    ShouldBeAnObject,
146}
147
148impl std::error::Error for ParseError {
149    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
150        match &self.kind {
151            ParseErrorKind::Erased(err) => Some(&**err),
152            ParseErrorKind::Json(err) => Some(err),
153            ParseErrorKind::ShouldBeAnObject => None,
154        }
155    }
156}
157
158impl fmt::Debug for ParseError {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        fmt::Display::fmt(self, f)
161    }
162}
163
164impl fmt::Display for ParseError {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(f, "while deserializing {:?}: ", self.object)?;
167
168        match &self.kind {
169            ParseErrorKind::Erased(err) => write!(f, "{err}"),
170            ParseErrorKind::Json(err) => write!(f, "{err}"),
171            ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
172        }
173    }
174}
175
176impl ParseError {
177    /// Create a [`ParseError`] from a generic std Error for a CDR object.
178    fn from_cdr_err(err: json::Error) -> Self {
179        Self {
180            object: ObjectType::Tariff,
181            kind: ParseErrorKind::Json(err),
182        }
183    }
184
185    /// Create a [`ParseError`] from a generic std Error for a tariff object.
186    fn from_tariff_err(err: json::Error) -> Self {
187        Self {
188            object: ObjectType::Tariff,
189            kind: ParseErrorKind::Json(err),
190        }
191    }
192
193    /// Create a [`ParseError`] from a generic std Error for a CDR object.
194    fn from_cdr_serde_err(err: serde_json::Error) -> Self {
195        Self {
196            object: ObjectType::Cdr,
197            kind: ParseErrorKind::Erased(err.into()),
198        }
199    }
200
201    /// Create a [`ParseError`] from a generic std Error for a tariff object.
202    fn from_tariff_serde_err(err: serde_json::Error) -> Self {
203        Self {
204            object: ObjectType::Tariff,
205            kind: ParseErrorKind::Erased(err.into()),
206        }
207    }
208
209    fn cdr_should_be_object() -> ParseError {
210        Self {
211            object: ObjectType::Cdr,
212            kind: ParseErrorKind::ShouldBeAnObject,
213        }
214    }
215
216    fn tariff_should_be_object() -> ParseError {
217        Self {
218            object: ObjectType::Tariff,
219            kind: ParseErrorKind::ShouldBeAnObject,
220        }
221    }
222
223    /// Deconstruct the error.
224    pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
225        (self.object, self.kind)
226    }
227}
228
229/// The type of OCPI objects that can be parsed.
230#[derive(Copy, Clone, Debug, Eq, PartialEq)]
231pub enum ObjectType {
232    /// An OCPI Charge Detail Record.
233    ///
234    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
235    Cdr,
236
237    /// An OCPI tariff.
238    ///
239    /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>)
240    Tariff,
241}
242
243fn null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
244where
245    T: Default + Deserialize<'de>,
246    D: Deserializer<'de>,
247{
248    let opt = Option::deserialize(deserializer)?;
249    Ok(opt.unwrap_or_default())
250}
251
252#[cfg(test)]
253mod test {
254    #![allow(
255        clippy::unwrap_in_result,
256        reason = "unwraps are allowed anywhere in tests"
257    )]
258
259    use std::{env, fmt, io::IsTerminal as _, path::Path, sync::Once};
260
261    use rust_decimal::Decimal;
262    use serde::{
263        de::{value::StrDeserializer, IntoDeserializer as _},
264        Deserialize,
265    };
266    use tracing::debug;
267    use tracing_subscriber::util::SubscriberInitExt as _;
268
269    use crate::{json, DateTime};
270
271    /// Creates and sets the default tracing subscriber if not already done.
272    #[track_caller]
273    pub fn setup() {
274        static INITIALIZED: Once = Once::new();
275
276        INITIALIZED.call_once_force(|state| {
277            if state.is_poisoned() {
278                return;
279            }
280
281            let is_tty = std::io::stderr().is_terminal();
282
283            let level = match env::var("RUST_LOG") {
284                Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
285                Err(err) => match err {
286                    env::VarError::NotPresent => tracing::Level::INFO,
287                    env::VarError::NotUnicode(_) => {
288                        panic!("`RUST_LOG` is not unicode");
289                    }
290                },
291            };
292
293            let subscriber = tracing_subscriber::fmt()
294                .with_ansi(is_tty)
295                .with_file(true)
296                .with_level(false)
297                .with_line_number(true)
298                .with_max_level(level)
299                .with_target(false)
300                .with_test_writer()
301                .without_time()
302                .finish();
303
304            subscriber
305                .try_init()
306                .expect("Init tracing_subscriber::Subscriber");
307        });
308    }
309
310    /// Gives access to the inner `Decimal` by reference.
311    pub trait AsDecimal {
312        fn as_dec(&self) -> &Decimal;
313    }
314
315    /// Compares two decimal objects with a tolerance
316    pub trait DecimalPartialEq<Rhs = Self> {
317        #[must_use]
318        fn eq_dec(&self, other: &Rhs) -> bool;
319    }
320
321    #[track_caller]
322    pub fn assert_no_unexpected_fields(unexpected_fields: &json::UnexpectedFields<'_>) {
323        if !unexpected_fields.is_empty() {
324            const MAX_FIELD_DISPLAY: usize = 20;
325
326            if unexpected_fields.len() > MAX_FIELD_DISPLAY {
327                let truncated_fields = unexpected_fields
328                    .iter()
329                    .take(MAX_FIELD_DISPLAY)
330                    .map(|path| path.to_string())
331                    .collect::<Vec<_>>();
332
333                panic!(
334                    "Unexpected fields found({}); displaying the first ({}): \n{}\n... and {} more",
335                    unexpected_fields.len(),
336                    truncated_fields.len(),
337                    truncated_fields.join(",\n"),
338                    unexpected_fields.len() - truncated_fields.len(),
339                )
340            } else {
341                panic!(
342                    "Unexpected fields found({}):\n{}",
343                    unexpected_fields.len(),
344                    unexpected_fields.to_strings().join(",\n")
345                )
346            };
347        }
348    }
349
350    #[track_caller]
351    pub fn assert_unexpected_fields(
352        unexpected_fields: &json::UnexpectedFields<'_>,
353        expected: &[&'static str],
354    ) {
355        if unexpected_fields.len() != expected.len() {
356            let unexpected_fields = unexpected_fields
357                .into_iter()
358                .map(|path| path.to_string())
359                .collect::<Vec<_>>();
360
361            panic!(
362                "The unexpected fields and expected fields lists have different lengths.\n\nUnexpected fields found:\n{}",
363                unexpected_fields.join(",\n")
364            );
365        }
366
367        let unmatched_paths = unexpected_fields
368            .into_iter()
369            .zip(expected.iter())
370            .filter(|(a, b)| a != *b)
371            .collect::<Vec<_>>();
372
373        if !unmatched_paths.is_empty() {
374            let unmatched_paths = unmatched_paths
375                .into_iter()
376                .map(|(a, b)| format!("{a} != {b}"))
377                .collect::<Vec<_>>();
378
379            panic!(
380                "The unexpected fields don't match the expected fields.\n\nUnexpected fields found:\n{}",
381                unmatched_paths.join(",\n")
382            );
383        }
384    }
385
386    /// A Field in the expect JSON.
387    ///
388    /// We need to distinguish between a field that's present and null and absent.
389    #[derive(Debug, Default)]
390    pub enum Expectation<T> {
391        /// The field is present.
392        Present(ExpectValue<T>),
393
394        /// The field is not preent.
395        #[default]
396        Absent,
397    }
398
399    /// The value of an expectation field.
400    #[derive(Debug)]
401    pub enum ExpectValue<T> {
402        /// The field has a value.
403        Some(T),
404
405        /// The field is set to `null`.
406        Null,
407    }
408
409    impl<T> ExpectValue<T>
410    where
411        T: fmt::Debug,
412    {
413        /// Convert the expectation into an `Option`.
414        pub fn into_option(self) -> Option<T> {
415            match self {
416                Self::Some(v) => Some(v),
417                Self::Null => None,
418            }
419        }
420
421        /// Consume the expectation and return the inner value of type `T`.
422        ///
423        /// # Panics
424        ///
425        /// Parnics if the `FieldValue` is `Null`.
426        #[track_caller]
427        pub fn expect_value(self) -> T {
428            match self {
429                ExpectValue::Some(v) => v,
430                ExpectValue::Null => panic!("the field expects a value"),
431            }
432        }
433    }
434
435    impl<'de, T> Deserialize<'de> for Expectation<T>
436    where
437        T: Deserialize<'de>,
438    {
439        #[expect(clippy::unwrap_in_result, reason = "This is test util code")]
440        #[track_caller]
441        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
442        where
443            D: serde::Deserializer<'de>,
444        {
445            let value = serde_json::Value::deserialize(deserializer)?;
446
447            if value.is_null() {
448                return Ok(Expectation::Present(ExpectValue::Null));
449            }
450
451            let v = T::deserialize(value).unwrap();
452            Ok(Expectation::Present(ExpectValue::Some(v)))
453        }
454    }
455
456    /// Create a `DateTime` from a RFC 3339 formatted string.
457    #[track_caller]
458    pub fn datetime_from_str(s: &str) -> DateTime {
459        let de: StrDeserializer<'_, serde::de::value::Error> = s.into_deserializer();
460        DateTime::deserialize(de).unwrap()
461    }
462
463    /// Try read an expectation JSON file based on the name of the given object JSON file.
464    ///
465    /// If the JSON object file is called `cdr.json` with a feature of `price` an expectation file
466    /// called `expect_cdr_price.json` is searched for.
467    #[track_caller]
468    pub fn read_expect_json(json_file_path: &Path, feature: &str) -> Option<String> {
469        let json_dir = json_file_path
470            .parent()
471            .expect("The given file should live in a dir");
472
473        let json_file_name = json_file_path
474            .file_stem()
475            .expect("The `json_file_path` should be a file")
476            .to_str()
477            .expect("The `json_file_path` should have a valid name");
478
479        // An underscore is prefixed to the filename to exclude the file from being included
480        // as input for a `test_each` glob driven test.
481        let expect_file_name = format!("output_{feature}__{json_file_name}.json");
482
483        debug!("Try to read expectation file: `{expect_file_name}`");
484
485        let s = std::fs::read_to_string(json_dir.join(&expect_file_name))
486            .ok()
487            .map(|mut s| {
488                json_strip_comments::strip(&mut s).ok();
489                s
490            });
491
492        debug!("Successfully Read expectation file: `{expect_file_name}`");
493        s
494    }
495}
496
497#[cfg(test)]
498mod test_rust_decimal_arbitrary_precision {
499    use rust_decimal_macros::dec;
500
501    use crate::Number;
502
503    #[test]
504    fn should_serialize_decimal_with_12_fraction_digits() {
505        let dec = dec!(0.123456789012);
506        let actual = serde_json::to_string(&dec).unwrap();
507        assert_eq!(actual, r#""0.123456789012""#.to_owned());
508    }
509
510    #[test]
511    fn should_serialize_decimal_with_8_fraction_digits() {
512        let dec = dec!(37.12345678);
513        let actual = serde_json::to_string(&dec).unwrap();
514        assert_eq!(actual, r#""37.12345678""#.to_owned());
515    }
516
517    #[test]
518    fn should_serialize_0_decimal_without_fraction_digits() {
519        let dec = dec!(0);
520        let actual = serde_json::to_string(&dec).unwrap();
521        assert_eq!(actual, r#""0""#.to_owned());
522    }
523
524    #[test]
525    fn should_serialize_12_num_with_4_fraction_digits() {
526        let num: Number = dec!(0.1234).try_into().unwrap();
527        let actual = serde_json::to_string(&num).unwrap();
528        assert_eq!(actual, r#""0.1234""#.to_owned());
529    }
530
531    #[test]
532    fn should_serialize_8_num_with_4_fraction_digits() {
533        let num: Number = dec!(37.1234).try_into().unwrap();
534        let actual = serde_json::to_string(&num).unwrap();
535        assert_eq!(actual, r#""37.1234""#.to_owned());
536    }
537
538    #[test]
539    fn should_serialize_0_num_without_fraction_digits() {
540        let num: Number = dec!(0).try_into().unwrap();
541        let actual = serde_json::to_string(&num).unwrap();
542        assert_eq!(actual, r#""0""#.to_owned());
543    }
544}