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 enumeration;
164pub mod generate;
165pub mod guess;
166pub mod json;
167pub mod lint;
168pub mod money;
169pub mod number;
170pub mod price;
171pub mod string;
172pub mod tariff;
173pub mod timezone;
174mod v211;
175mod v221;
176pub mod warning;
177pub mod weekday;
178
179use std::{collections::BTreeSet, fmt};
180
181use warning::IntoCaveat;
182use weekday::Weekday;
183
184#[doc(inline)]
185pub use duration::{ToDuration, ToHoursDecimal};
186
187#[doc(inline)]
188pub use energy::{Ampere, Kw, Kwh};
189
190#[doc(inline)]
191pub(crate) use enumeration::{Enum, IntoEnum};
192
193#[doc(inline)]
194pub use money::{Cost, Money, Price, Vat, VatApplicable};
195
196#[doc(inline)]
197pub use warning::{Caveat, Verdict, VerdictExt, Warning};
198
199/// Set of unexpected fields encountered while parsing a CDR or tariff.
200pub type UnexpectedFields = BTreeSet<String>;
201
202/// The Id for a tariff used in the pricing of a CDR.
203pub type TariffId = String;
204
205/// The OCPI versions supported by this crate
206#[derive(Clone, Copy, Debug, PartialEq)]
207pub enum Version {
208    V221,
209    V211,
210}
211
212impl Versioned for Version {
213    fn version(&self) -> Version {
214        *self
215    }
216}
217
218impl fmt::Display for Version {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Version::V221 => f.write_str("v221"),
222            Version::V211 => f.write_str("v211"),
223        }
224    }
225}
226
227/// An object for a specific OCPI [`Version`].
228pub trait Versioned: fmt::Debug {
229    /// Return the OCPI `Version` of this object.
230    fn version(&self) -> Version;
231}
232
233/// An object with an uncertain [`Version`].
234pub trait Unversioned: fmt::Debug {
235    /// The concrete [`Versioned`] type.
236    type Versioned: Versioned;
237
238    /// Forced an [`Unversioned`] object to be the given [`Version`].
239    ///
240    /// This does not change the structure of the OCPI object.
241    /// It simply relabels the object as a different OCPI Version.
242    ///
243    /// Use this with care.
244    fn force_into_versioned(self, version: Version) -> Self::Versioned;
245}
246
247/// Errors that can happen if a JSON str is parsed.
248pub struct ParseError {
249    /// The type of object we were trying to deserialize.
250    object: ObjectType,
251
252    /// The error that occurred while deserializing.
253    kind: ParseErrorKind,
254}
255
256/// The kind of Error that occurred.
257#[derive(Debug)]
258pub enum ParseErrorKind {
259    /// Some Error types are erased to avoid leaking dependencies.
260    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
261
262    /// The integrated JSON parser was unable to parse a JSON str.
263    Json(json::Error),
264
265    /// The OCPI object should be a JSON object.
266    ShouldBeAnObject,
267}
268
269impl fmt::Display for ParseErrorKind {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        match self {
272            ParseErrorKind::Internal(_) => f.write_str("internal"),
273            ParseErrorKind::Json(error) => write!(f, "{error}"),
274            ParseErrorKind::ShouldBeAnObject => f.write_str("The element should be an object."),
275        }
276    }
277}
278
279impl Warning for ParseErrorKind {
280    fn id(&self) -> warning::Id {
281        match self {
282            ParseErrorKind::Internal(_) => warning::Id::from_static("internal"),
283            ParseErrorKind::Json(error) => error.id(),
284            ParseErrorKind::ShouldBeAnObject => warning::Id::from_static("should_be_an_object"),
285        }
286    }
287}
288
289impl std::error::Error for ParseError {
290    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
291        match &self.kind {
292            ParseErrorKind::Internal(err) => Some(&**err),
293            ParseErrorKind::Json(err) => Some(err),
294            ParseErrorKind::ShouldBeAnObject => None,
295        }
296    }
297}
298
299impl fmt::Debug for ParseError {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        fmt::Display::fmt(self, f)
302    }
303}
304
305impl fmt::Display for ParseError {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        write!(f, "while deserializing {:?}: ", self.object)?;
308
309        match &self.kind {
310            ParseErrorKind::Internal(err) => write!(f, "{err}"),
311            ParseErrorKind::Json(err) => write!(f, "{err}"),
312            ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
313        }
314    }
315}
316
317impl ParseError {
318    /// Create a [`ParseError`] from a generic std Error for a CDR object.
319    fn from_cdr_err(err: json::Error) -> Self {
320        Self {
321            object: ObjectType::Tariff,
322            kind: ParseErrorKind::Json(err),
323        }
324    }
325
326    /// Create a [`ParseError`] from a generic std Error for a tariff object.
327    fn from_tariff_err(err: json::Error) -> Self {
328        Self {
329            object: ObjectType::Tariff,
330            kind: ParseErrorKind::Json(err),
331        }
332    }
333
334    fn cdr_should_be_object() -> ParseError {
335        Self {
336            object: ObjectType::Cdr,
337            kind: ParseErrorKind::ShouldBeAnObject,
338        }
339    }
340
341    fn tariff_should_be_object() -> ParseError {
342        Self {
343            object: ObjectType::Tariff,
344            kind: ParseErrorKind::ShouldBeAnObject,
345        }
346    }
347
348    /// Deconstruct the error.
349    pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
350        (self.object, self.kind)
351    }
352}
353
354/// The type of OCPI objects that can be parsed.
355#[derive(Copy, Clone, Debug, Eq, PartialEq)]
356pub enum ObjectType {
357    /// An OCPI Charge Detail Record.
358    ///
359    /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
360    Cdr,
361
362    /// An OCPI tariff.
363    ///
364    /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>)
365    Tariff,
366}
367
368impl fmt::Display for ObjectType {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        match self {
371            ObjectType::Cdr => f.write_str("CDR"),
372            ObjectType::Tariff => f.write_str("tariff"),
373        }
374    }
375}
376
377/// Add two types together and saturate to max if the addition operation overflows.
378///
379/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
380pub(crate) trait SaturatingAdd {
381    /// Add two types together and saturate to max if the addition operation overflows.
382    #[must_use]
383    fn saturating_add(self, other: Self) -> Self;
384}
385
386/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
387///
388/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
389pub(crate) trait SaturatingSub {
390    /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
391    #[must_use]
392    fn saturating_sub(self, other: Self) -> Self;
393}
394
395/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
396pub(crate) struct DisplayOption<T>(Option<T>)
397where
398    T: fmt::Display;
399
400impl<T> fmt::Display for DisplayOption<T>
401where
402    T: fmt::Display,
403{
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        match &self.0 {
406            Some(v) => fmt::Display::fmt(v, f),
407            None => f.write_str("∅"),
408        }
409    }
410}