ocpi_tariffs/cdr.rs
1//! Parse a CDR and price the result with a tariff.
2
3#[cfg(test)]
4mod test_every_field_set;
5
6use std::fmt;
7
8use chrono_tz::Tz;
9
10use crate::{
11 generate, guess,
12 json::{self},
13 price, schema, tariff,
14 warning::{Caveat, IntoCaveat as _},
15 Verdict,
16};
17
18/// Infer which OCPI [`Version`] a CDR [`json::Document`] is, without validating it.
19///
20/// Use this when the version of the CDR is not known up front. The [`json::Document`] is obtained
21/// by calling [`json::parse_object`]. The returned [`guess::CdrVersion`] is either
22/// [`Certain`](guess::Version::Certain) or [`Uncertain`](guess::Version::Uncertain) about the version.
23///
24/// To check the CDR against the OCPI schema for a known [`Version`], use [`build`].
25///
26/// # Example
27///
28/// ```rust
29/// # use ocpi_tariffs::{cdr, json, Version};
30/// #
31/// # const CDR_JSON: &str = include_str!("cdr.json");
32///
33/// let doc = json::parse_object(CDR_JSON)?;
34/// let cdr = cdr::infer_version(doc).certain_or(Version::V211);
35///
36/// # Ok::<(), json::ParseError>(())
37/// ```
38pub fn infer_version(json: json::Document<'_>) -> guess::CdrVersion<'_> {
39 guess::cdr_version(json)
40}
41
42/// Validate a [`json::Document`] against the OCPI CDR schema for the given [`Version`][^spec-v211][^spec-v221].
43///
44/// The [`json::Document`] is obtained by calling [`json::parse_object`]. Any unexpected, missing,
45/// or wrongly typed fields are reported as a [`warning::Set`](crate::warning::Set) of
46/// [`schema::Warning`]s carried by the returned [`Caveat`].
47///
48/// # Example
49///
50/// ```rust
51/// # use ocpi_tariffs::{cdr, json, Version};
52/// #
53/// # const CDR_JSON: &str = include_str!("cdr.json");
54///
55/// let doc = json::parse_object(CDR_JSON)?;
56/// let (cdr, warnings) = cdr::build(doc, Version::V211).into_parts();
57///
58/// if !warnings.is_empty() {
59/// eprintln!("The CDR has `{}` schema warnings.", warnings.len_warnings());
60/// }
61///
62/// # Ok::<(), json::ParseError>(())
63/// ```
64///
65/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>.
66/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>.
67pub fn build(
68 json: json::Document<'_>,
69 version: crate::Version,
70) -> Caveat<Versioned<'_>, schema::Warning> {
71 let (version, warnings) = match version {
72 crate::Version::V221 => {
73 let (cdr, warnings) = schema::v221::build_cdr(&json).into_parts();
74 (Version::V221(cdr), warnings)
75 }
76 crate::Version::V211 => {
77 let (cdr, warnings) = schema::v211::build_cdr(&json).into_parts();
78 (Version::V211(cdr), warnings)
79 }
80 };
81 let versioned = Versioned { doc: json, version };
82 versioned.into_caveat(warnings)
83}
84
85/// Validate a [`VersionedJson`] against the OCPI CDR schema for its known [`Version`].
86///
87/// Use this when the [`Version`] has already been resolved - for example a
88/// [`VersionedJson`] obtained from [`infer_version`] via [`certain_or`](guess::Version::certain_or).
89pub fn build_versioned(json: VersionedJson<'_>) -> Caveat<Versioned<'_>, schema::Warning> {
90 let VersionedJson { doc, version } = json;
91 build(doc, version)
92}
93
94/// Generate a [`PartialCdr`](generate::PartialCdr) that can be priced by the given tariff.
95///
96/// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
97/// does not know anything about the EVSE location or the token used to authenticate the chargesession.
98///
99/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
100pub fn generate_from_tariff(
101 tariff: &tariff::Versioned<'_>,
102 config: &generate::Config,
103) -> Verdict<generate::Report, generate::Warning> {
104 generate::cdr_from_tariff(tariff, config)
105}
106
107/// Price a single `CDR` and return a [`Report`](price::Report).
108///
109/// The `CDR` is checked for internal consistency before being priced. As pricing a `CDR` with
110/// contradictory data will lead to a difficult to debug [`Report`](price::Report).
111/// An [`Error`](price::Warning) is returned if the `CDR` is deemed to be internally inconsistent.
112///
113/// > **_Note_** Pricing the CDR does not require a spec compliant CDR or tariff.
114/// > A best effort is made to parse the given CDR and tariff JSON.
115///
116/// The [`Report`](price::Report) contains the charge session priced according to the specified
117/// tariff and a selection of fields from the source `CDR` that can be used for comparing the
118/// source `CDR` totals with the calculated totals. The [`Report`](price::Report) also contains
119/// a list of unknown fields to help spot misspelled fields.
120///
121/// The source of the tariffs can be controlled using the [`TariffSource`](price::TariffSource).
122/// The timezone can be found or inferred using the [`timezone::find_or_infer`](crate::timezone::find_or_infer) function.
123///
124/// # Example
125///
126/// ```rust
127/// # use ocpi_tariffs::{cdr, json, price, warning, Version};
128/// #
129/// # const CDR_JSON: &str = include_str!("cdr.json");
130///
131/// let doc = json::parse_object(CDR_JSON)?;
132/// let (cdr, _warnings) = cdr::build(doc, Version::V211).into_parts();
133///
134/// let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
135/// let (report, warnings) = report.into_parts();
136///
137/// if !warnings.is_empty() {
138/// eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
139///
140/// for group in warnings {
141/// let (element, warnings) = group.to_parts();
142/// eprintln!(" {}", element.path);
143///
144/// for warning in warnings {
145/// eprintln!(" - {warning}");
146/// }
147/// }
148/// }
149///
150/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
151/// ```
152pub fn price(
153 cdr: &Versioned<'_>,
154 tariff_source: price::TariffSource<'_>,
155 timezone: Tz,
156) -> Verdict<price::Report, price::Warning> {
157 price::cdr(cdr, tariff_source, timezone)
158}
159
160/// A `json::Element` that has been processed by either the [`infer_version`] or [`build`]
161/// functions and has been identified as being a certain [`Version`].
162#[derive(Clone)]
163pub struct Versioned<'buf> {
164 /// The parsed JSON.
165 doc: json::Document<'buf>,
166
167 /// The `Version` of the tariff, determined during parsing.
168 version: Version<'buf>,
169}
170
171impl fmt::Debug for Versioned<'_> {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 if f.alternate() {
174 match &self.version {
175 Version::V211(cdr) => fmt::Debug::fmt(&cdr, f),
176 Version::V221(cdr) => fmt::Debug::fmt(&cdr, f),
177 }
178 } else {
179 match &self.version {
180 Version::V211(_) => f.write_str("V211"),
181 Version::V221(_) => f.write_str("V221"),
182 }
183 }
184 }
185}
186
187impl crate::Versioned for Versioned<'_> {
188 fn version(&self) -> crate::Version {
189 match self.version {
190 Version::V211(_) => crate::Version::V211,
191 Version::V221(_) => crate::Version::V221,
192 }
193 }
194}
195
196impl<'buf> Versioned<'buf> {
197 /// Return the inner [`json::Document`] and discard the version info.
198 pub fn into_doc(self) -> json::Document<'buf> {
199 self.doc
200 }
201
202 /// Return the inner [`json::Element`] and discard the version info.
203 pub fn as_element(&self) -> &json::Element<'buf> {
204 self.doc.root()
205 }
206
207 /// Return the inner [`json::Document`] and discard the version info.
208 pub fn as_doc(&self) -> &json::Document<'buf> {
209 &self.doc
210 }
211
212 /// Return the inner JSON `str` and discard the version info.
213 pub fn as_json_str(&self) -> &'buf str {
214 self.doc.source()
215 }
216}
217
218#[expect(
219 clippy::large_enum_variant,
220 reason = "the v2.1.1 and v2.2.1 CDR IRs differ in size; this short-lived versioned \
221 value is not worth boxing"
222)]
223#[derive(Clone)]
224enum Version<'buf> {
225 V211(schema::v211::Cdr<'buf>),
226 V221(schema::v221::Cdr<'buf>),
227}
228
229/// A `json::Document` that has been processed by [`infer_version`] and has been identified
230/// as being a concrete [`Version`].
231pub struct VersionedJson<'buf> {
232 /// The parsed JSON.
233 doc: json::Document<'buf>,
234
235 /// The `Version` of the CDR, determined during parsing.
236 version: crate::Version,
237}
238
239impl fmt::Debug for VersionedJson<'_> {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 if f.alternate() {
242 fmt::Debug::fmt(&self.doc, f)
243 } else {
244 match self.version {
245 crate::Version::V211 => f.write_str("V211"),
246 crate::Version::V221 => f.write_str("V221"),
247 }
248 }
249 }
250}
251
252impl crate::Versioned for VersionedJson<'_> {
253 fn version(&self) -> crate::Version {
254 self.version
255 }
256}
257
258impl<'buf> VersionedJson<'buf> {
259 /// Create a new `Versioned` object.
260 pub(crate) fn new(element: json::Document<'buf>, version: crate::Version) -> Self {
261 Self {
262 doc: element,
263 version,
264 }
265 }
266
267 /// Return the inner [`json::Document`] and discard the version info.
268 pub fn into_doc(self) -> json::Document<'buf> {
269 self.doc
270 }
271
272 /// Return the inner [`json::Element`] and discard the version info.
273 pub fn as_element(&self) -> &json::Element<'buf> {
274 self.doc.root()
275 }
276
277 /// Return a reference to the inner [`json::Document`].
278 pub fn as_doc(&self) -> &json::Document<'buf> {
279 &self.doc
280 }
281
282 /// Return the inner JSON `str` and discard the version info.
283 pub fn as_json_str(&self) -> &'buf str {
284 self.doc.source()
285 }
286}
287
288/// A `json::Document` that has been processed by [`infer_version`] and has been identified
289/// as being a concrete [`Version`].
290#[derive(Debug)]
291pub struct Unversioned<'buf> {
292 /// The root `Element` of the parsed source.
293 doc: json::Document<'buf>,
294}
295
296impl<'buf> Unversioned<'buf> {
297 /// Create an unversioned [`json::Element`].
298 pub(crate) fn new(doc: json::Document<'buf>) -> Self {
299 Self { doc }
300 }
301
302 /// Return the inner [`json::Element`] and discard the version info.
303 pub fn into_doc(self) -> json::Document<'buf> {
304 self.doc
305 }
306
307 /// Return the inner [`json::Element`] and discard the version info.
308 pub fn as_element(&self) -> &json::Element<'buf> {
309 self.doc.root()
310 }
311}
312
313impl<'buf> crate::Unversioned for Unversioned<'buf> {
314 type Versioned = VersionedJson<'buf>;
315
316 fn force_into_versioned(self, version: crate::Version) -> VersionedJson<'buf> {
317 let Self { doc } = self;
318 VersionedJson { doc, version }
319 }
320}