par_validator/builder.rs
1//! Key-path–driven validation builder ([`Rule`]).
2//!
3//! Each rule is a **`fn` predicate** paired with an error value `E`. The predicate returns
4//! **`true` if the field is valid**; `false` means failure and the paired `E` is returned from
5//! [`Rule::apply`].
6//!
7//! For loading rule **metadata** (which field, which predicate, error code) from a CSV catalog, see
8//! the `rule_csv_catalog` example and `examples/data/validation_catalog.csv` in the crate source.
9//!
10//! # Minimal runnable example
11//!
12//! From the crate directory: `cargo run --example basics` — see `examples/basics.rs`.
13
14use std::fmt::Debug;
15
16use rayon::prelude::*;
17use rust_key_paths::{AccessorTrait, KpType};
18
19/// Binds a [`KpType`] (from `#[derive(Kp)]`) to a root value and a list of validation predicates.
20///
21/// Each rule is a **`bool` predicate** plus an **`E`** value returned when the predicate is
22/// **`false`**.
23///
24/// # Semantics
25///
26/// - **Mandatory** rules run in order. The first predicate that returns **`false`** stops the run;
27/// [`apply`](Self::apply) returns `vec![that rule’s E]`.
28/// - **Non-mandatory** rules run **in parallel** (Rayon). Each predicate that returns **`false`**
29/// contributes its `E`; passing rules produce no entry. The result may be **empty** if all pass.
30///
31/// # Examples
32///
33/// ```
34/// use key_paths_derive::Kp;
35/// use par_validator::builder::Rule;
36///
37/// #[derive(Kp)]
38/// struct Invoice {
39/// reference: String,
40/// }
41///
42/// fn non_blank(r: Option<&String>) -> bool {
43/// r.map(|s| !s.trim().is_empty()).unwrap_or(false)
44/// }
45///
46/// fn len_at_most_8(r: Option<&String>) -> bool {
47/// r.map(|s| s.len() <= 8).unwrap_or(false)
48/// }
49///
50/// let inv = Invoice {
51/// reference: "INV-01".into(),
52/// };
53///
54/// let failures: Vec<&'static str> = Rule::new(Invoice::reference())
55/// .with_root(&inv)
56/// .mandatory_rule(non_blank, "reference_blank")
57/// .rule(len_at_most_8, "reference_too_long")
58/// .apply();
59///
60/// assert!(failures.is_empty());
61///
62/// let bad = Invoice {
63/// reference: "TOO_LONG_REFERENCE".into(),
64/// };
65/// let failures = Rule::new(Invoice::reference())
66/// .with_root(&bad)
67/// .mandatory_rule(non_blank, "reference_blank")
68/// .rule(len_at_most_8, "reference_too_long")
69/// .apply();
70///
71/// assert_eq!(failures, vec!["reference_too_long"]);
72/// ```
73pub struct Rule<'a, R, V, E: PartialEq + Eq + Send + Sync> {
74 root: Option<&'a R>,
75 kp: KpType<'a, R, V>,
76 mandatory_rules: Vec<(fn(Option<&'a V>) -> bool, E)>,
77 rules: Vec<(fn(Option<&'a V>) -> bool, E)>,
78}
79
80impl<'a, R, V, E> Rule<'a, R, V, E>
81where
82 E: Debug + Clone + 'static + PartialEq + Eq + Send + Sync,
83 R: Sync,
84 V: Sync,
85{
86 /// Starts a builder for the given statically dispatched key path (`#[derive(Kp)]` fields).
87 pub fn new(kp: KpType<'a, R, V>) -> Self {
88 Self {
89 root: None,
90 kp,
91 rules: vec![],
92 mandatory_rules: vec![],
93 }
94 }
95
96 /// Supply the struct instance `root` that the key path reads from.
97 pub fn with_root(mut self, root: &'a R) -> Self {
98 self.root = Some(root);
99 self
100 }
101
102 /// Append a rule executed **in parallel** with other non-mandatory rules when you call
103 /// [`apply`](Self::apply).
104 pub fn rule(mut self, f: fn(Option<&'a V>) -> bool, e: E) -> Self {
105 self.rules.push((f, e));
106 self
107 }
108
109 /// Rule that runs **before** parallel rules, in order. On the first predicate that returns
110 /// **`false`**, [`apply`](Self::apply) returns `vec![e]` for that rule’s error value `e`.
111 pub fn mandatory_rule(mut self, f: fn(Option<&'a V>) -> bool, e: E) -> Self {
112 self.mandatory_rules.push((f, e));
113 self
114 }
115
116 /// Resolves `Option<&V>` via the key path, runs mandatory rules, then runs remaining rules on
117 /// a Rayon thread pool.
118 pub fn apply(&self) -> Vec<E> {
119 let val = self.kp.get_optional(self.root);
120 for rule in self.mandatory_rules.iter() {
121 if !rule.0(val) {
122 return vec![rule.1.clone()];
123 }
124 }
125 self.rules
126 .par_iter()
127 .filter_map(|rule| if rule.0(val) { None } else { Some(rule.1.clone()) })
128 .collect()
129 }
130}