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