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}