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