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, json, price, tariff, ObjectType, ParseError, ReasonableStr, Verdict, Version,
12};
13
14/// Parse a `&str` into a [`Versioned`] CDR using a schema for the given [`Version`][^spec-v211][^spec-v221] to check for
15/// any unexpected fields.
16///
17/// # Example
18///
19/// ```rust
20/// # use ocpi_tariffs::{cdr, Version, ParseError};
21/// #
22/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
23///
24/// let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
25/// let cdr::ParseReport {
26///     cdr,
27///     unexpected_fields,
28/// } = report;
29///
30/// if !unexpected_fields.is_empty() {
31///     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
32///
33///     for path in &unexpected_fields {
34///         eprintln!("{path}");
35///     }
36/// }
37///
38/// # Ok::<(), ParseError>(())
39/// ```
40///
41/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>
42/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>
43pub fn parse_with_version(cdr_json: &str, version: Version) -> Result<ParseReport<'_>, ParseError> {
44    let cdr_json = ReasonableStr::new(cdr_json).map_err(ParseError::from_kind(ObjectType::Cdr))?;
45    match version {
46        Version::V221 => {
47            let schema = &*crate::v221::CDR_SCHEMA;
48            let report =
49                json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
50            let json::ParseReport {
51                element,
52                unexpected_fields,
53            } = report;
54            Ok(ParseReport {
55                cdr: Versioned {
56                    source: cdr_json.into_inner(),
57                    element,
58                    version: Version::V221,
59                },
60                unexpected_fields,
61            })
62        }
63        Version::V211 => {
64            let schema = &*crate::v211::CDR_SCHEMA;
65            let report =
66                json::parse_with_schema(cdr_json, schema).map_err(ParseError::from_cdr_err)?;
67            let json::ParseReport {
68                element,
69                unexpected_fields,
70            } = report;
71            Ok(ParseReport {
72                cdr: Versioned {
73                    source: cdr_json.into_inner(),
74                    element,
75                    version,
76                },
77                unexpected_fields,
78            })
79        }
80    }
81}
82
83/// A report of calling [`parse_with_version`].  
84#[derive(Debug)]
85pub struct ParseReport<'buf> {
86    /// The root JSON [`Element`](json::Element).
87    pub cdr: Versioned<'buf>,
88
89    /// A list of fields that were not expected: The schema did not define them.
90    pub unexpected_fields: json::UnexpectedFields<'buf>,
91}
92
93/// Parse the JSON and try to guess the [`Version`] based on fields defined in the OCPI v2.1.1[^spec-v211] and v2.2.1[^spec-v221] CDR spec.
94///
95/// The parser is forgiving and will not complain if the CDR JSON is missing required fields.
96/// The parser will also not complain if unexpected fields are present in the JSON.
97/// The [`Version`] guess is based on fields that exist.
98///
99/// # Example
100///
101/// ```rust
102/// # use ocpi_tariffs::{cdr, guess, ParseError, Version, Versioned as _};
103/// #
104/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
105/// let cdr = cdr::parse(CDR_JSON)?;
106///
107/// match cdr {
108///     guess::Version::Certain(cdr) => {
109///         println!("The CDR version is `{}`", cdr.version());
110///     },
111///     guess::Version::Uncertain(_cdr) => {
112///         eprintln!("Unable to guess the version of given CDR JSON.");
113///     }
114/// }
115///
116/// # Ok::<(), ParseError>(())
117/// ```
118///
119/// [^ocpi-spec-v211-cdr]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>
120/// [^ocpi-spec-v221-cdr]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>
121pub fn parse(cdr_json: &str) -> Result<guess::CdrVersion<'_>, ParseError> {
122    let cdr_json = ReasonableStr::new(cdr_json).map_err(ParseError::from_kind(ObjectType::Cdr))?;
123    guess::cdr_version(cdr_json)
124}
125
126/// Guess the [`Version`][^spec-v211][^spec-v221] of the given CDR JSON and report on any unexpected fields.
127///
128/// The parser is forgiving and will not complain if the CDR JSON is missing required fields.
129/// The parser will also not complain if unexpected fields are present in the JSON.
130/// The [`Version`] guess is based on fields that exist.
131///
132/// # Example
133///
134/// ```rust
135/// # use ocpi_tariffs::{cdr, guess, ParseError, Version, Versioned};
136/// #
137/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
138///
139/// let report = cdr::parse_and_report(CDR_JSON)?;
140/// let guess::CdrReport {
141///     version,
142///     unexpected_fields,
143/// } = report;
144///
145/// if !unexpected_fields.is_empty() {
146///     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
147///
148///     for path in &unexpected_fields {
149///         eprintln!("{path}");
150///     }
151/// }
152///
153/// match version {
154///     guess::Version::Certain(cdr) => {
155///         println!("The CDR version is `{}`", cdr.version());
156///     },
157///     guess::Version::Uncertain(_cdr) => {
158///         eprintln!("Unable to guess the version of given CDR JSON.");
159///     }
160/// }
161///
162/// # Ok::<(), ParseError>(())
163/// ```
164///
165/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_cdrs.md>
166/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>
167pub fn parse_and_report(cdr_json: &str) -> Result<guess::CdrReport<'_>, ParseError> {
168    let cdr_json = ReasonableStr::new(cdr_json).map_err(ParseError::from_kind(ObjectType::Cdr))?;
169    guess::cdr_version_and_report(cdr_json)
170}
171
172/// Generate a [`PartialCdr`](generate::PartialCdr) that can be priced by the given tariff.
173///
174/// The CDR is partial as not all required fields are set as the `cdr_from_tariff` function
175/// does not know anything about the EVSE location or the token used to authenticate the chargesession.
176///
177/// * See: [OCPI spec 2.2.1: CDR](<https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_cdrs.asciidoc>)
178pub fn generate_from_tariff(
179    tariff: &tariff::Versioned<'_>,
180    config: &generate::Config,
181) -> Verdict<generate::Report, generate::Warning> {
182    generate::cdr_from_tariff(tariff, config)
183}
184
185/// Price a single `CDR` and return a [`Report`](price::Report).
186///
187/// The `CDR` is checked for internal consistency before being priced. As pricing a `CDR` with
188/// contradictory data will lead to a difficult to debug [`Report`](price::Report).
189/// An [`Error`](price::Warning) is returned if the `CDR` is deemed to be internally inconsistent.
190///
191/// > **_Note_** Pricing the CDR does not require a spec compliant CDR or tariff.
192/// > A best effort is made to parse the given CDR and tariff JSON.
193///
194/// The [`Report`](price::Report) contains the charge session priced according to the specified
195/// tariff and a selection of fields from the source `CDR` that can be used for comparing the
196/// source `CDR` totals with the calculated totals. The [`Report`](price::Report) also contains
197/// a list of unknown fields to help spot misspelled fields.
198///
199/// The source of the tariffs can be controlled using the [`TariffSource`](price::TariffSource).
200/// The timezone can be found or inferred using the [`timezone::find_or_infer`](crate::timezone::find_or_infer) function.
201///
202/// # Example
203///
204/// ```rust
205/// # use ocpi_tariffs::{cdr, price, warning, Version, ParseError};
206/// #
207/// # const CDR_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time/cdr.json");
208///
209/// let report = cdr::parse_with_version(CDR_JSON, Version::V211)?;
210/// let cdr::ParseReport {
211///     cdr,
212///     unexpected_fields,
213/// } = report;
214///
215/// # if !unexpected_fields.is_empty() {
216/// #     eprintln!("Strange... there are fields in the CDR that are not defined in the spec.");
217/// #
218/// #     for path in &unexpected_fields {
219/// #         eprintln!("{path}");
220/// #     }
221/// # }
222///
223/// let report = cdr::price(&cdr, price::TariffSource::UseCdr, chrono_tz::Tz::Europe__Amsterdam).unwrap();
224/// let (report, warnings) = report.into_parts();
225///
226/// if !warnings.is_empty() {
227///     eprintln!("Pricing the CDR resulted in `{}` warnings", warnings.len_warnings());
228///
229///     for group in warnings {
230///         let (element, warnings) = group.to_parts();
231///         eprintln!("  {}", element.path());
232///
233///         for warning in warnings {
234///             eprintln!("    - {warning}");
235///         }
236///     }
237/// }
238///
239/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
240/// ```
241pub fn price(
242    cdr: &Versioned<'_>,
243    tariff_source: price::TariffSource<'_>,
244    timezone: Tz,
245) -> Verdict<price::Report, price::Warning> {
246    price::cdr(cdr, tariff_source, timezone)
247}
248
249/// A [`json::Element`] that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
250/// and was determined to be one of the supported [`Version`]s.
251pub struct Versioned<'buf> {
252    /// The source JSON as string.
253    source: &'buf str,
254
255    /// The parsed JSON as structured [`Element`](crate::json::Element)s.
256    element: json::Element<'buf>,
257
258    /// The `Version` of the CDR, determined during parsing.
259    version: Version,
260}
261
262impl fmt::Debug for Versioned<'_> {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        if f.alternate() {
265            fmt::Debug::fmt(&self.element, f)
266        } else {
267            match self.version {
268                Version::V211 => f.write_str("V211"),
269                Version::V221 => f.write_str("V221"),
270            }
271        }
272    }
273}
274
275impl crate::Versioned for Versioned<'_> {
276    fn version(&self) -> Version {
277        self.version
278    }
279}
280
281impl<'buf> Versioned<'buf> {
282    pub(crate) fn new(source: &'buf str, element: json::Element<'buf>, version: Version) -> Self {
283        Self {
284            source,
285            element,
286            version,
287        }
288    }
289
290    /// Return the inner [`json::Element`] and discard the version info.
291    pub fn into_element(self) -> json::Element<'buf> {
292        self.element
293    }
294
295    /// Return the inner [`json::Element`] and discard the version info.
296    pub fn as_element(&self) -> &json::Element<'buf> {
297        &self.element
298    }
299
300    /// Return the inner JSON `str` and discard the version info.
301    pub fn as_json_str(&self) -> &'buf str {
302        self.source
303    }
304}
305
306/// A [`json::Element`] that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
307/// and was determined to not be one of the supported [`Version`]s.
308#[derive(Debug)]
309pub struct Unversioned<'buf> {
310    source: &'buf str,
311    element: json::Element<'buf>,
312}
313
314impl<'buf> Unversioned<'buf> {
315    /// Create an unversioned [`json::Element`].
316    pub(crate) fn new(source: &'buf str, elem: json::Element<'buf>) -> Self {
317        Self {
318            source,
319            element: elem,
320        }
321    }
322
323    /// Return the inner [`json::Element`] and discard the version info.
324    pub fn into_element(self) -> json::Element<'buf> {
325        self.element
326    }
327
328    /// Return the inner [`json::Element`] and discard the version info.
329    pub fn as_element(&self) -> &json::Element<'buf> {
330        &self.element
331    }
332
333    /// Return the inner `str` and discard the version info.
334    pub fn as_json_str(&self) -> &'buf str {
335        self.source
336    }
337}
338
339impl<'buf> crate::Unversioned for Unversioned<'buf> {
340    type Versioned = Versioned<'buf>;
341
342    fn force_into_versioned(self, version: Version) -> Versioned<'buf> {
343        let Self { source, element } = self;
344        Versioned {
345            source,
346            element,
347            version,
348        }
349    }
350}