1use dpp_plugin_traits::{PluginError, PluginFieldError, PluginInput};
13use serde_json::Value;
14
15fn present<'a>(input: &'a Value, key: &str) -> Option<&'a Value> {
17 match input.get(key) {
18 Some(Value::Null) | None => None,
19 other => other,
20 }
21}
22
23#[must_use]
25pub fn num(input: &PluginInput, key: &str) -> Option<f64> {
26 input
27 .get(key)
28 .and_then(Value::as_f64)
29 .filter(|n| n.is_finite())
30}
31
32#[must_use]
34pub fn str_of<'a>(input: &'a PluginInput, key: &str) -> Option<&'a str> {
35 input.get(key).and_then(Value::as_str)
36}
37
38pub struct Validator<'a> {
40 input: &'a Value,
41 errors: Vec<PluginFieldError>,
42}
43
44impl<'a> Validator<'a> {
45 #[must_use]
46 pub fn new(input: &'a PluginInput) -> Self {
47 Self {
48 input,
49 errors: Vec::new(),
50 }
51 }
52
53 fn push_opt(&mut self, key: &str, err: Option<(&str, String)>) {
54 if let Some((code, message)) = err {
55 self.errors.push(PluginFieldError {
56 field: format!("/{key}"),
57 code: code.to_owned(),
58 message,
59 });
60 }
61 }
62
63 pub fn require_str(&mut self, key: &str) -> &mut Self {
65 let err = match present(self.input, key) {
66 None => Some(("missing", format!("{key} is required"))),
67 Some(v) => match v.as_str() {
68 Some(s) if !s.trim().is_empty() => None,
69 Some(_) => Some(("empty", format!("{key} must not be empty"))),
70 None => Some(("type", format!("{key} must be a string"))),
71 },
72 };
73 self.push_opt(key, err);
74 self
75 }
76
77 pub fn require_enum(&mut self, key: &str, allowed: &[&str]) -> &mut Self {
79 let err = match present(self.input, key).and_then(Value::as_str) {
80 None => Some(("missing", format!("{key} is required"))),
81 Some(s) if allowed.contains(&s) => None,
82 Some(_) => Some(("out_of_range", format!("{key} must be one of {allowed:?}"))),
83 };
84 self.push_opt(key, err);
85 self
86 }
87
88 pub fn require_gtin(&mut self, key: &str) -> &mut Self {
90 let err = match present(self.input, key).and_then(Value::as_str) {
91 None => Some(("missing", format!("{key} is required"))),
92 Some(g) if g.len() == 14 && g.bytes().all(|b| b.is_ascii_digit()) => {
93 if gs1_check_digit_valid(g) {
94 None
95 } else {
96 Some(("checksum", format!("{key} has an invalid GS1 check digit")))
97 }
98 }
99 Some(_) => Some(("format", format!("{key} must be 14 digits"))),
100 };
101 self.push_opt(key, err);
102 self
103 }
104
105 pub fn require_country(&mut self, key: &str) -> &mut Self {
107 let err = match present(self.input, key).and_then(Value::as_str) {
108 None => Some(("missing", format!("{key} is required"))),
109 Some(c) if c.len() == 2 && c.bytes().all(|b| b.is_ascii_uppercase()) => {
110 if ISO_3166_1_A2.binary_search(&c).is_ok() {
111 None
112 } else {
113 Some((
114 "invalid",
115 format!("{key} is not a recognized ISO 3166-1 alpha-2 code"),
116 ))
117 }
118 }
119 Some(_) => Some((
120 "format",
121 format!("{key} must be a 2-letter uppercase country code"),
122 )),
123 };
124 self.push_opt(key, err);
125 self
126 }
127
128 pub fn require_positive(&mut self, key: &str) -> &mut Self {
130 let err = match num(self.input, key) {
131 None => Some((
132 "missing",
133 format!("{key} is required and must be a finite number"),
134 )),
135 Some(v) if v <= 0.0 => Some(("out_of_range", format!("{key} must be greater than 0"))),
136 Some(_) => None,
137 };
138 self.push_opt(key, err);
139 self
140 }
141
142 pub fn require_non_negative(&mut self, key: &str) -> &mut Self {
144 let err = match num(self.input, key) {
145 None => Some((
146 "missing",
147 format!("{key} is required and must be a finite number"),
148 )),
149 Some(v) if v < 0.0 => Some(("out_of_range", format!("{key} must be 0 or greater"))),
150 Some(_) => None,
151 };
152 self.push_opt(key, err);
153 self
154 }
155
156 pub fn require_pct(&mut self, key: &str) -> &mut Self {
158 let err = match num(self.input, key) {
159 None => Some((
160 "missing",
161 format!("{key} is required and must be a number in 0..=100"),
162 )),
163 Some(v) if !(0.0..=100.0).contains(&v) => {
164 Some(("out_of_range", format!("{key} must be in 0..=100")))
165 }
166 Some(_) => None,
167 };
168 self.push_opt(key, err);
169 self
170 }
171
172 pub fn require_positive_int(&mut self, key: &str) -> &mut Self {
174 let err = match present(self.input, key).and_then(Value::as_u64) {
175 None => Some((
176 "missing",
177 format!("{key} is required and must be a non-negative integer"),
178 )),
179 Some(0) => Some(("out_of_range", format!("{key} must be at least 1"))),
180 Some(_) => None,
181 };
182 self.push_opt(key, err);
183 self
184 }
185
186 pub fn require_bool(&mut self, key: &str) -> &mut Self {
188 let err = match present(self.input, key) {
189 None => Some(("missing", format!("{key} is required"))),
190 Some(v) if v.is_boolean() => None,
191 Some(_) => Some(("type", format!("{key} must be a boolean"))),
192 };
193 self.push_opt(key, err);
194 self
195 }
196
197 pub fn require_non_empty_array(&mut self, key: &str) -> &mut Self {
199 let err = match present(self.input, key).and_then(Value::as_array) {
200 None => Some(("missing", format!("{key} is required and must be an array"))),
201 Some(a) if a.is_empty() => Some(("empty", format!("{key} must not be empty"))),
202 Some(_) => None,
203 };
204 self.push_opt(key, err);
205 self
206 }
207
208 pub fn optional_pct(&mut self, key: &str) -> &mut Self {
210 let err = match present(self.input, key) {
211 None => None,
212 Some(v) => match v.as_f64().filter(|n| n.is_finite()) {
213 Some(n) if (0.0..=100.0).contains(&n) => None,
214 _ => Some(("out_of_range", format!("{key} must be a number in 0..=100"))),
215 },
216 };
217 self.push_opt(key, err);
218 self
219 }
220
221 pub fn finish(&mut self) -> Result<(), PluginError> {
223 if self.errors.is_empty() {
224 Ok(())
225 } else {
226 Err(PluginError::ValidationErrors(std::mem::take(
227 &mut self.errors,
228 )))
229 }
230 }
231}
232
233fn gs1_check_digit_valid(gtin: &str) -> bool {
238 let bytes = gtin.as_bytes();
239 debug_assert_eq!(bytes.len(), 14, "caller must check length == 14 first");
240 let sum: u32 = bytes[..13]
241 .iter()
242 .enumerate()
243 .map(|(i, &b)| {
244 let d = (b - b'0') as u32;
245 if i % 2 == 0 { d * 3 } else { d }
246 })
247 .sum();
248 let expected = (10 - sum % 10) % 10;
249 expected == (bytes[13] - b'0') as u32
250}
251
252const ISO_3166_1_A2: &[&str] = &[
254 "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
255 "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS",
256 "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
257 "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE",
258 "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF",
259 "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
260 "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM",
261 "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC",
262 "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK",
263 "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA",
264 "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG",
265 "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
266 "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS",
267 "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO",
268 "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
269 "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW",
270];
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use serde_json::json;
276
277 #[test]
278 fn collects_all_failures() {
279 let input = json!({ "gtin": "12-34", "voltage": -1.0 });
280 let err = Validator::new(&input)
281 .require_gtin("gtin")
282 .require_positive("voltage")
283 .require_str("name")
284 .finish()
285 .unwrap_err();
286 match err {
287 PluginError::ValidationErrors(errs) => assert_eq!(errs.len(), 3),
288 other => panic!("expected ValidationErrors, got {other:?}"),
289 }
290 }
291
292 #[test]
293 fn valid_input_passes() {
294 let input = json!({
295 "gtin": "12345678901231",
296 "country": "DE",
297 "pct": 42.0,
298 "count": 3,
299 "flag": true,
300 "items": [1]
301 });
302 assert!(
303 Validator::new(&input)
304 .require_gtin("gtin")
305 .require_country("country")
306 .require_pct("pct")
307 .require_positive_int("count")
308 .require_bool("flag")
309 .require_non_empty_array("items")
310 .finish()
311 .is_ok()
312 );
313 }
314
315 #[test]
316 fn enum_and_country_and_pct_bounds() {
317 let input = json!({ "cls": "Z", "country": "de", "pct": 150.0 });
318 let err = Validator::new(&input)
319 .require_enum("cls", &["A", "B"])
320 .require_country("country")
321 .require_pct("pct")
322 .finish()
323 .unwrap_err();
324 match err {
325 PluginError::ValidationErrors(errs) => assert_eq!(errs.len(), 3),
326 other => panic!("expected ValidationErrors, got {other:?}"),
327 }
328 }
329
330 #[test]
331 fn gtin_invalid_check_digit_is_rejected() {
332 let input = json!({ "gtin": "12345678901234" });
334 let err = Validator::new(&input)
335 .require_gtin("gtin")
336 .finish()
337 .unwrap_err();
338 match err {
339 PluginError::ValidationErrors(errs) => {
340 assert_eq!(errs.len(), 1);
341 assert_eq!(errs[0].code, "checksum");
342 }
343 other => panic!("expected ValidationErrors, got {other:?}"),
344 }
345 }
346
347 #[test]
348 fn gtin_valid_check_digit_passes() {
349 let input = json!({ "gtin": "12345678901231" });
351 assert!(Validator::new(&input).require_gtin("gtin").finish().is_ok());
352 }
353
354 #[test]
355 fn country_not_in_iso_list_is_rejected() {
356 let input = json!({ "country": "XX" });
358 let err = Validator::new(&input)
359 .require_country("country")
360 .finish()
361 .unwrap_err();
362 match err {
363 PluginError::ValidationErrors(errs) => {
364 assert_eq!(errs.len(), 1);
365 assert_eq!(errs[0].code, "invalid");
366 }
367 other => panic!("expected ValidationErrors, got {other:?}"),
368 }
369 }
370
371 #[test]
372 fn country_valid_iso_code_passes() {
373 for code in ["DE", "NO", "FR", "US", "JP"] {
374 let input = json!({ "country": code });
375 assert!(
376 Validator::new(&input)
377 .require_country("country")
378 .finish()
379 .is_ok(),
380 "{code} should be a valid ISO 3166-1 alpha-2 code"
381 );
382 }
383 }
384
385 #[test]
386 fn optional_pct_absent_is_ok_present_out_of_range_fails() {
387 let ok = json!({});
388 assert!(Validator::new(&ok).optional_pct("x").finish().is_ok());
389 let bad = json!({ "x": 101.0 });
390 assert!(Validator::new(&bad).optional_pct("x").finish().is_err());
391 }
392
393 #[test]
394 fn readers_extract_values() {
395 let input = json!({ "n": 3.5, "s": "hi", "bad": "x" });
396 assert_eq!(num(&input, "n"), Some(3.5));
397 assert_eq!(num(&input, "bad"), None);
398 assert_eq!(str_of(&input, "s"), Some("hi"));
399 }
400}