1pub(crate) mod v211;
2pub(crate) mod v221;
3pub mod v2x;
4
5use std::{borrow::Cow, fmt};
6
7use crate::{
8 country, currency, datetime, from_warning_all, json, money, number, string, tariff, warning,
9 Versioned as _, Warning,
10};
11
12use super::Error;
13
14pub(crate) fn lint(tariff: &tariff::Versioned<'_>) -> Result<Report, Error> {
19 let null_field_warnings = json::walk::DepthFirst::new(tariff.as_element())
20 .filter(|&elem| elem.value().is_null())
21 .map(|elem| Warning::with_elem(WarningKind::NeedlessNullField, elem))
22 .collect::<Vec<_>>();
23
24 let warnings = warning::Set::from_vec(null_field_warnings);
25
26 match tariff.version() {
27 crate::Version::V221 => v221::lint(tariff.as_element(), warnings),
28 crate::Version::V211 => v211::lint(tariff.as_element(), warnings),
29 }
30}
31
32#[derive(Debug)]
34pub struct Report {
35 pub warnings: warning::Set<WarningKind>,
37}
38
39#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
40pub enum WarningKind {
41 Country(country::WarningKind),
42
43 CpoCountryCodeShouldBeAlpha2,
45
46 Currency(currency::WarningKind),
47
48 DateTime(datetime::WarningKind),
49
50 Elements(v2x::elements::WarningKind),
51
52 MinPriceIsGreaterThanMax,
54
55 Money(money::WarningKind),
56
57 NeedlessNullField,
59
60 Number(number::WarningKind),
61
62 FieldRequired {
64 field_name: Cow<'static, str>,
65 },
66
67 String(string::WarningKind),
68
69 StartDateTimeIsAfterEndDateTime,
71}
72
73from_warning_all!(
74 country::WarningKind => WarningKind::Country,
75 currency::WarningKind => WarningKind::Currency,
76 datetime::WarningKind => WarningKind::DateTime,
77 number::WarningKind => WarningKind::Number,
78 money::WarningKind => WarningKind::Money,
79 string::WarningKind => WarningKind::String,
80 v2x::elements::WarningKind => WarningKind::Elements
81);
82
83impl warning::Kind for WarningKind {
84 fn id(&self) -> Cow<'static, str> {
85 match self {
86 Self::CpoCountryCodeShouldBeAlpha2 => "cpo_country_code_should_be_alpha2".into(),
87 Self::Country(kind) => kind.id(),
88 Self::Currency(kind) => kind.id(),
89 Self::DateTime(kind) => kind.id(),
90 Self::Elements(kind) => kind.id(),
91 Self::MinPriceIsGreaterThanMax => "min_price_is_greater_than_max".into(),
92 Self::Number(kind) => kind.id(),
93 Self::Money(kind) => kind.id(),
94 Self::NeedlessNullField => "needless_null_field".into(),
95 Self::StartDateTimeIsAfterEndDateTime => {
96 "start_date_time_is_after_end_date_time".into()
97 }
98 Self::String(kind) => kind.id(),
99 Self::FieldRequired { field_name } => format!("field_required({field_name})").into(),
100 }
101 }
102}
103
104impl fmt::Display for WarningKind {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::CpoCountryCodeShouldBeAlpha2 => {
108 f.write_str("The value should be an alpha-2 ISO 3166-1 country code")
109 }
110 Self::Country(kind) => fmt::Display::fmt(kind, f),
111 Self::Currency(kind) => fmt::Display::fmt(kind, f),
112 Self::DateTime(kind) => fmt::Display::fmt(kind, f),
113 Self::Elements(kind) => fmt::Display::fmt(kind, f),
114 Self::MinPriceIsGreaterThanMax => {
115 f.write_str("The `min_price` is greater than `max_price`.")
116 }
117 Self::Number(kind) => fmt::Display::fmt(kind, f),
118 Self::Money(kind) => fmt::Display::fmt(kind, f),
119 Self::NeedlessNullField => write!(f, "Null field: the field can simply be removed."),
120 Self::StartDateTimeIsAfterEndDateTime => {
121 f.write_str("The `start_date_time` is after the `end_date_time`.")
122 }
123 Self::String(kind) => fmt::Display::fmt(kind, f),
124 Self::FieldRequired { field_name } => {
125 write!(f, "Field is required: {field_name}")
126 }
127 }
128 }
129}
130
131#[cfg(test)]
132pub mod test {
133 #![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
134 #![allow(clippy::panic, reason = "tests are allowed panic")]
135
136 use std::{collections::BTreeMap, path::Path};
137
138 use assert_matches::assert_matches;
139
140 use crate::{
141 guess, json, tariff,
142 test::{self, ExpectFile, Expectation},
143 warning, Version, Versioned,
144 };
145
146 use super::Report;
147
148 pub fn lint_tariff(tariff_json: &str, path: &Path, expected_version: Version) {
149 const LINT_FEATURE: &str = "lint";
150 const PARSE_FEATURE: &str = "parse";
151
152 test::setup();
153 let expect_json = test::read_expect_json(path, PARSE_FEATURE);
154 let parse_expect = test::parse_expect_json(expect_json.as_deref());
155 let report = tariff::parse_and_report(tariff_json).unwrap();
156
157 let tariff = {
158 let version = tariff::test::assert_parse_guess_report(report, parse_expect);
159 let tariff = assert_matches!(version, guess::Version::Certain(tariff) => tariff);
160 assert_eq!(tariff.version(), expected_version);
161 tariff
162 };
163
164 let expect_json = test::read_expect_json(path, LINT_FEATURE);
165 let lint_expect = test::parse_expect_json(expect_json.as_deref());
166
167 let report = super::lint(&tariff).unwrap();
168
169 assert_report(tariff.as_element(), report, lint_expect);
170 }
171
172 #[derive(serde::Deserialize)]
174 pub struct Expect {
175 #[serde(default)]
176 warnings: Expectation<BTreeMap<String, Vec<String>>>,
177 }
178
179 #[track_caller]
180 pub(crate) fn assert_report(
181 root: &json::Element<'_>,
182 report: Report,
183 expect: ExpectFile<Expect>,
184 ) {
185 let Report { warnings } = report;
186
187 let ExpectFile {
191 value: expect,
192 expect_file_name,
193 } = expect;
194
195 let Some(expect) = expect else {
196 assert!(
197 warnings.is_empty(),
198 "There is no expectation file at `{expect_file_name}` but the tariff has warnings;\n{:?}",
199 warnings.group_by_elem(root).into_id_map()
200 );
201 return;
202 };
203 warning::test::assert_warnings(&expect_file_name, root, &warnings, expect.warnings);
204 }
205}