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) -> crate::SmartString {
89 match self {
90 Self::CpoCountryCodeShouldBeAlpha2 => "cpo_country_code_should_be_alpha2".into(),
91 Self::Country(kind) => kind.id(),
92 Self::Currency(kind) => kind.id(),
93 Self::DateTime(kind) => kind.id(),
94 Self::Elements(kind) => kind.id(),
95 Self::MinPriceIsGreaterThanMax => "min_price_is_greater_than_max".into(),
96 Self::Number(kind) => kind.id(),
97 Self::Money(kind) => kind.id(),
98 Self::NeedlessNullField => "needless_null_field".into(),
99 Self::StartDateTimeIsAfterEndDateTime => {
100 "start_date_time_is_after_end_date_time".into()
101 }
102 Self::String(kind) => kind.id(),
103 Self::FieldRequired { field_name } => format!("field_required({field_name})").into(),
104 }
105 }
106}
107
108impl fmt::Display for Warning {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 Self::CpoCountryCodeShouldBeAlpha2 => {
112 f.write_str("The value should be an alpha-2 ISO 3166-1 country code")
113 }
114 Self::Country(kind) => fmt::Display::fmt(kind, f),
115 Self::Currency(kind) => fmt::Display::fmt(kind, f),
116 Self::DateTime(kind) => fmt::Display::fmt(kind, f),
117 Self::Elements(kind) => fmt::Display::fmt(kind, f),
118 Self::MinPriceIsGreaterThanMax => {
119 f.write_str("The `min_price` is greater than `max_price`.")
120 }
121 Self::Number(kind) => fmt::Display::fmt(kind, f),
122 Self::Money(kind) => fmt::Display::fmt(kind, f),
123 Self::NeedlessNullField => write!(f, "Null field: the field can simply be removed."),
124 Self::StartDateTimeIsAfterEndDateTime => {
125 f.write_str("The `start_date_time` is after the `end_date_time`.")
126 }
127 Self::String(kind) => fmt::Display::fmt(kind, f),
128 Self::FieldRequired { field_name } => {
129 write!(f, "Field is required: `{field_name}`")
130 }
131 }
132 }
133}
134
135#[cfg(test)]
136pub mod test {
137 #![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
138 #![allow(clippy::panic, reason = "tests are allowed panic")]
139
140 use std::{collections::BTreeMap, path::Path};
141
142 use assert_matches::assert_matches;
143
144 use crate::{
145 guess, tariff,
146 test::{self, ExpectFile, Expectation},
147 warning, Version, Versioned,
148 };
149
150 use super::Report;
151
152 pub fn lint_tariff(tariff_json: &str, path: &Path, expected_version: Version) {
153 const LINT_FEATURE: &str = "lint";
154 const PARSE_FEATURE: &str = "parse";
155
156 test::setup();
157 let expect_json = test::read_expect_json(path, PARSE_FEATURE);
158 let parse_expect = test::parse_expect_json(expect_json.as_deref());
159 let report = tariff::parse_and_report(tariff_json).unwrap();
160
161 let tariff = {
162 let version = tariff::test::assert_parse_guess_report(report, parse_expect);
163 let tariff = assert_matches!(version, guess::Version::Certain(tariff) => tariff);
164 assert_eq!(tariff.version(), expected_version);
165 tariff
166 };
167
168 let expect_json = test::read_expect_json(path, LINT_FEATURE);
169 let lint_expect = test::parse_expect_json(expect_json.as_deref());
170
171 let report = super::lint(&tariff);
172
173 assert_report(report, lint_expect);
174 }
175
176 #[derive(serde::Deserialize)]
178 pub struct Expect {
179 #[serde(default)]
180 warnings: Expectation<BTreeMap<String, Vec<String>>>,
181 }
182
183 #[track_caller]
184 pub(crate) fn assert_report(report: Report, expect: ExpectFile<Expect>) {
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.path_id_map()
200 );
201 return;
202 };
203 warning::test::assert_warnings(&expect_file_name, &warnings, expect.warnings);
204 }
205}