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