Skip to main content

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//! - Use the [`cdr::parse`] and [`tariff::parse`] function to parse and guess which OCPI version of a CDR or tariff you have.
8//! - Use the [`cdr::parse_with_version`] and [`tariff::parse_with_version`] functions to parse a CDR of tariff as the given version.
9//! - Use the [`tariff::lint`] to lint a tariff: flag common errors, bugs, dangerous constructs and stylistic flaws in the tariff.
10//!
11//! # Examples
12//!
13//! ## Price a CDR with embedded tariff
14//!
15//! If you have a CDR JSON with an embedded tariff you can price the CDR with the following code:
16//!
17//! ```rust
18//! # use ocpi_tariffs::{cdr, price, warning, Version};
19//! #
20//! # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
21//!
22//! let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
23//! let cdr::ParseReport {
24//!     cdr,
25//!     unexpected_fields,
26//! } = report;
27//!
28//! # if !unexpected_fields.is_empty() {
29//! #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
30//! #
31//! #     for path in &unexpected_fields {
32//! #         eprintln!("{path}");
33//! #     }
34//! # }
35//!
36//! let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
37//! let (report, warnings) = report.into_parts();
38//!
39//! if !warnings.is_empty() {
40//!     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
41//!
42//!     for group in warnings {
43//!         let (element, warnings) = group.to_parts();
44//!         eprintln!("  {}", element.path());
45//!
46//!         for warning in warnings {
47//!             eprintln!("    - {warning}");
48//!         }
49//!     }
50//! }
51//!
52//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
53//! ```
54//!
55//! ## Price a CDR using tariff in separate JSON file
56//!
57//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
58//! following code:
59//!
60//! ```rust
61//! # use ocpi_tariffs::{cdr, price, tariff, warning, Version};
62//! #
63//! # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/cdr.json");
64//! # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
65//!
66//! let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
67//! let cdr::ParseReport {
68//!     cdr,
69//!     unexpected_fields,
70//! } = report;
71//!
72//! # if !unexpected_fields.is_empty() {
73//! #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
74//! #
75//! #     for path in &unexpected_fields {
76//! #         eprintln!("{path}");
77//! #     }
78//! # }
79//!
80//! let tariff::ParseReport {
81//!     tariff,
82//!     unexpected_fields,
83//! } = tariff::parse_with_version(TARIFF_JSON, Version::V211).unwrap();
84//! let report = cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), chrono_tz::Tz::Europe__Amsterdam).unwrap();
85//! let (report, warnings) = report.into_parts();
86//!
87//! if !warnings.is_empty() {
88//!     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
89//!
90//!     for group in warnings {
91//!         let (element, warnings) = group.to_parts();
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_warnings());
133//!
134//! for group in report.warnings {
135//!     let (element, warnings) = group.to_parts();
136//!     eprintln!(
137//!         "Warnings reported for `json::Element` at path: `{}`",
138//!         element.path()
139//!     );
140//!
141//!     for warning in warnings {
142//!         eprintln!("  * {warning}");
143//!     }
144//!
145//!     eprintln!();
146//! }
147//!
148//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
149//! ```
150
151#[cfg(test)]
152mod test;
153
154#[cfg(test)]
155mod test_rust_decimal_arbitrary_precision;
156
157pub mod cdr;
158pub mod country;
159pub mod currency;
160pub mod datetime;
161pub mod duration;
162mod energy;
163pub mod generate;
164pub mod guess;
165pub mod json;
166pub mod lint;
167pub mod money;
168pub mod number;
169pub mod price;
170pub mod string;
171pub mod tariff;
172pub mod timezone;
173mod v211;
174mod v221;
175pub mod warning;
176pub mod weekday;
177
178use std::{collections::BTreeSet, fmt};
179
180use warning::IntoCaveat;
181use weekday::Weekday;
182
183#[doc(inline)]
184pub use duration::{ToDuration, ToHoursDecimal};
185#[doc(inline)]
186pub use energy::{Ampere, Kw, Kwh};
187#[doc(inline)]
188pub use money::{Cost, Money, Price, Vat, VatApplicable};
189#[doc(inline)]
190pub use warning::{Caveat, Verdict, VerdictExt, Warning};
191
192/// Set of unexpected fields encountered while parsing a CDR or tariff.
193pub type UnexpectedFields = BTreeSet<String>;
194
195/// The Id for a tariff used in the pricing of a CDR.
196pub type TariffId = String;
197
198/// The OCPI versions supported by this crate
199#[derive(Clone, Copy, Debug, PartialEq)]
200pub enum Version {
201    V221,
202    V211,
203}
204
205impl Versioned for Version {
206    fn version(&self) -> Version {
207        *self
208    }
209}
210
211impl fmt::Display for Version {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        match self {
214            Version::V221 => f.write_str("v221"),
215            Version::V211 => f.write_str("v211"),
216        }
217    }
218}
219
220/// An object for a specific OCPI [`Version`].
221pub trait Versioned: fmt::Debug {
222    /// Return the OCPI `Version` of this object.
223    fn version(&self) -> Version;
224}
225
226/// An object with an uncertain [`Version`].
227pub trait Unversioned: fmt::Debug {
228    /// The concrete [`Versioned`] type.
229    type Versioned: Versioned;
230
231    /// Forced an [`Unversioned`] object to be the given [`Version`].
232    ///
233    /// This does not change the structure of the OCPI object.
234    /// It simply relabels the object as a different OCPI Version.
235    ///
236    /// Use this with care.
237    fn force_into_versioned(self, version: Version) -> Self::Versioned;
238}
239
240/// Out of range error type used in various converting APIs
241#[derive(Clone, Copy, Hash, PartialEq, Eq)]
242pub struct OutOfRange(());
243
244impl std::error::Error for OutOfRange {}
245
246impl OutOfRange {
247    const fn new() -> OutOfRange {
248        OutOfRange(())
249    }
250}
251
252impl fmt::Display for OutOfRange {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        write!(f, "out of range")
255    }
256}
257
258impl fmt::Debug for OutOfRange {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f, "out of range")
261    }
262}
263
264/// Errors that can happen if a JSON str is parsed.
265pub struct ParseError {
266    /// The type of object we were trying to deserialize.
267    object: ObjectType,
268
269    /// The error that occurred while deserializing.
270    kind: ParseErrorKind,
271}
272
273/// The kind of Error that occurred.
274#[derive(Debug)]
275pub enum ParseErrorKind {
276    /// Some Error types are erased to avoid leaking dependencies.
277    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
278
279    /// The integrated JSON parser was unable to parse a JSON str.
280    Json(json::Error),
281
282    /// The OCPI object should be a JSON object.
283    ShouldBeAnObject,
284}
285
286impl fmt::Display for ParseErrorKind {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        match self {
289            ParseErrorKind::Internal(_) => f.write_str("internal"),
290            ParseErrorKind::Json(error) => write!(f, "{error}"),
291            ParseErrorKind::ShouldBeAnObject => f.write_str("The element should be an object."),
292        }
293    }
294}
295
296impl Warning for ParseErrorKind {
297    fn id(&self) -> warning::Id {
298        match self {
299            ParseErrorKind::Internal(_) => warning::Id::from_static("internal"),
300            ParseErrorKind::Json(error) => error.id(),
301            ParseErrorKind::ShouldBeAnObject => warning::Id::from_static("should_be_an_object"),
302        }
303    }
304}
305
306impl std::error::Error for ParseError {
307    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
308        match &self.kind {
309            ParseErrorKind::Internal(err) => Some(&**err),
310            ParseErrorKind::Json(err) => Some(err),
311            ParseErrorKind::ShouldBeAnObject => None,
312        }
313    }
314}
315
316impl fmt::Debug for ParseError {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        fmt::Display::fmt(self, f)
319    }
320}
321
322impl fmt::Display for ParseError {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        write!(f, "while deserializing {:?}: ", self.object)?;
325
326        match &self.kind {
327            ParseErrorKind::Internal(err) => write!(f, "{err}"),
328            ParseErrorKind::Json(err) => write!(f, "{err}"),
329            ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
330        }
331    }
332}
333
334impl ParseError {
335    /// Create a [`ParseError`] from a generic std Error for a CDR object.
336    fn from_cdr_err(err: json::Error) -> Self {
337        Self {
338            object: ObjectType::Tariff,
339            kind: ParseErrorKind::Json(err),
340        }
341    }
342
343    /// Create a [`ParseError`] from a generic std Error for a tariff object.
344    fn from_tariff_err(err: json::Error) -> Self {
345        Self {
346            object: ObjectType::Tariff,
347            kind: ParseErrorKind::Json(err),
348        }
349    }
350
351    fn cdr_should_be_object() -> ParseError {
352        Self {
353            object: ObjectType::Cdr,
354            kind: ParseErrorKind::ShouldBeAnObject,
355        }
356    }
357
358    fn tariff_should_be_object() -> ParseError {
359        Self {
360            object: ObjectType::Tariff,
361            kind: ParseErrorKind::ShouldBeAnObject,
362        }
363    }
364
365    /// Deconstruct the error.
366    pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
367        (self.object, self.kind)
368    }
369}
370
371/// The type of OCPI objects that can be parsed.
372#[derive(Copy, Clone, Debug, Eq, PartialEq)]
373pub enum ObjectType {
374    /// An OCPI Charge Detail Record.
375    ///
376    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
377    Cdr,
378
379    /// An OCPI tariff.
380    ///
381    /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>)
382    Tariff,
383}
384
385impl fmt::Display for ObjectType {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        match self {
388            ObjectType::Cdr => f.write_str("CDR"),
389            ObjectType::Tariff => f.write_str("tariff"),
390        }
391    }
392}
393
394/// Add two types together and saturate to max if the addition operation overflows.
395///
396/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
397pub(crate) trait SaturatingAdd {
398    /// Add two types together and saturate to max if the addition operation overflows.
399    #[must_use]
400    fn saturating_add(self, other: Self) -> Self;
401}
402
403/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
404///
405/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
406pub(crate) trait SaturatingSub {
407    /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
408    #[must_use]
409    fn saturating_sub(self, other: Self) -> Self;
410}
411
412/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
413pub(crate) struct DisplayOption<T>(Option<T>)
414where
415    T: fmt::Display;
416
417impl<T> fmt::Display for DisplayOption<T>
418where
419    T: fmt::Display,
420{
421    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422        match &self.0 {
423            Some(v) => fmt::Display::fmt(v, f),
424            None => f.write_str("∅"),
425        }
426    }
427}
428
429/// A type used to deserialize a JSON string value into a structured Rust enum.
430///
431/// The deserialized value may not map to a `Known` variant in the enum and therefore be `Unknown`.
432/// The caller can then decide what to do with the `Unknown` variant.
433#[derive(Clone, Debug)]
434pub(crate) enum Enum<T> {
435    Known(T),
436    Unknown(String),
437}
438
439/// Create an `Enum<T>` from a `&str`.
440///
441/// This is used in conjunction with `FromJson`
442pub(crate) trait IntoEnum: Sized {
443    fn enum_from_str(s: &str) -> Enum<Self>;
444}
445
446impl<T> IntoCaveat for Enum<T>
447where
448    T: IntoCaveat + IntoEnum,
449{
450    fn into_caveat<W: Warning>(self, warnings: warning::Set<W>) -> Caveat<Self, W> {
451        Caveat::new(self, warnings)
452    }
453}