1use dpp_plugin_traits::{PluginComplianceStatus, PluginError, PluginFieldError, PluginInput};
14use dpp_rules::common::identifier::{DidRejection, check_did, is_absolute_web_url};
15use serde_json::Value;
16
17fn present<'a>(input: &'a Value, key: &str) -> Option<&'a Value> {
19 match input.get(key) {
20 Some(Value::Null) | None => None,
21 other => other,
22 }
23}
24
25#[must_use]
27pub fn num(input: &PluginInput, key: &str) -> Option<f64> {
28 input
29 .get(key)
30 .and_then(Value::as_f64)
31 .filter(|n| n.is_finite())
32}
33
34#[must_use]
36pub fn str_of<'a>(input: &'a PluginInput, key: &str) -> Option<&'a str> {
37 input.get(key).and_then(Value::as_str)
38}
39
40#[must_use]
44pub fn threshold_status(value: Option<f64>, threshold: f64) -> PluginComplianceStatus {
45 if value.is_some_and(|v| v <= threshold) {
46 PluginComplianceStatus::Compliant
47 } else {
48 PluginComplianceStatus::NonCompliant
49 }
50}
51
52pub struct Validator<'a> {
54 input: &'a Value,
55 errors: Vec<PluginFieldError>,
56}
57
58impl<'a> Validator<'a> {
59 #[must_use]
60 pub fn new(input: &'a PluginInput) -> Self {
61 Self {
62 input,
63 errors: Vec::new(),
64 }
65 }
66
67 fn push_opt(&mut self, key: &str, err: Option<(&str, String)>) {
68 if let Some((code, message)) = err {
69 self.errors.push(PluginFieldError {
70 field: format!("/{key}"),
71 code: code.to_owned(),
72 message,
73 });
74 }
75 }
76
77 fn push_at(&mut self, field: String, code: &str, message: String) {
83 self.errors.push(PluginFieldError {
84 field,
85 code: code.to_owned(),
86 message,
87 });
88 }
89
90 pub fn require_str(&mut self, key: &str) -> &mut Self {
92 let err = match present(self.input, key) {
93 None => Some(("missing", format!("{key} is required"))),
94 Some(v) => match v.as_str() {
95 Some(s) if !s.trim().is_empty() => None,
96 Some(_) => Some(("empty", format!("{key} must not be empty"))),
97 None => Some(("type", format!("{key} must be a string"))),
98 },
99 };
100 self.push_opt(key, err);
101 self
102 }
103
104 pub fn require_enum(&mut self, key: &str, allowed: &[&str]) -> &mut Self {
106 let err = match present(self.input, key).and_then(Value::as_str) {
107 None => Some(("missing", format!("{key} is required"))),
108 Some(s) if allowed.contains(&s) => None,
109 Some(_) => Some(("out_of_range", format!("{key} must be one of {allowed:?}"))),
110 };
111 self.push_opt(key, err);
112 self
113 }
114
115 pub fn require_gtin(&mut self, key: &str) -> &mut Self {
117 let err = match present(self.input, key).and_then(Value::as_str) {
118 None => Some(("missing", format!("{key} is required"))),
119 Some(g) if g.len() == 14 && g.bytes().all(|b| b.is_ascii_digit()) => {
120 if gs1_check_digit_valid(g) {
121 None
122 } else {
123 Some(("checksum", format!("{key} has an invalid GS1 check digit")))
124 }
125 }
126 Some(_) => Some(("format", format!("{key} must be 14 digits"))),
127 };
128 self.push_opt(key, err);
129 self
130 }
131
132 pub fn require_product_identifier(&mut self, key: &str) -> &mut Self {
162 let Some(object) = present(self.input, key).and_then(Value::as_object) else {
163 self.push_opt(key, Some(("missing", format!("{key} is required"))));
164 return self;
165 };
166 let Some(scheme) = object.get("scheme").and_then(Value::as_str) else {
167 self.push_at(
168 format!("/{key}/scheme"),
169 "missing",
170 format!("{key}.scheme is required"),
171 );
172 return self;
173 };
174 let (field, err) = match scheme {
178 "gs1" => (
179 "gtin",
180 match object.get("gtin").and_then(Value::as_str) {
181 None => Some(("missing", format!("{key}.gtin is required for scheme gs1"))),
182 Some(g) if g.len() == 14 && g.bytes().all(|b| b.is_ascii_digit()) => {
183 if gs1_check_digit_valid(g) {
184 None
185 } else {
186 Some((
187 "checksum",
188 format!("{key}.gtin has an invalid GS1 check digit"),
189 ))
190 }
191 }
192 Some(_) => Some(("format", format!("{key}.gtin must be 14 digits"))),
193 },
194 ),
195 "identificationLink" => (
196 "url",
197 match object.get("url").and_then(Value::as_str) {
198 None => Some((
199 "missing",
200 format!("{key}.url is required for scheme identificationLink"),
201 )),
202 Some(u) if is_absolute_web_url(u) => None,
206 Some(_) => Some(("format", format!("{key}.url must be an absolute URL"))),
207 },
208 ),
209 "did" => (
210 "did",
211 match object.get("did").and_then(Value::as_str) {
212 None => Some(("missing", format!("{key}.did is required for scheme did"))),
213 Some(d) => match check_did(d) {
216 Ok(()) => None,
217 Err(DidRejection::UnsupportedMethod(method)) => Some((
218 "format",
219 format!(
220 "{key}.did method '{method}' is not did:web, did:ethr or did:ebsi"
221 ),
222 )),
223 Err(DidRejection::EmptyMethodId) => Some((
224 "format",
225 format!("{key}.did names a method but no identifier"),
226 )),
227 Err(DidRejection::Malformed) => {
228 Some(("format", format!("{key}.did is not a well-formed W3C DID")))
229 }
230 },
231 },
232 ),
233 other => {
234 self.push_at(
235 format!("/{key}/scheme"),
236 "unknown",
237 format!("{key}.scheme '{other}' is not an EN 18219 clause 5 scheme"),
238 );
239 return self;
240 }
241 };
242 if let Some((code, message)) = err {
243 self.push_at(format!("/{key}/{field}"), code, message);
244 }
245 self
246 }
247
248 pub fn require_country(&mut self, key: &str) -> &mut Self {
250 let err = match present(self.input, key).and_then(Value::as_str) {
251 None => Some(("missing", format!("{key} is required"))),
252 Some(c) if c.len() == 2 && c.bytes().all(|b| b.is_ascii_uppercase()) => {
253 if dpp_rules::country_code_valid(c) {
254 None
255 } else {
256 Some((
257 "invalid",
258 format!("{key} is not a recognized ISO 3166-1 alpha-2 code"),
259 ))
260 }
261 }
262 Some(_) => Some((
263 "format",
264 format!("{key} must be a 2-letter uppercase country code"),
265 )),
266 };
267 self.push_opt(key, err);
268 self
269 }
270
271 pub fn require_positive(&mut self, key: &str) -> &mut Self {
273 let err = match num(self.input, key) {
274 None => Some((
275 "missing",
276 format!("{key} is required and must be a finite number"),
277 )),
278 Some(v) if v <= 0.0 => Some(("out_of_range", format!("{key} must be greater than 0"))),
279 Some(_) => None,
280 };
281 self.push_opt(key, err);
282 self
283 }
284
285 pub fn require_non_negative(&mut self, key: &str) -> &mut Self {
287 let err = match num(self.input, key) {
288 None => Some((
289 "missing",
290 format!("{key} is required and must be a finite number"),
291 )),
292 Some(v) if v < 0.0 => Some(("out_of_range", format!("{key} must be 0 or greater"))),
293 Some(_) => None,
294 };
295 self.push_opt(key, err);
296 self
297 }
298
299 pub fn require_pct(&mut self, key: &str) -> &mut Self {
301 let err = match num(self.input, key) {
302 None => Some((
303 "missing",
304 format!("{key} is required and must be a number in 0..=100"),
305 )),
306 Some(v) if !(0.0..=100.0).contains(&v) => {
307 Some(("out_of_range", format!("{key} must be in 0..=100")))
308 }
309 Some(_) => None,
310 };
311 self.push_opt(key, err);
312 self
313 }
314
315 pub fn require_positive_int(&mut self, key: &str) -> &mut Self {
317 let err = match present(self.input, key).and_then(Value::as_u64) {
318 None => Some((
319 "missing",
320 format!("{key} is required and must be a non-negative integer"),
321 )),
322 Some(0) => Some(("out_of_range", format!("{key} must be at least 1"))),
323 Some(_) => None,
324 };
325 self.push_opt(key, err);
326 self
327 }
328
329 pub fn optional_positive_int(&mut self, key: &str) -> &mut Self {
337 let err = match present(self.input, key) {
338 None => None,
339 Some(v) => match v.as_u64() {
340 None => Some((
341 "invalid_type",
342 format!("{key} must be a non-negative integer"),
343 )),
344 Some(0) => Some(("out_of_range", format!("{key} must be at least 1"))),
345 Some(_) => None,
346 },
347 };
348 self.push_opt(key, err);
349 self
350 }
351
352 pub fn require_bool(&mut self, key: &str) -> &mut Self {
354 let err = match present(self.input, key) {
355 None => Some(("missing", format!("{key} is required"))),
356 Some(v) if v.is_boolean() => None,
357 Some(_) => Some(("type", format!("{key} must be a boolean"))),
358 };
359 self.push_opt(key, err);
360 self
361 }
362
363 pub fn require_non_empty_array(&mut self, key: &str) -> &mut Self {
365 let err = match present(self.input, key).and_then(Value::as_array) {
366 None => Some(("missing", format!("{key} is required and must be an array"))),
367 Some(a) if a.is_empty() => Some(("empty", format!("{key} must not be empty"))),
368 Some(_) => None,
369 };
370 self.push_opt(key, err);
371 self
372 }
373
374 pub fn require_object(&mut self, key: &str) -> &mut Self {
387 let err = match present(self.input, key).and_then(Value::as_object) {
388 None => Some((
389 "missing",
390 format!("{key} is required and must be an object"),
391 )),
392 Some(o) if o.is_empty() => Some(("empty", format!("{key} must not be empty"))),
393 Some(_) => None,
394 };
395 self.push_opt(key, err);
396 self
397 }
398
399 pub fn optional_pct(&mut self, key: &str) -> &mut Self {
401 let err = match present(self.input, key) {
402 None => None,
403 Some(v) => match v.as_f64().filter(|n| n.is_finite()) {
404 Some(n) if (0.0..=100.0).contains(&n) => None,
405 _ => Some(("out_of_range", format!("{key} must be a number in 0..=100"))),
406 },
407 };
408 self.push_opt(key, err);
409 self
410 }
411
412 pub fn optional_range(&mut self, key: &str, min: f64, max: f64) -> &mut Self {
418 let err = match present(self.input, key) {
419 None => None,
420 Some(v) => match v.as_f64().filter(|n| n.is_finite()) {
421 Some(n) if (min..=max).contains(&n) => None,
422 _ => Some((
423 "out_of_range",
424 format!("{key} must be a number in {min}..={max}"),
425 )),
426 },
427 };
428 self.push_opt(key, err);
429 self
430 }
431
432 pub fn optional_non_negative(&mut self, key: &str) -> &mut Self {
434 let err = match present(self.input, key) {
435 None => None,
436 Some(v) => match v.as_f64().filter(|n| n.is_finite()) {
437 Some(n) if n >= 0.0 => None,
438 _ => Some(("out_of_range", format!("{key} must be a finite number ≥ 0"))),
439 },
440 };
441 self.push_opt(key, err);
442 self
443 }
444
445 pub fn finish(&mut self) -> Result<(), PluginError> {
447 if self.errors.is_empty() {
448 Ok(())
449 } else {
450 Err(PluginError::ValidationErrors(std::mem::take(
451 &mut self.errors,
452 )))
453 }
454 }
455}
456
457fn gs1_check_digit_valid(gtin: &str) -> bool {
462 let bytes = gtin.as_bytes();
463 debug_assert_eq!(bytes.len(), 14, "caller must check length == 14 first");
464 let sum: u32 = bytes[..13]
465 .iter()
466 .enumerate()
467 .map(|(i, &b)| {
468 let d = (b - b'0') as u32;
469 if i % 2 == 0 { d * 3 } else { d }
470 })
471 .sum();
472 let expected = (10 - sum % 10) % 10;
473 expected == (bytes[13] - b'0') as u32
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use serde_json::json;
480
481 #[test]
482 fn collects_all_failures() {
483 let input = json!({ "gtin": "12-34", "voltage": -1.0 });
484 let err = Validator::new(&input)
485 .require_gtin("gtin")
486 .require_positive("voltage")
487 .require_str("name")
488 .finish()
489 .unwrap_err();
490 match err {
491 PluginError::ValidationErrors(errs) => assert_eq!(errs.len(), 3),
492 other => panic!("expected ValidationErrors, got {other:?}"),
493 }
494 }
495
496 #[test]
497 fn valid_input_passes() {
498 let input = json!({
499 "gtin": "12345678901231",
500 "country": "DE",
501 "pct": 42.0,
502 "count": 3,
503 "flag": true,
504 "items": [1]
505 });
506 assert!(
507 Validator::new(&input)
508 .require_gtin("gtin")
509 .require_country("country")
510 .require_pct("pct")
511 .require_positive_int("count")
512 .require_bool("flag")
513 .require_non_empty_array("items")
514 .finish()
515 .is_ok()
516 );
517 }
518
519 #[test]
520 fn enum_and_country_and_pct_bounds() {
521 let input = json!({ "cls": "Z", "country": "de", "pct": 150.0 });
522 let err = Validator::new(&input)
523 .require_enum("cls", &["A", "B"])
524 .require_country("country")
525 .require_pct("pct")
526 .finish()
527 .unwrap_err();
528 match err {
529 PluginError::ValidationErrors(errs) => assert_eq!(errs.len(), 3),
530 other => panic!("expected ValidationErrors, got {other:?}"),
531 }
532 }
533
534 #[test]
535 fn gtin_invalid_check_digit_is_rejected() {
536 let input = json!({ "gtin": "12345678901234" });
538 let err = Validator::new(&input)
539 .require_gtin("gtin")
540 .finish()
541 .unwrap_err();
542 match err {
543 PluginError::ValidationErrors(errs) => {
544 assert_eq!(errs.len(), 1);
545 assert_eq!(errs[0].code, "checksum");
546 }
547 other => panic!("expected ValidationErrors, got {other:?}"),
548 }
549 }
550
551 #[test]
552 fn gtin_valid_check_digit_passes() {
553 let input = json!({ "gtin": "12345678901231" });
555 assert!(Validator::new(&input).require_gtin("gtin").finish().is_ok());
556 }
557
558 #[test]
559 fn country_not_in_iso_list_is_rejected() {
560 let input = json!({ "country": "XX" });
562 let err = Validator::new(&input)
563 .require_country("country")
564 .finish()
565 .unwrap_err();
566 match err {
567 PluginError::ValidationErrors(errs) => {
568 assert_eq!(errs.len(), 1);
569 assert_eq!(errs[0].code, "invalid");
570 }
571 other => panic!("expected ValidationErrors, got {other:?}"),
572 }
573 }
574
575 #[test]
576 fn country_valid_iso_code_passes() {
577 for code in ["DE", "NO", "FR", "US", "JP"] {
578 let input = json!({ "country": code });
579 assert!(
580 Validator::new(&input)
581 .require_country("country")
582 .finish()
583 .is_ok(),
584 "{code} should be a valid ISO 3166-1 alpha-2 code"
585 );
586 }
587 }
588
589 #[test]
590 fn optional_pct_absent_is_ok_present_out_of_range_fails() {
591 let ok = json!({});
592 assert!(Validator::new(&ok).optional_pct("x").finish().is_ok());
593 let bad = json!({ "x": 101.0 });
594 assert!(Validator::new(&bad).optional_pct("x").finish().is_err());
595 }
596
597 #[test]
598 fn optional_range_absent_ok_present_bounded() {
599 assert!(
601 Validator::new(&json!({}))
602 .optional_range("s", 0.0, 10.0)
603 .finish()
604 .is_ok()
605 );
606 assert!(
608 Validator::new(&json!({ "s": 6.0 }))
609 .optional_range("s", 0.0, 10.0)
610 .finish()
611 .is_ok()
612 );
613 for bad in [json!({ "s": 999999.0 }), json!({ "s": -1.0 })] {
615 assert!(
616 Validator::new(&bad)
617 .optional_range("s", 0.0, 10.0)
618 .finish()
619 .is_err()
620 );
621 }
622 }
623
624 #[test]
625 fn optional_non_negative_absent_ok_negative_fails() {
626 assert!(
627 Validator::new(&json!({}))
628 .optional_non_negative("c")
629 .finish()
630 .is_ok()
631 );
632 assert!(
633 Validator::new(&json!({ "c": 0.0 }))
634 .optional_non_negative("c")
635 .finish()
636 .is_ok()
637 );
638 assert!(
639 Validator::new(&json!({ "c": -999.0 }))
640 .optional_non_negative("c")
641 .finish()
642 .is_err()
643 );
644 }
645
646 #[test]
647 fn readers_extract_values() {
648 let input = json!({ "n": 3.5, "s": "hi", "bad": "x" });
649 assert_eq!(num(&input, "n"), Some(3.5));
650 assert_eq!(num(&input, "bad"), None);
651 assert_eq!(str_of(&input, "s"), Some("hi"));
652 }
653}