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