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}