skp_validator_rules/custom/
dependency.rs1use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4use std::sync::Arc;
5
6#[derive(Debug, Clone)]
8pub enum DependencyCondition {
9 Present,
11 Absent,
13 Equals(String),
15 NotEquals(String),
17 In(Vec<String>),
19 NotIn(Vec<String>),
21}
22
23pub type DependencyValidator<T> = Arc<dyn Fn(&T, &ValidationContext) -> bool + Send + Sync>;
25
26pub struct DependencyRule<T>
40where
41 T: ?Sized,
42{
43 pub field_name: String,
45 pub depends_on_field: Option<String>,
47 pub condition: Option<DependencyCondition>,
49 pub validator: Option<DependencyValidator<T>>,
51 pub required_when_met: bool,
53 pub message: Option<String>,
55}
56
57impl<T: ?Sized> DependencyRule<T> {
58 pub fn new(field_name: impl Into<String>) -> Self {
60 Self {
61 field_name: field_name.into(),
62 depends_on_field: None,
63 condition: None,
64 validator: None,
65 required_when_met: false,
66 message: None,
67 }
68 }
69
70 pub fn depends_on(mut self, field: impl Into<String>, condition: DependencyCondition) -> Self {
72 self.depends_on_field = Some(field.into());
73 self.condition = Some(condition);
74 self
75 }
76
77 pub fn then_required(mut self) -> Self {
79 self.required_when_met = true;
80 self
81 }
82
83 pub fn then_validate<F>(mut self, validator: F) -> Self
85 where
86 F: Fn(&T, &ValidationContext) -> bool + Send + Sync + 'static,
87 {
88 self.validator = Some(Arc::new(validator));
89 self
90 }
91
92 pub fn message(mut self, msg: impl Into<String>) -> Self {
94 self.message = Some(msg.into());
95 self
96 }
97
98 fn get_message(&self) -> String {
99 self.message.clone().unwrap_or_else(|| {
100 if let Some(ref dep_field) = self.depends_on_field {
101 format!("Field '{}' is required based on '{}'", self.field_name, dep_field)
102 } else {
103 format!("Field '{}' validation failed", self.field_name)
104 }
105 })
106 }
107
108 fn check_condition(&self, ctx: &ValidationContext) -> bool {
110 let Some(ref dep_field) = self.depends_on_field else {
111 return false;
112 };
113
114 let Some(ref condition) = self.condition else {
115 return false;
116 };
117
118 let dep_value = ctx.get_string(dep_field);
119
120 match condition {
121 DependencyCondition::Present => dep_value.is_some_and(|v| !v.is_empty()),
122 DependencyCondition::Absent => dep_value.is_none_or(|v| v.is_empty()),
123 DependencyCondition::Equals(expected) => dep_value.is_some_and(|v| v == expected),
124 DependencyCondition::NotEquals(expected) => dep_value.is_none_or(|v| v != expected),
125 DependencyCondition::In(values) => {
126 dep_value.is_some_and(|v| values.iter().any(|expected| v == expected))
127 }
128 DependencyCondition::NotIn(values) => {
129 dep_value.is_none_or(|v| !values.iter().any(|expected| v == expected))
130 }
131 }
132 }
133}
134
135impl<T: ?Sized> std::fmt::Debug for DependencyRule<T> {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("DependencyRule")
138 .field("field_name", &self.field_name)
139 .field("depends_on_field", &self.depends_on_field)
140 .field("condition", &self.condition)
141 .field("required_when_met", &self.required_when_met)
142 .finish()
143 }
144}
145
146impl Rule<str> for DependencyRule<str> {
147 fn validate(&self, value: &str, ctx: &ValidationContext) -> ValidationResult<()> {
148 if !self.check_condition(ctx) {
150 return Ok(());
152 }
153
154 if self.required_when_met && value.trim().is_empty() {
156 return Err(ValidationErrors::from_iter([
157 ValidationError::root("dependency.required", self.get_message())
158 ]));
159 }
160
161 if let Some(ref validator) = self.validator
163 && !validator(value, ctx)
164 {
165 return Err(ValidationErrors::from_iter([
166 ValidationError::root("dependency.custom", self.get_message())
167 ]));
168 }
169
170 Ok(())
171 }
172
173 fn name(&self) -> &'static str {
174 "dependency"
175 }
176
177 fn default_message(&self) -> String {
178 self.get_message()
179 }
180}
181
182impl Rule<String> for DependencyRule<String> {
183 fn validate(&self, value: &String, ctx: &ValidationContext) -> ValidationResult<()> {
184 if !self.check_condition(ctx) {
186 return Ok(());
187 }
188
189 if self.required_when_met && value.trim().is_empty() {
190 return Err(ValidationErrors::from_iter([
191 ValidationError::root("dependency.required", self.get_message())
192 ]));
193 }
194
195 if let Some(ref validator) = self.validator
196 && !validator(value, ctx)
197 {
198 return Err(ValidationErrors::from_iter([
199 ValidationError::root("dependency.custom", self.get_message())
200 ]));
201 }
202
203 Ok(())
204 }
205
206 fn name(&self) -> &'static str {
207 "dependency"
208 }
209
210 fn default_message(&self) -> String {
211 self.get_message()
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_depends_on_equals() {
221 let rule = DependencyRule::<str>::new("shipping_address")
222 .depends_on("shipping_method", DependencyCondition::Equals("delivery".to_string()))
223 .then_required();
224
225 let delivery_ctx = ValidationContext::new()
227 .with_field("shipping_method", "delivery");
228
229 assert!(rule.validate("", &delivery_ctx).is_err()); assert!(rule.validate("123 Main St", &delivery_ctx).is_ok());
231
232 let pickup_ctx = ValidationContext::new()
234 .with_field("shipping_method", "pickup");
235
236 assert!(rule.validate("", &pickup_ctx).is_ok()); }
238
239 #[test]
240 fn test_depends_on_present() {
241 let rule = DependencyRule::<str>::new("postal_code")
242 .depends_on("country", DependencyCondition::Present)
243 .then_required();
244
245 let with_country = ValidationContext::new()
246 .with_field("country", "US");
247
248 assert!(rule.validate("", &with_country).is_err());
249
250 let without_country = ValidationContext::new();
251 assert!(rule.validate("", &without_country).is_ok());
252 }
253
254 #[test]
255 fn test_depends_on_in() {
256 let rule = DependencyRule::<str>::new("state")
257 .depends_on("country", DependencyCondition::In(vec!["US".to_string(), "CA".to_string()]))
258 .then_required();
259
260 let us_ctx = ValidationContext::new().with_field("country", "US");
261 assert!(rule.validate("", &us_ctx).is_err());
262
263 let uk_ctx = ValidationContext::new().with_field("country", "UK");
264 assert!(rule.validate("", &uk_ctx).is_ok()); }
266}