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}