Skip to main content

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}