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_set_to, money, number, tariff, warning,
9 Versioned as _,
10};
11
12use super::Error;
13
14pub(crate) fn lint(tariff: &tariff::Versioned<'_>) -> Result<Report, Error> {
19 match tariff.version() {
20 crate::Version::V221 => v221::lint(tariff.as_element()),
21 crate::Version::V211 => v211::lint(tariff.as_element()),
22 }
23}
24
25#[derive(Debug)]
27pub struct Report {
28 warnings: warning::Report<WarningKind>,
30}
31
32impl Report {
33 pub fn into_warning_report(self) -> warning::Report<WarningKind> {
34 self.warnings
35 }
36}
37
38#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
39pub enum WarningKind {
40 Country(country::WarningKind),
41
42 CpoCountryCodeShouldBeAlpha2,
44
45 Currency(currency::WarningKind),
46
47 DateTime(datetime::WarningKind),
48
49 Elements(v2x::elements::WarningKind),
50
51 MinPriceIsGreaterThanMax,
53
54 Money(money::WarningKind),
55
56 Number(number::WarningKind),
57
58 RequiredField(&'static str),
60
61 StartDateTimeIsAfterEndDateTime,
63}
64
65from_warning_set_to!(country::WarningKind => WarningKind);
66from_warning_set_to!(datetime::WarningKind => WarningKind);
67from_warning_set_to!(number::WarningKind => WarningKind);
68from_warning_set_to!(money::WarningKind => WarningKind);
69from_warning_set_to!(v2x::elements::WarningKind => WarningKind);
70
71impl From<country::WarningKind> for WarningKind {
72 fn from(warn_kind: country::WarningKind) -> Self {
73 Self::Country(warn_kind)
74 }
75}
76
77impl From<currency::WarningKind> for WarningKind {
78 fn from(warn_kind: currency::WarningKind) -> Self {
79 Self::Currency(warn_kind)
80 }
81}
82
83impl From<datetime::WarningKind> for WarningKind {
84 fn from(warn_kind: datetime::WarningKind) -> Self {
85 Self::DateTime(warn_kind)
86 }
87}
88
89impl From<v2x::elements::WarningKind> for WarningKind {
90 fn from(warn_kind: v2x::elements::WarningKind) -> Self {
91 Self::Elements(warn_kind)
92 }
93}
94
95impl From<number::WarningKind> for WarningKind {
96 fn from(warn_kind: number::WarningKind) -> Self {
97 Self::Number(warn_kind)
98 }
99}
100
101impl From<money::WarningKind> for WarningKind {
102 fn from(warn_kind: money::WarningKind) -> Self {
103 Self::Money(warn_kind)
104 }
105}
106
107impl warning::Kind for WarningKind {
108 fn id(&self) -> Cow<'static, str> {
109 match self {
110 WarningKind::CpoCountryCodeShouldBeAlpha2 => "cpo_country_code_should_be_alpha2".into(),
111 WarningKind::Country(kind) => format!("country.{}", kind.id()).into(),
112 WarningKind::Currency(kind) => format!("currency.{}", kind.id()).into(),
113 WarningKind::DateTime(kind) => format!("datetime.{}", kind.id()).into(),
114 WarningKind::Elements(kind) => format!("elements.{}", kind.id()).into(),
115 WarningKind::MinPriceIsGreaterThanMax => "min_price_is_greater_than_max".into(),
116 WarningKind::Number(kind) => format!("number.{}", kind.id()).into(),
117 WarningKind::Money(kind) => format!("price.{}", kind.id()).into(),
118 WarningKind::StartDateTimeIsAfterEndDateTime => {
119 "start_date_time_is_after_end_date_time".into()
120 }
121 WarningKind::RequiredField(field_name) => format!("required_field.{field_name}").into(),
122 }
123 }
124}
125
126impl fmt::Display for WarningKind {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 WarningKind::CpoCountryCodeShouldBeAlpha2 => {
130 f.write_str("The value should be an alpha-2 ISO 3166-1 country code")
131 }
132 WarningKind::Country(kind) => fmt::Display::fmt(kind, f),
133 WarningKind::Currency(kind) => fmt::Display::fmt(kind, f),
134 WarningKind::DateTime(kind) => fmt::Display::fmt(kind, f),
135 WarningKind::Elements(kind) => fmt::Display::fmt(kind, f),
136 WarningKind::MinPriceIsGreaterThanMax => {
137 f.write_str("The `min_price` is greater than `max_price`.")
138 }
139 WarningKind::Number(kind) => fmt::Display::fmt(kind, f),
140 WarningKind::Money(kind) => fmt::Display::fmt(kind, f),
141 WarningKind::StartDateTimeIsAfterEndDateTime => {
142 f.write_str("The `start_date_time` is after the `end_date_time`.")
143 }
144 WarningKind::RequiredField(field_name) => write!(f, "Required field `{field_name}`"),
145 }
146 }
147}
148
149#[cfg(test)]
150pub mod test {
151 #![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
152 #![allow(clippy::panic, reason = "tests are allowed panic")]
153
154 use std::{
155 collections::{BTreeMap, BTreeSet},
156 path::Path,
157 };
158
159 use assert_matches::assert_matches;
160
161 use crate::{
162 guess, json, tariff, test,
163 warning::{ElementReport, Kind as _},
164 Version, Versioned,
165 };
166
167 use super::Report;
168
169 pub fn lint_tariff(tariff_json: &str, path: &Path, expected_version: Version) {
170 const LINT_FEATURE: &str = "lint";
171 const PARSE_FEATURE: &str = "parse";
172
173 test::setup();
174 let expect_json = test::read_expect_json(path, PARSE_FEATURE);
175 let parse_expect = tariff::test::parse_expect_json(expect_json.as_deref());
176 let report = tariff::parse_and_report(tariff_json).unwrap();
177
178 let tariff = {
179 let version = tariff::test::assert_parse_report(report, parse_expect);
180 let tariff = assert_matches!(version, guess::Version::Certain(tariff) => tariff);
181 assert_eq!(tariff.version(), expected_version);
182 tariff
183 };
184
185 let expect_json = test::read_expect_json(path, LINT_FEATURE);
186 let lint_expect = parse_expect_json(expect_json.as_deref());
187
188 let report = super::lint(&tariff).unwrap();
189
190 assert_report(tariff.as_element(), report, lint_expect);
191 }
192
193 #[derive(serde::Deserialize)]
195 pub struct Expect<'buf> {
196 #[serde(borrow)]
197 warnings: BTreeMap<&'buf str, Vec<&'buf str>>,
198 }
199
200 #[track_caller]
201 pub fn parse_expect_json(expect_json: Option<&str>) -> Option<Expect<'_>> {
202 expect_json.map(|json| serde_json::from_str(json).expect("Unable to parse expect JSON"))
203 }
204
205 #[track_caller]
206 pub fn assert_report(element: &json::Element<'_>, report: Report, expect: Option<Expect<'_>>) {
207 let Report { warnings } = report;
208
209 assert!(
213 warnings.is_empty() || expect.is_some(),
214 "Warnings reported:\n{{\n \"warnings\": {{\n {}\n }}\n}}",
215 warnings
216 .iter(element)
217 .map(|ElementReport { element, warnings }| {
218 format!(
219 "\"{}\": [{}]",
220 element.path(),
221 warnings
222 .iter()
223 .map(|w| format!("\"{}\"", w.id()))
224 .collect::<Vec<_>>()
225 .join(", ")
226 )
227 })
228 .collect::<Vec<_>>()
229 .join(",\n")
230 );
231
232 let Some(Expect {
233 warnings: warnings_expect,
234 }) = expect
235 else {
236 return;
237 };
238
239 for warning in warnings.iter(element) {
240 let ElementReport { element, warnings } = warning;
241 let path_str = element.path().to_string();
242 let Some(warnings_expect) = warnings_expect.get(&*path_str) else {
243 let warning_ids = warnings
244 .iter()
245 .map(|k| format!(" \"{}\",", k.id()))
246 .collect::<Vec<_>>()
247 .join("\n");
248
249 panic!("No warnings expected for `Element` at `{path_str}` but {} warnings were reported:\n[\n{}\n]", warnings.len(), warning_ids);
250 };
251
252 let warnings_expect = warnings_expect.iter().collect::<BTreeSet<_>>();
253
254 for warning_kind in warnings {
255 let id = warning_kind.id();
256 assert!(
257 warnings_expect.contains(&&*id),
258 "Unexpected warning `{id}` for `Element` at `{path_str}`"
259 );
260 }
261 }
262 }
263}