1pub(crate) mod v211;
4pub(crate) mod v221;
5pub(crate) mod v2x;
6
7use std::{borrow::Cow, fmt};
8
9use crate::{
10 country, currency, datetime, duration, from_warning_set_to, guess, json, lint, money, number,
11 string, warning, weekday, ParseError, Version,
12};
13
14#[derive(Debug)]
15pub enum WarningKind {
16 Country(country::WarningKind),
18 Currency(currency::WarningKind),
19 DateTime(datetime::WarningKind),
20 Decode(json::decode::WarningKind),
21 Duration(duration::WarningKind),
22
23 FieldInvalidType {
25 expected_type: json::ValueKind,
27 },
28
29 FieldInvalidValue {
31 value: String,
33
34 message: Cow<'static, str>,
36 },
37
38 FieldRequired {
40 field_name: Cow<'static, str>,
41 },
42
43 Money(money::WarningKind),
44
45 TotalCostClampedToMin,
49
50 TotalCostClampedToMax,
54
55 NoElements,
57
58 NotActive,
60 Number(number::WarningKind),
61
62 String(string::WarningKind),
63 Weekday(weekday::WarningKind),
64}
65
66impl WarningKind {
67 fn field_invalid_value(
69 value: impl Into<String>,
70 message: impl Into<Cow<'static, str>>,
71 ) -> Self {
72 WarningKind::FieldInvalidValue {
73 value: value.into(),
74 message: message.into(),
75 }
76 }
77}
78
79impl fmt::Display for WarningKind {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::String(warning_kind) => write!(f, "{warning_kind}"),
83 Self::Country(warning_kind) => write!(f, "{warning_kind}"),
84 Self::Currency(warning_kind) => write!(f, "{warning_kind}"),
85 Self::DateTime(warning_kind) => write!(f, "{warning_kind}"),
86 Self::Decode(warning_kind) => write!(f, "{warning_kind}"),
87 Self::Duration(warning_kind) => write!(f, "{warning_kind}"),
88 Self::FieldInvalidType { expected_type } => {
89 write!(f, "Field has invalid type. Expected type `{expected_type}`")
90 }
91 Self::FieldInvalidValue { value, message } => {
92 write!(f, "Field has invalid value `{value}`: {message}")
93 }
94 Self::FieldRequired { field_name } => {
95 write!(f, "Field is required: {field_name}")
96 }
97 Self::Money(warning_kind) => write!(f, "{warning_kind}"),
98 Self::NoElements => f.write_str("The tariff has no `elements`"),
99 Self::NotActive => f.write_str("The tariff is not active for `Cdr::start_date_time`"),
100 Self::Number(warning_kind) => write!(f, "{warning_kind}"),
101 Self::TotalCostClampedToMin => write!(
102 f,
103 "The given tariff has a `min_price` set and the `total_cost` fell below it."
104 ),
105 Self::TotalCostClampedToMax => write!(
106 f,
107 "The given tariff has a `max_price` set and the `total_cost` exceeded it."
108 ),
109 Self::Weekday(warning_kind) => write!(f, "{warning_kind}"),
110 }
111 }
112}
113
114impl warning::Kind for WarningKind {
115 fn id(&self) -> Cow<'static, str> {
116 match self {
117 Self::String(kind) => kind.id(),
118 Self::Country(kind) => kind.id(),
119 Self::Currency(kind) => kind.id(),
120 Self::DateTime(kind) => kind.id(),
121 Self::Decode(kind) => kind.id(),
122 Self::Duration(kind) => kind.id(),
123 Self::FieldInvalidType { .. } => "field_invalid_type".into(),
124 Self::FieldInvalidValue { .. } => "field_invalid_value".into(),
125 Self::FieldRequired { .. } => "field_required".into(),
126 Self::Money(kind) => kind.id(),
127 Self::NoElements => "no_elements".into(),
128 Self::NotActive => "not_active".into(),
129 Self::Number(kind) => kind.id(),
130 Self::TotalCostClampedToMin => "total_cost_clamped_to_min".into(),
131 Self::TotalCostClampedToMax => "total_cost_clamped_to_max".into(),
132 Self::Weekday(kind) => kind.id(),
133 }
134 }
135}
136
137impl From<country::WarningKind> for WarningKind {
138 fn from(warn_kind: country::WarningKind) -> Self {
139 Self::Country(warn_kind)
140 }
141}
142
143impl From<currency::WarningKind> for WarningKind {
144 fn from(warn_kind: currency::WarningKind) -> Self {
145 Self::Currency(warn_kind)
146 }
147}
148
149impl From<datetime::WarningKind> for WarningKind {
150 fn from(warn_kind: datetime::WarningKind) -> Self {
151 Self::DateTime(warn_kind)
152 }
153}
154
155impl From<duration::WarningKind> for WarningKind {
156 fn from(warn_kind: duration::WarningKind) -> Self {
157 Self::Duration(warn_kind)
158 }
159}
160
161impl From<json::decode::WarningKind> for WarningKind {
162 fn from(warn_kind: json::decode::WarningKind) -> Self {
163 Self::Decode(warn_kind)
164 }
165}
166
167impl From<money::WarningKind> for WarningKind {
168 fn from(warn_kind: money::WarningKind) -> Self {
169 Self::Money(warn_kind)
170 }
171}
172
173impl From<number::WarningKind> for WarningKind {
174 fn from(warn_kind: number::WarningKind) -> Self {
175 Self::Number(warn_kind)
176 }
177}
178
179impl From<string::WarningKind> for WarningKind {
180 fn from(warn_kind: string::WarningKind) -> Self {
181 Self::String(warn_kind)
182 }
183}
184
185impl From<weekday::WarningKind> for WarningKind {
186 fn from(warn_kind: weekday::WarningKind) -> Self {
187 Self::Weekday(warn_kind)
188 }
189}
190
191from_warning_set_to!(string::WarningKind => WarningKind);
192from_warning_set_to!(country::WarningKind => WarningKind);
193from_warning_set_to!(currency::WarningKind => WarningKind);
194from_warning_set_to!(datetime::WarningKind => WarningKind);
195from_warning_set_to!(duration::WarningKind => WarningKind);
196from_warning_set_to!(weekday::WarningKind => WarningKind);
197from_warning_set_to!(number::WarningKind => WarningKind);
198from_warning_set_to!(money::WarningKind => WarningKind);
199
200pub fn parse_with_version(source: &str, version: Version) -> Result<ParseReport<'_>, ParseError> {
230 match version {
231 Version::V221 => {
232 let schema = &*crate::v221::TARIFF_SCHEMA;
233 let report =
234 json::parse_with_schema(source, schema).map_err(ParseError::from_cdr_err)?;
235 let json::ParseReport {
236 element,
237 unexpected_fields,
238 } = report;
239 Ok(ParseReport {
240 tariff: Versioned::new(source, element, Version::V221),
241 unexpected_fields,
242 })
243 }
244 Version::V211 => {
245 let schema = &*crate::v211::TARIFF_SCHEMA;
246 let report =
247 json::parse_with_schema(source, schema).map_err(ParseError::from_cdr_err)?;
248 let json::ParseReport {
249 element,
250 unexpected_fields,
251 } = report;
252 Ok(ParseReport {
253 tariff: Versioned::new(source, element, Version::V211),
254 unexpected_fields,
255 })
256 }
257 }
258}
259
260pub fn parse(tariff_json: &str) -> Result<guess::TariffVersion<'_>, ParseError> {
290 guess::tariff_version(tariff_json)
291}
292
293pub fn parse_and_report(tariff_json: &str) -> Result<guess::TariffReport<'_>, ParseError> {
349 guess::tariff_version_with_report(tariff_json)
350}
351
352#[derive(Debug)]
354pub struct ParseReport<'buf> {
355 pub tariff: Versioned<'buf>,
357
358 pub unexpected_fields: json::UnexpectedFields<'buf>,
360}
361
362#[derive(Clone)]
365pub struct Versioned<'buf> {
366 source: &'buf str,
368
369 element: json::Element<'buf>,
371
372 version: Version,
374}
375
376impl fmt::Debug for Versioned<'_> {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 if f.alternate() {
379 fmt::Debug::fmt(&self.element, f)
380 } else {
381 match self.version {
382 Version::V211 => f.write_str("V211"),
383 Version::V221 => f.write_str("V221"),
384 }
385 }
386 }
387}
388
389impl crate::Versioned for Versioned<'_> {
390 fn version(&self) -> Version {
391 self.version
392 }
393}
394
395impl<'buf> Versioned<'buf> {
396 pub(crate) fn new(source: &'buf str, element: json::Element<'buf>, version: Version) -> Self {
397 Self {
398 source,
399 element,
400 version,
401 }
402 }
403
404 pub fn into_element(self) -> json::Element<'buf> {
406 self.element
407 }
408
409 pub fn as_element(&self) -> &json::Element<'buf> {
411 &self.element
412 }
413
414 pub fn as_json_str(&self) -> &'buf str {
416 self.source
417 }
418}
419
420#[derive(Debug)]
423pub struct Unversioned<'buf> {
424 source: &'buf str,
426
427 element: json::Element<'buf>,
429}
430
431impl<'buf> Unversioned<'buf> {
432 pub(crate) fn new(source: &'buf str, elem: json::Element<'buf>) -> Self {
434 Self {
435 source,
436 element: elem,
437 }
438 }
439
440 pub fn into_element(self) -> json::Element<'buf> {
442 self.element
443 }
444
445 pub fn as_element(&self) -> &json::Element<'buf> {
447 &self.element
448 }
449
450 pub fn as_json_str(&self) -> &'buf str {
452 self.source
453 }
454}
455
456impl<'buf> crate::Unversioned for Unversioned<'buf> {
457 type Versioned = Versioned<'buf>;
458
459 fn force_into_versioned(self, version: Version) -> Versioned<'buf> {
460 let Self { source, element } = self;
461 Versioned {
462 source,
463 element,
464 version,
465 }
466 }
467}
468
469pub fn lint(tariff: &Versioned<'_>) -> Result<lint::tariff::Report, lint::Error> {
518 lint::tariff(tariff)
519}
520
521#[cfg(test)]
522mod test_real_world {
523 use std::path::Path;
524
525 use assert_matches::assert_matches;
526
527 use crate::{guess, test, Version, Versioned as _};
528
529 use super::{parse_and_report, test::assert_parse_guess_report};
530
531 #[test_each::file(
532 glob = "ocpi-tariffs/test_data/v211/real_world/*/tariff*.json",
533 name(segments = 2)
534 )]
535 fn test_parse_v211(tariff_json: &str, path: &Path) {
536 test::setup();
537 expect_version(tariff_json, path, Version::V211);
538 }
539
540 #[test_each::file(
541 glob = "ocpi-tariffs/test_data/v221/real_world/*/tariff*.json",
542 name(segments = 2)
543 )]
544 fn test_parse_v221(tariff_json: &str, path: &Path) {
545 test::setup();
546 expect_version(tariff_json, path, Version::V221);
547 }
548
549 fn expect_version(tariff_json: &str, path: &Path, expected_version: Version) {
551 let report = parse_and_report(tariff_json).unwrap();
552
553 let expect_json = test::read_expect_json(path, "parse");
554 let parse_expect = test::parse_expect_json(expect_json.as_deref());
555
556 let tariff = assert_matches!(&report.version, guess::Version::Certain(tariff) => tariff);
557 assert_eq!(tariff.version(), expected_version);
558
559 assert_parse_guess_report(report, parse_expect);
560 }
561}
562
563#[cfg(test)]
564pub mod test {
565 #![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
566 #![allow(clippy::panic, reason = "tests are allowed panic")]
567
568 use std::collections::BTreeMap;
569
570 use crate::{
571 guess, json,
572 test::{ExpectFile, Expectation},
573 warning,
574 };
575
576 #[derive(Debug, serde::Deserialize)]
578 pub struct ParseExpect {
579 #[serde(default)]
580 unexpected_fields: Expectation<Vec<json::test::PathGlob>>,
581 }
582
583 #[derive(Debug, serde::Deserialize)]
585 pub struct FromJsonExpect {
586 #[serde(default)]
587 warnings: Expectation<BTreeMap<String, Vec<String>>>,
588 }
589
590 #[track_caller]
593 pub(crate) fn assert_parse_guess_report(
594 report: guess::TariffReport<'_>,
595 expect: ExpectFile<ParseExpect>,
596 ) -> guess::TariffVersion<'_> {
597 let guess::Report {
598 version,
599 mut unexpected_fields,
600 } = report;
601
602 let ExpectFile {
603 value: expect,
604 expect_file_name,
605 } = expect;
606
607 let Some(expect) = expect else {
608 json::test::expect_no_unexpected_fields(&expect_file_name, &unexpected_fields);
609 return version;
610 };
611
612 let ParseExpect {
613 unexpected_fields: expected,
614 } = expect;
615
616 json::test::expect_unexpected_fields(&expect_file_name, &mut unexpected_fields, expected);
617
618 version
619 }
620
621 #[track_caller]
624 pub(crate) fn assert_parse_report(
625 mut unexpected_fields: json::UnexpectedFields<'_>,
626 expect: ExpectFile<ParseExpect>,
627 ) {
628 let ExpectFile {
629 value,
630 expect_file_name,
631 } = expect;
632
633 let Some(ParseExpect {
634 unexpected_fields: expected,
635 }) = value
636 else {
637 json::test::expect_no_unexpected_fields(&expect_file_name, &unexpected_fields);
638 return;
639 };
640
641 json::test::expect_unexpected_fields(&expect_file_name, &mut unexpected_fields, expected);
642 }
643
644 pub(crate) fn assert_from_json_warnings(
645 root: &json::Element<'_>,
646 warnings: &warning::Set<super::WarningKind>,
647 expect: ExpectFile<FromJsonExpect>,
648 ) {
649 let ExpectFile {
650 value,
651 expect_file_name,
652 } = expect;
653
654 let Some(expect) = value else {
658 assert!(
659 warnings.is_empty(),
660 "There is no expectation file at `{expect_file_name}` but the tariff has warnings;\n{:?}",
661 warnings.group_by_elem(root).into_stringified_map()
662 );
663 return;
664 };
665
666 warning::test::assert_warnings(&expect_file_name, root, warnings, expect.warnings);
667 }
668}