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 [`json::parse_object`] to parse a CDR or tariff `&str` into a [`json::Document`].
8//! - Use [`cdr::infer_version`] or [`tariff::infer_version`] to guess which OCPI [`Version`] a CDR or tariff is.
9//! - Use the [`cdr::build`] and [`tariff::build`] functions to check a [`json::Document`] against the schema for a given version.
10//! - Use the [`tariff::lint`] to lint a tariff: flag common errors, bugs, dangerous constructs and stylistic flaws in the tariff.
11//!
12//! # Examples
13//!
14//! ## Price a CDR with embedded tariff
15//!
16//! If you have a CDR JSON with an embedded tariff you can price the CDR with the following code:
17//!
18//! ```rust
19//! # use ocpi_tariffs::{cdr, json, price, warning, Version};
20//! #
21//! # const CDR_JSON: &str = include_str!("cdr.json");
22//!
23//! let doc = json::parse_object(CDR_JSON)?;
24//! let (cdr, _warnings) = cdr::build(doc, Version::V211).into_parts();
25//!
26//! let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
27//! let (report, warnings) = report.into_parts();
28//!
29//! if !warnings.is_empty() {
30//! eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
31//!
32//! for group in warnings {
33//! let (element, warnings) = group.to_parts();
34//! eprintln!(" {}", element.path);
35//!
36//! for warning in warnings {
37//! eprintln!(" - {warning}");
38//! }
39//! }
40//! }
41//!
42//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
43//! ```
44//!
45//! ## Price a CDR using tariff in separate JSON file
46//!
47//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
48//! following code:
49//!
50//! ```rust
51//! # use ocpi_tariffs::{cdr, json, price, tariff, warning, Version};
52//! #
53//! # const CDR_JSON: &str = include_str!("cdr.json");
54//! # const TARIFF_JSON: &str = include_str!("tariff.json");
55//!
56//! let cdr_doc = json::parse_object(CDR_JSON)?;
57//! let (cdr, _cdr_warnings) = cdr::build(cdr_doc, Version::V211).into_parts();
58//!
59//! let tariff_doc = json::parse_object(TARIFF_JSON)?;
60//! let (tariff, _tariff_warnings) = tariff::build(tariff_doc, Version::V211).into_parts();
61//!
62//! let report = cdr::price(&cdr, price::TariffSource::Override(vec![tariff]), chrono_tz::Tz::Europe__Amsterdam).unwrap();
63//! let (report, warnings) = report.into_parts();
64//!
65//! if !warnings.is_empty() {
66//! eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
67//!
68//! for group in warnings {
69//! let (element, warnings) = group.to_parts();
70//! eprintln!(" {}", element.path);
71//!
72//! for warning in warnings {
73//! eprintln!(" - {warning}");
74//! }
75//! }
76//! }
77//!
78//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
79//! ```
80//!
81//! ## Lint a tariff
82//!
83//! ```rust
84//! # use ocpi_tariffs::{guess, json, tariff, warning};
85//! #
86//! # const TARIFF_JSON: &str = include_str!("tariff.json");
87//!
88//! let doc = json::parse_object(TARIFF_JSON)?;
89//! let guess::Version::Certain(tariff) = tariff::infer_version(doc) else {
90//! return Err("Unable to guess the version of given tariff JSON.".into());
91//! };
92//! let tariff = tariff::build_versioned(tariff).ignore_warnings();
93//!
94//! let report = tariff::lint(&tariff);
95//!
96//! eprintln!("`{}` lint warnings found", report.warnings.len_warnings());
97//!
98//! for group in report.warnings {
99//! let (element, warnings) = group.to_parts();
100//! eprintln!(
101//! "Warnings reported for `json::Element` at path: `{}`",
102//! element.path
103//! );
104//!
105//! for warning in warnings {
106//! eprintln!(" * {warning}");
107//! }
108//!
109//! eprintln!();
110//! }
111//!
112//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
113//! ```
114
115#[cfg(test)]
116mod test;
117
118#[cfg(test)]
119mod test_rust_decimal_arbitrary_precision;
120
121pub mod cdr;
122pub mod country;
123pub mod currency;
124pub mod datetime;
125pub mod duration;
126mod energy;
127pub mod enumeration;
128pub mod explain;
129pub mod generate;
130pub mod guess;
131pub mod json;
132pub mod lint;
133pub mod money;
134pub mod number;
135pub mod price;
136pub mod schema;
137pub mod string;
138pub mod tariff;
139pub mod timezone;
140pub mod warning;
141pub mod weekday;
142
143use std::fmt;
144
145#[doc(inline)]
146pub use duration::{ToDuration, ToHoursDecimal};
147#[doc(inline)]
148pub use energy::{Ampere, Kw, Kwh};
149#[doc(inline)]
150use enumeration::{Enum, IntoEnum};
151#[doc(inline)]
152pub use explain::Language;
153#[doc(inline)]
154pub use money::{Cost, Money, Price, Vat};
155#[doc(inline)]
156use schema::FromSchema;
157use warning::IntoCaveat;
158#[doc(inline)]
159pub use warning::{Caveat, Verdict, VerdictExt, Warning};
160use weekday::Weekday;
161
162/// The Id for a tariff used in the pricing of a CDR.
163pub type TariffId = String;
164
165/// The OCPI versions supported by this crate.
166#[derive(Clone, Copy, Debug, PartialEq)]
167pub enum Version {
168 /// OCPI version 2.2.1.
169 ///
170 /// See: <https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes>.
171 V221,
172
173 /// OCPI version 2.1.1.
174 ///
175 /// See: <https://github.com/ocpi/ocpi/tree/release-2.1.1-bugfixes>.
176 V211,
177}
178
179impl Versioned for Version {
180 fn version(&self) -> Version {
181 *self
182 }
183}
184
185impl fmt::Display for Version {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 Version::V221 => f.write_str("v221"),
189 Version::V211 => f.write_str("v211"),
190 }
191 }
192}
193
194/// An object for a specific OCPI [`Version`].
195pub trait Versioned: fmt::Debug {
196 /// Return the OCPI `Version` of this object.
197 fn version(&self) -> Version;
198}
199
200/// An object with an uncertain [`Version`].
201pub trait Unversioned: fmt::Debug {
202 /// The concrete [`Versioned`] type.
203 type Versioned: Versioned;
204
205 /// Forced an [`Unversioned`] object to be the given [`Version`].
206 ///
207 /// This does not change the structure of the OCPI object.
208 /// It simply relabels the object as a different OCPI Version.
209 ///
210 /// Use this with care.
211 fn force_into_versioned(self, version: Version) -> Self::Versioned;
212}
213
214/// Add two types together and saturate to max if the addition operation overflows.
215///
216/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
217trait SaturatingAdd {
218 /// Add two types together and saturate to max if the addition operation overflows.
219 #[must_use]
220 fn saturating_add(self, other: Self) -> Self;
221}
222
223/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
224///
225/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
226trait SaturatingSub {
227 /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
228 #[must_use]
229 fn saturating_sub(self, other: Self) -> Self;
230}
231
232/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
233struct DisplayOption<T>(Option<T>)
234where
235 T: fmt::Display;
236
237impl<T> fmt::Display for DisplayOption<T>
238where
239 T: fmt::Display,
240{
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match &self.0 {
243 Some(v) => fmt::Display::fmt(v, f),
244 None => f.write_str("∅"),
245 }
246 }
247}