ocpi_tariffs/tariff.rs
1//! Parse a tariff and lint the result.
2use std::fmt;
3
4use crate::{guess, json, lint, ParseError, Version};
5
6/// Parse a `&str` into a [`Versioned`] tariff using a schema for the given [`Version`][^spec-v211][^spec-v221] to check for
7/// any unexpected fields.
8///
9/// # Example
10///
11/// ```rust
12/// # use ocpi_tariffs::{tariff, Version, ParseError};
13/// #
14/// # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
15///
16/// let report = tariff::parse_with_version(TARIFF_JSON, Version::V211)?;
17/// let tariff::ParseReport {
18/// tariff,
19/// unexpected_fields,
20/// } = report;
21///
22/// if !unexpected_fields.is_empty() {
23/// eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
24///
25/// for path in &unexpected_fields {
26/// eprintln!("{path}");
27/// }
28/// }
29///
30/// # Ok::<(), ParseError>(())
31/// ```
32///
33/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md>
34/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>
35pub fn parse_with_version(source: &str, version: Version) -> Result<ParseReport<'_>, ParseError> {
36 match version {
37 Version::V221 => {
38 let schema = &*crate::v221::TARIFF_SCHEMA;
39 let report =
40 json::parse_with_schema(source, schema).map_err(ParseError::from_cdr_err)?;
41 let json::ParseReport {
42 element,
43 unexpected_fields,
44 } = report;
45 Ok(ParseReport {
46 tariff: Versioned::new(source, element, Version::V221),
47 unexpected_fields,
48 })
49 }
50 Version::V211 => {
51 let schema = &*crate::v211::TARIFF_SCHEMA;
52 let report =
53 json::parse_with_schema(source, schema).map_err(ParseError::from_cdr_err)?;
54 let json::ParseReport {
55 element,
56 unexpected_fields,
57 } = report;
58 Ok(ParseReport {
59 tariff: Versioned::new(source, element, Version::V211),
60 unexpected_fields,
61 })
62 }
63 }
64}
65
66/// Parse the JSON and try guess the [`Version`] based on fields defined in the OCPI v2.1.1[^spec-v211] and v2.2.1[^spec-v221] tariff spec.
67///
68/// The parser is forgiving and will not complain if the tariff JSON is missing required fields.
69/// The parser will also not complain if unexpected fields are present in the JSON.
70/// The [`Version`] guess is based on fields that exist.
71///
72/// # Example
73///
74/// ```rust
75/// # use ocpi_tariffs::{tariff, guess, ParseError, Version, Versioned as _};
76/// #
77/// # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
78/// let tariff = tariff::parse(TARIFF_JSON)?;
79///
80/// match tariff {
81/// guess::Version::Certain(tariff) => {
82/// println!("The tariff version is `{}`", tariff.version());
83/// },
84/// guess::Version::Uncertain(_tariff) => {
85/// eprintln!("Unable to guess the version of given tariff JSON.");
86/// }
87/// }
88///
89/// # Ok::<(), ParseError>(())
90/// ```
91///
92/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md>
93/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>
94pub fn parse(tariff_json: &str) -> Result<guess::TariffVersion<'_>, ParseError> {
95 guess::tariff_version(tariff_json)
96}
97
98/// Guess the [`Version`][^spec-v211][^spec-v221] of the given tariff JSON and report on any unexpected fields.
99///
100/// The parser is forgiving and will not complain if the tariff JSON is missing required fields.
101/// The parser will also not complain if unexpected fields are present in the JSON.
102/// The [`Version`] guess is based on fields that exist.
103///
104/// # Example
105///
106/// ```rust
107/// # use ocpi_tariffs::{guess, tariff, warning};
108/// #
109/// # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
110///
111/// let report = tariff::parse_and_report(TARIFF_JSON)?;
112/// let guess::Report {
113/// unexpected_fields,
114/// version,
115/// } = report;
116///
117/// if !unexpected_fields.is_empty() {
118/// eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
119///
120/// for path in &unexpected_fields {
121/// eprintln!(" * {path}");
122/// }
123///
124/// eprintln!();
125/// }
126///
127/// let guess::Version::Certain(tariff) = version else {
128/// return Err("Unable to guess the version of given CDR JSON.".into());
129/// };
130///
131/// let report = tariff::lint(&tariff)?;
132/// let warnings = report.into_warning_report();
133///
134/// eprintln!("`{}` lint warnings found", warnings.len());
135///
136/// for warning::ElementReport { element, warnings } in warnings.iter(tariff.as_element()) {
137/// eprintln!(
138/// "Warnings reported for `json::Element` at path: `{}`",
139/// element.path()
140/// );
141///
142/// for warning in warnings {
143/// eprintln!(" * {warning}");
144/// }
145///
146/// eprintln!();
147/// }
148///
149/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
150/// ```
151///
152/// [^spec-v211]: <https://github.com/ocpi/ocpi/blob/release-2.1.1-bugfixes/mod_tariffs.md>
153/// [^spec-v221]: <https://github.com/ocpi/ocpi/blob/release-2.2.1-bugfixes/mod_tariffs.asciidoc>
154pub fn parse_and_report(tariff_json: &str) -> Result<guess::TariffReport<'_>, ParseError> {
155 guess::tariff_version_with_report(tariff_json)
156}
157
158/// A [`Versioned`] tariff along with a set of unexpected fields.
159#[derive(Debug)]
160pub struct ParseReport<'buf> {
161 /// The root JSON `Element`.
162 pub tariff: Versioned<'buf>,
163
164 /// A list of fields that were not expected: The schema did not define them.
165 pub unexpected_fields: json::UnexpectedFields<'buf>,
166}
167
168/// A `json::Element` that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
169/// and has been identified as being a certain [`Version`].
170pub struct Versioned<'buf> {
171 /// The source JSON as string.
172 source: &'buf str,
173
174 /// The parsed JSON as structured [`Element`](crate::json::Element)s.
175 element: json::Element<'buf>,
176
177 /// The `Version` of the tariff, determined during parsing.
178 version: Version,
179}
180
181impl fmt::Debug for Versioned<'_> {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 if f.alternate() {
184 fmt::Debug::fmt(&self.element, f)
185 } else {
186 match self.version {
187 Version::V211 => f.write_str("V211"),
188 Version::V221 => f.write_str("V221"),
189 }
190 }
191 }
192}
193
194impl crate::Versioned for Versioned<'_> {
195 fn version(&self) -> Version {
196 self.version
197 }
198}
199
200impl<'buf> Versioned<'buf> {
201 pub(crate) fn new(source: &'buf str, element: json::Element<'buf>, version: Version) -> Self {
202 Self {
203 source,
204 element,
205 version,
206 }
207 }
208
209 /// Return the inner [`json::Element`] and discard the version info.
210 pub fn into_element(self) -> json::Element<'buf> {
211 self.element
212 }
213
214 /// Return the inner [`json::Element`] and discard the version info.
215 pub fn as_element(&self) -> &json::Element<'buf> {
216 &self.element
217 }
218
219 /// Return the inner JSON `str` and discard the version info.
220 pub fn as_json_str(&self) -> &'buf str {
221 self.source
222 }
223}
224
225/// A [`json::Element`] that has been parsed by the either the [`parse_with_version`] or [`parse`] functions
226/// and was determined to not be one of the supported [`Version`]s.
227#[derive(Debug)]
228pub struct Unversioned<'buf> {
229 /// The source JSON as string.
230 source: &'buf str,
231
232 /// A list of fields that were not expected: The schema did not define them.
233 element: json::Element<'buf>,
234}
235
236impl<'buf> Unversioned<'buf> {
237 /// Create an unversioned [`json::Element`].
238 pub(crate) fn new(source: &'buf str, elem: json::Element<'buf>) -> Self {
239 Self {
240 source,
241 element: elem,
242 }
243 }
244
245 /// Return the inner [`json::Element`] and discard the version info.
246 pub fn into_element(self) -> json::Element<'buf> {
247 self.element
248 }
249
250 /// Return the inner [`json::Element`] and discard the version info.
251 pub fn as_element(&self) -> &json::Element<'buf> {
252 &self.element
253 }
254
255 /// Return the inner JSON `&str` and discard the version info.
256 pub fn as_json_str(&self) -> &'buf str {
257 self.source
258 }
259}
260
261impl<'buf> crate::Unversioned for Unversioned<'buf> {
262 type Versioned = Versioned<'buf>;
263
264 fn force_into_versioned(self, version: Version) -> Versioned<'buf> {
265 let Self { source, element } = self;
266 Versioned {
267 source,
268 element,
269 version,
270 }
271 }
272}
273
274/// Lint the given tariff and return a [`lint::tariff::Report`] of any `Warning`s found.
275///
276/// # Example
277///
278/// ```rust
279/// # use ocpi_tariffs::{guess, tariff, warning};
280/// #
281/// # const TARIFF_JSON: &str = include_str!("../test_data/v211/real_world/time_and_parking_time_separate_tariff/tariff.json");
282///
283/// let report = tariff::parse_and_report(TARIFF_JSON)?;
284/// let guess::Report {
285/// unexpected_fields,
286/// version,
287/// } = report;
288///
289/// if !unexpected_fields.is_empty() {
290/// eprintln!("Strange... there are fields in the tariff that are not defined in the spec.");
291///
292/// for path in &unexpected_fields {
293/// eprintln!(" * {path}");
294/// }
295///
296/// eprintln!();
297/// }
298///
299/// let guess::Version::Certain(tariff) = version else {
300/// return Err("Unable to guess the version of given CDR JSON.".into());
301/// };
302///
303/// let report = tariff::lint(&tariff)?;
304/// let warnings = report.into_warning_report();
305///
306/// eprintln!("`{}` lint warnings found", warnings.len());
307///
308/// for warning::ElementReport { element, warnings } in warnings.iter(tariff.as_element()) {
309/// eprintln!(
310/// "Warnings reported for `json::Element` at path: `{}`",
311/// element.path()
312/// );
313///
314/// for warning in warnings {
315/// eprintln!(" * {warning}");
316/// }
317///
318/// eprintln!();
319/// }
320///
321/// # Ok::<(), Box<dyn std::error::Error + Send + Sync + 'static>>(())
322/// ```
323pub fn lint(tariff: &Versioned<'_>) -> Result<lint::tariff::Report, lint::Error> {
324 lint::tariff(tariff)
325}
326
327#[cfg(test)]
328mod test_real_world {
329 use std::path::Path;
330
331 use assert_matches::assert_matches;
332
333 use crate::{guess, test, Version, Versioned as _};
334
335 use super::{
336 parse_and_report,
337 test::{assert_parse_report, parse_expect_json},
338 };
339
340 #[test_each::file(
341 glob = "ocpi-tariffs/test_data/v211/real_world/*/tariff*.json",
342 name(segments = 2)
343 )]
344 fn test_parse_v211(tariff_json: &str, path: &Path) {
345 test::setup();
346 expect_version(tariff_json, path, Version::V211);
347 }
348
349 #[test_each::file(
350 glob = "ocpi-tariffs/test_data/v221/real_world/*/tariff*.json",
351 name(segments = 2)
352 )]
353 fn test_parse_v221(tariff_json: &str, path: &Path) {
354 test::setup();
355 expect_version(tariff_json, path, Version::V221);
356 }
357
358 /// Parse the given JSON as a tariff and generate a report on the unexpected fields.
359 fn expect_version(tariff_json: &str, path: &Path, expected_version: Version) {
360 let report = parse_and_report(tariff_json).unwrap();
361
362 let expect_json = test::read_expect_json(path, "parse");
363 let parse_expect = parse_expect_json(expect_json.as_deref());
364
365 let tariff = assert_matches!(&report.version, guess::Version::Certain(tariff) => tariff);
366 assert_eq!(tariff.version(), expected_version);
367
368 assert_parse_report(report, parse_expect);
369 }
370}
371
372#[cfg(test)]
373pub mod test {
374 #![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
375 #![allow(clippy::panic, reason = "tests are allowed panic")]
376
377 use crate::{
378 guess, json,
379 test::{assert_no_unexpected_fields, Expectation},
380 };
381
382 /// Expectations for the result of calling `json::parse_and_report`.
383 #[derive(Debug, serde::Deserialize)]
384 pub struct ParseExpect<'buf> {
385 #[serde(borrow, default)]
386 unexpected_fields: Expectation<Vec<json::test::PathGlob<'buf>>>,
387 }
388
389 #[track_caller]
390 pub fn parse_expect_json(expect_json: Option<&str>) -> Option<ParseExpect<'_>> {
391 expect_json.map(|json| serde_json::from_str(json).expect("Unable to parse expect JSON"))
392 }
393
394 /// Assert that the `TariffReport` resulting from the call to `tariff::parse_and_report`
395 /// matches the expectations of the `ParseExpect` file.
396 #[track_caller]
397 pub fn assert_parse_report<'bin>(
398 report: guess::TariffReport<'bin>,
399 expect: Option<ParseExpect<'_>>,
400 ) -> guess::TariffVersion<'bin> {
401 let guess::Report {
402 version,
403 mut unexpected_fields,
404 } = report;
405 let Some(expect) = expect else {
406 assert_no_unexpected_fields(&unexpected_fields);
407 return version;
408 };
409
410 let ParseExpect {
411 unexpected_fields: unexpected_fields_expect,
412 } = expect;
413
414 if let Expectation::Present(expectation) = unexpected_fields_expect {
415 let unexpected_fields_expect = expectation.expect_value();
416
417 for expect_glob in unexpected_fields_expect {
418 unexpected_fields.filter_matches(&expect_glob);
419 }
420
421 assert_no_unexpected_fields(&unexpected_fields);
422 } else {
423 assert_no_unexpected_fields(&unexpected_fields);
424 }
425
426 version
427 }
428}