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::from_json`] and [`tariff::from_json`] functions to check a [`json::Document`] against the schema for a given version.
10//! - Use [`tariff::lint`] to lint a tariff: flag the constructs a schema-valid tariff can still get wrong.
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::from_json(doc, Version::V211).into_parts();
25//!
26//! let Ok(report) = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam)
27//! else {
28//! return Err("The CDR could not be priced.".into());
29//! };
30//! let (report, warnings) = report.into_parts();
31//!
32//! if !warnings.is_empty() {
33//! eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
34//!
35//! for group in warnings {
36//! let (element, warnings) = group.to_parts();
37//! eprintln!(" {}", element.path);
38//!
39//! for warning in warnings {
40//! eprintln!(" - {warning}");
41//! }
42//! }
43//! }
44//!
45//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
46//! ```
47//!
48//! ## Price a CDR using tariff in separate JSON file
49//!
50//! If you have a CDR JSON with a tariff in a separate JSON file you can price the CDR with the
51//! following code:
52//!
53//! ```rust
54//! # use ocpi_tariffs::{cdr, json, price, tariff, warning, Version};
55//! #
56//! # const CDR_JSON: &str = include_str!("cdr.json");
57//! # const TARIFF_JSON: &str = include_str!("tariff.json");
58//!
59//! let cdr_doc = json::parse_object(CDR_JSON)?;
60//! let (cdr, _cdr_warnings) = cdr::from_json(cdr_doc, Version::V211).into_parts();
61//!
62//! let tariff_doc = json::parse_object(TARIFF_JSON)?;
63//! let (tariff, _tariff_warnings) = tariff::from_json(tariff_doc, Version::V211).into_parts();
64//!
65//! let source = price::TariffSource::Override(vec![tariff]);
66//! let Ok(report) = cdr::price(&cdr, source, chrono_tz::Tz::Europe__Amsterdam) else {
67//! return Err("The CDR could not be priced.".into());
68//! };
69//! let (report, warnings) = report.into_parts();
70//!
71//! if !warnings.is_empty() {
72//! eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
73//!
74//! for group in warnings {
75//! let (element, warnings) = group.to_parts();
76//! eprintln!(" {}", element.path);
77//!
78//! for warning in warnings {
79//! eprintln!(" - {warning}");
80//! }
81//! }
82//! }
83//!
84//! # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
85//! ```
86//!
87
88// Set here rather than in `[workspace.lints.rust]`: an example is its own crate and links every
89// dependency and dev-dependency of the package, so a workspace-wide setting reports each of the
90// dozen an example does not happen to use.
91#![warn(
92 unused_crate_dependencies,
93 reason = "a dependency nothing imports is still compiled, audited and imposed on consumers"
94)]
95// Also set here rather than in `[workspace.lints.rust]`, because this crate's public API is the
96// one that has consumers. A workspace-wide setting would also demand documentation of the `cli`
97// crate, whose public items exist only to back its binary, and of the build script.
98#![warn(missing_docs)]
99
100#[cfg(test)]
101mod test;
102
103#[cfg(test)]
104mod test_rust_decimal_arbitrary_precision;
105
106pub mod cdr;
107pub mod country;
108pub mod currency;
109pub mod datetime;
110pub mod duration;
111mod energy;
112pub mod explain;
113pub mod fix;
114pub mod generate;
115pub mod guess;
116pub mod json;
117pub mod lint;
118pub mod money;
119pub mod number;
120pub mod price;
121pub mod schema;
122pub mod string;
123pub mod tariff;
124pub mod timezone;
125pub mod warning;
126pub mod weekday;
127
128use std::fmt;
129
130#[doc(inline)]
131pub use duration::{ToDuration, ToHoursDecimal};
132#[doc(inline)]
133pub use energy::{Ampere, Kw, Kwh};
134#[doc(inline)]
135pub use explain::Language;
136#[doc(inline)]
137pub use money::{Cost, Money, Price, Vat};
138use schema::FromSchema;
139use warning::IntoCaveat;
140#[doc(inline)]
141pub use warning::{Caveat, Verdict, VerdictExt, Warning};
142use weekday::Weekday;
143
144/// The Id for a tariff used in the pricing of a CDR.
145pub type TariffId = String;
146
147/// The OCPI versions supported by this crate.
148#[derive(Clone, Copy, Debug, PartialEq)]
149pub enum Version {
150 /// OCPI version 2.2.1.
151 ///
152 /// See: <https://github.com/ocpi/ocpi/tree/release-2.2.1-bugfixes>.
153 V221,
154
155 /// OCPI version 2.1.1.
156 ///
157 /// See: <https://github.com/ocpi/ocpi/tree/release-2.1.1-bugfixes>.
158 V211,
159}
160
161impl Versioned for Version {
162 fn version(&self) -> Version {
163 *self
164 }
165}
166
167impl fmt::Display for Version {
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 match self {
170 Version::V221 => f.write_str("v221"),
171 Version::V211 => f.write_str("v211"),
172 }
173 }
174}
175
176/// An object for a specific OCPI [`Version`].
177pub trait Versioned: fmt::Debug {
178 /// Return the OCPI `Version` of this object.
179 fn version(&self) -> Version;
180}
181
182/// An object with an uncertain [`Version`].
183pub trait Unversioned: fmt::Debug {
184 /// The concrete [`Versioned`] type.
185 type Versioned: Versioned;
186
187 /// Forced an [`Unversioned`] object to be the given [`Version`].
188 ///
189 /// This does not change the structure of the OCPI object.
190 /// It simply relabels the object as a different OCPI Version.
191 ///
192 /// Use this with care.
193 fn force_into_versioned(self, version: Version) -> Self::Versioned;
194}
195
196/// Add two types together and saturate to max if the addition operation overflows.
197///
198/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
199trait SaturatingAdd {
200 /// Add two types together and saturate to max if the addition operation overflows.
201 #[must_use]
202 fn saturating_add(self, other: Self) -> Self;
203}
204
205/// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
206///
207/// This is private to the crate as `ocpi-tarifffs` does not want to provide numerical types for use by other crates.
208trait SaturatingSub {
209 /// Subtract two types from each other and saturate to zero if the subtraction operation overflows.
210 #[must_use]
211 fn saturating_sub(self, other: Self) -> Self;
212}
213
214/// A debug utility to `Display` an `Option<T>` as either `Display::fmt(T)` or the null set `∅`.
215struct DisplayOption<T>(Option<T>)
216where
217 T: fmt::Display;
218
219impl<T> fmt::Display for DisplayOption<T>
220where
221 T: fmt::Display,
222{
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 match &self.0 {
225 Some(v) => fmt::Display::fmt(v, f),
226 None => f.write_str("∅"),
227 }
228 }
229}