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!("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!("cdr.json");
64//! # const TARIFF_JSON: &str = include_str!("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!("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_reasonable_str;
156
157#[cfg(test)]
158mod test_rust_decimal_arbitrary_precision;
159
160pub mod cdr;
161pub mod country;
162pub mod currency;
163pub mod datetime;
164pub mod duration;
165mod energy;
166pub mod enumeration;
167pub mod generate;
168pub mod guess;
169pub mod json;
170pub mod lint;
171pub mod money;
172pub mod number;
173pub mod price;
174pub mod string;
175pub mod tariff;
176pub mod timezone;
177mod v211;
178mod v221;
179pub mod warning;
180pub mod weekday;
181
182use std::{collections::BTreeSet, fmt};
183
184use warning::IntoCaveat;
185use weekday::Weekday;
186
187#[doc(inline)]
188pub use duration::{ToDuration, ToHoursDecimal};
189
190#[doc(inline)]
191pub use energy::{Ampere, Kw, Kwh};
192
193#[doc(inline)]
194use enumeration::{Enum, IntoEnum};
195
196#[doc(inline)]
197pub use money::{Cost, Money, Price, Vat};
198
199#[doc(inline)]
200pub use warning::{Caveat, Verdict, VerdictExt, Warning};
201
202/// Set of unexpected fields encountered while parsing a CDR or tariff.
203pub type UnexpectedFields = BTreeSet<String>;
204
205/// The Id for a tariff used in the pricing of a CDR.
206pub type TariffId = String;
207
208/// The OCPI versions supported by this crate.
209#[derive(Clone, Copy, Debug, PartialEq)]
210pub enum Version {
211 /// OCPI version 2.2.1.
212 ///
213 /// See: <https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes>.
214 V221,
215
216 /// OCPI version 2.1.1.
217 ///
218 /// See: <https://github.com/ocpi/ocpi/tree/release-2.1.1-bugfixes>.
219 V211,
220}
221
222impl Versioned for Version {
223 fn version(&self) -> Version {
224 *self
225 }
226}
227
228impl fmt::Display for Version {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 match self {
231 Version::V221 => f.write_str("v221"),
232 Version::V211 => f.write_str("v211"),
233 }
234 }
235}
236
237/// An object for a specific OCPI [`Version`].
238pub trait Versioned: fmt::Debug {
239 /// Return the OCPI `Version` of this object.
240 fn version(&self) -> Version;
241}
242
243/// An object with an uncertain [`Version`].
244pub trait Unversioned: fmt::Debug {
245 /// The concrete [`Versioned`] type.
246 type Versioned: Versioned;
247
248 /// Forced an [`Unversioned`] object to be the given [`Version`].
249 ///
250 /// This does not change the structure of the OCPI object.
251 /// It simply relabels the object as a different OCPI Version.
252 ///
253 /// Use this with care.
254 fn force_into_versioned(self, version: Version) -> Self::Versioned;
255}
256
257/// Errors that can happen if a JSON str is parsed.
258pub struct ParseError {
259 /// The type of object we were trying to deserialize.
260 object: ObjectType,
261
262 /// The error that occurred while deserializing.
263 kind: ParseErrorKind,
264}
265
266/// The kind of Error that occurred.
267#[derive(Debug)]
268pub enum ParseErrorKind {
269 /// Some Error types are erased to avoid leaking dependencies.
270 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
271
272 /// The integrated JSON parser was unable to parse a JSON str.
273 Json(json::Error),
274
275 /// The OCPI object should be a JSON object.
276 ShouldBeAnObject,
277
278 /// The size of the input `str` exceeds the maximum deemed reasonable.
279 SizeExceedsMax,
280}
281
282impl fmt::Display for ParseErrorKind {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 match self {
285 Self::Internal(_) => f.write_str("internal"),
286 Self::Json(error) => write!(f, "{error}"),
287 Self::ShouldBeAnObject => f.write_str("The element should be an object."),
288 Self::SizeExceedsMax => write!(
289 f,
290 "The input `&str` exceeds the reasonable maximum `{} MB`.",
291 ReasonableStr::FACTOR
292 ),
293 }
294 }
295}
296
297impl Warning for ParseErrorKind {
298 fn id(&self) -> warning::Id {
299 match self {
300 Self::Internal(_) => warning::Id::from_static("internal"),
301 Self::Json(error) => error.id(),
302 Self::ShouldBeAnObject => warning::Id::from_static("should_be_an_object"),
303 Self::SizeExceedsMax => warning::Id::from_static("size_exceeds_max"),
304 }
305 }
306}
307
308impl std::error::Error for ParseError {
309 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
310 match &self.kind {
311 ParseErrorKind::Internal(err) => Some(&**err),
312 ParseErrorKind::Json(err) => Some(err),
313 ParseErrorKind::ShouldBeAnObject | ParseErrorKind::SizeExceedsMax => None,
314 }
315 }
316}
317
318impl fmt::Debug for ParseError {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 fmt::Display::fmt(self, f)
321 }
322}
323
324impl fmt::Display for ParseError {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "while deserializing {:?}: ", self.object)?;
327
328 match &self.kind {
329 ParseErrorKind::Internal(err) => write!(f, "{err}"),
330 ParseErrorKind::Json(err) => write!(f, "{err}"),
331 ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
332 ParseErrorKind::SizeExceedsMax => write!(
333 f,
334 "The input `&str` exceeds the reasonable maximum `{} MB`.",
335 ReasonableStr::MAX_STR_INPUT_LEN
336 ),
337 }
338 }
339}
340
341impl ParseError {
342 /// Create a [`ParseError`] from a generic std Error for a CDR object.
343 fn from_cdr_err(err: json::Error) -> Self {
344 Self {
345 object: ObjectType::Cdr,
346 kind: ParseErrorKind::Json(err),
347 }
348 }
349
350 /// Create a [`ParseError`] from a generic std Error for a tariff object.
351 fn from_tariff_err(err: json::Error) -> Self {
352 Self {
353 object: ObjectType::Tariff,
354 kind: ParseErrorKind::Json(err),
355 }
356 }
357
358 /// Create a [`ParseError`] for a CDR that isn't a JSON object.
359 fn cdr_should_be_object() -> ParseError {
360 Self {
361 object: ObjectType::Cdr,
362 kind: ParseErrorKind::ShouldBeAnObject,
363 }
364 }
365
366 /// Create a [`ParseError`] for a CDR that isn't a JSON object.
367 fn tariff_should_be_object() -> ParseError {
368 Self {
369 object: ObjectType::Tariff,
370 kind: ParseErrorKind::ShouldBeAnObject,
371 }
372 }
373
374 /// Return a function that creates a [`ParseError`] from a kind.
375 fn from_kind(object: ObjectType) -> impl FnOnce(ParseErrorKind) -> Self {
376 move |kind| Self { object, kind }
377 }
378
379 /// Return the `ParseErrorKind`.
380 pub fn kind(&self) -> &ParseErrorKind {
381 &self.kind
382 }
383
384 /// Deconstruct the error into it's `ObjectType` and error kind.
385 pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
386 (self.object, self.kind)
387 }
388}
389
390/// A `str` that is checked for having a reasonable size.
391#[derive(Copy, Clone)]
392struct ReasonableStr<'buf>(&'buf str);
393
394impl<'buf> ReasonableStr<'buf> {
395 /// One million bytes of information.
396 const MEGA: usize = 1_000_000;
397
398 /// Limit the string to this many MB.
399 const FACTOR: usize = 5;
400
401 /// The maximum allowed size for a `str` given to a parse function.
402 ///
403 /// If the input `str` exceeds this size, a [`ParseErrorKind::SizeExceedsMax`] is returned.
404 ///
405 /// NOTE: Currently the largest tariff at `NLENE` is ~440 KB and the largest CDR is ~1.1 MB.
406 ///
407 /// NOTE: The motivation for a limit is to avoid parsing unseasonably large JSON objects
408 /// whether supplied through incompetence or maliciousness. Large JSON objects can be constructed
409 /// to have many warnings. This could bog down the function processing the JSON object.
410 const MAX_STR_INPUT_LEN: usize = Self::FACTOR * Self::MEGA;
411
412 /// Create new `ReasonableStr` object.
413 fn new(s: &'buf str) -> Result<ReasonableStr<'buf>, ParseErrorKind> {
414 if s.len() >= Self::MAX_STR_INPUT_LEN {
415 return Err(ParseErrorKind::SizeExceedsMax);
416 }
417
418 Ok(Self(s))
419 }
420
421 /// Unpack the contained `str`.
422 fn into_inner(self) -> &'buf str {
423 self.0
424 }
425}
426
427/// The type of OCPI objects that can be parsed.
428#[derive(Copy, Clone, Debug, Eq, PartialEq)]
429pub enum ObjectType {
430 /// An OCPI Charge Detail Record.
431 ///
432 /// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>).
433 Cdr,
434
435 /// An OCPI tariff.
436 ///
437 /// * See: [OCPI spec 2.2.1: Tariff](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>).
438 Tariff,
439}
440
441impl fmt::Display for ObjectType {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 match self {
444 ObjectType::Cdr => f.write_str("CDR"),
445 ObjectType::Tariff => f.write_str("tariff"),
446 }
447 }
448}
449
450/// Add two types together and saturate to max if the addition operation overflows.
451///
452/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
453trait SaturatingAdd {
454 /// Add two types together and saturate to max if the addition operation overflows.
455 #[must_use]
456 fn saturating_add(self, other: Self) -> Self;
457}
458
459/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
460///
461/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
462trait SaturatingSub {
463 /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
464 #[must_use]
465 fn saturating_sub(self, other: Self) -> Self;
466}
467
468/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
469struct DisplayOption<T>(Option<T>)
470where
471 T: fmt::Display;
472
473impl<T> fmt::Display for DisplayOption<T>
474where
475 T: fmt::Display,
476{
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 match &self.0 {
479 Some(v) => fmt::Display::fmt(v, f),
480 None => f.write_str("∅"),
481 }
482 }
483}