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