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