skp_validator_rules/custom/
contextual.rs1use skp_validator_core::{Rule, ValidationContext, ValidationErrors, ValidationError, ValidationResult};
4use std::sync::Arc;
5
6pub type ContextualCondition = Arc<dyn Fn(&ValidationContext) -> bool + Send + Sync>;
33
34pub type ContextualValidator<T> = Arc<dyn Fn(&T, &ValidationContext) -> bool + Send + Sync>;
36
37pub struct ContextualRule<T>
38where
39 T: ?Sized,
40{
41 pub rule_name: String,
43 pub condition: Option<ContextualCondition>,
45 pub validator: Option<ContextualValidator<T>>,
47 pub message: Option<String>,
49}
50
51impl<T: ?Sized> ContextualRule<T> {
52 pub fn new(name: impl Into<String>) -> Self {
54 Self {
55 rule_name: name.into(),
56 condition: None,
57 validator: None,
58 message: None,
59 }
60 }
61
62 pub fn when<F>(mut self, condition: F) -> Self
64 where
65 F: Fn(&ValidationContext) -> bool + Send + Sync + 'static,
66 {
67 self.condition = Some(Arc::new(condition));
68 self
69 }
70
71 pub fn validate_with<F>(mut self, validator: F) -> Self
73 where
74 F: Fn(&T, &ValidationContext) -> bool + Send + Sync + 'static,
75 {
76 self.validator = Some(Arc::new(validator));
77 self
78 }
79
80 pub fn message(mut self, msg: impl Into<String>) -> Self {
82 self.message = Some(msg.into());
83 self
84 }
85
86 fn get_message(&self) -> String {
87 self.message.clone().unwrap_or_else(|| {
88 format!("Contextual validation '{}' failed", self.rule_name)
89 })
90 }
91}
92
93impl<T: ?Sized> std::fmt::Debug for ContextualRule<T> {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("ContextualRule")
96 .field("rule_name", &self.rule_name)
97 .field("has_condition", &self.condition.is_some())
98 .field("has_validator", &self.validator.is_some())
99 .finish()
100 }
101}
102
103impl<T: ?Sized + 'static> Rule<T> for ContextualRule<T> {
104 fn validate(&self, value: &T, ctx: &ValidationContext) -> ValidationResult<()> {
105 if let Some(ref condition) = self.condition
107 && !condition(ctx)
108 {
109 return Ok(());
111 }
112
113 if let Some(ref validator) = self.validator
115 && !validator(value, ctx)
116 {
117 return Err(ValidationErrors::from_iter([
118 ValidationError::root(&self.rule_name, self.get_message())
119 ]));
120 }
121
122 Ok(())
123 }
124
125 fn name(&self) -> &'static str {
126 "contextual"
127 }
128
129 fn default_message(&self) -> String {
130 self.get_message()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_contextual_applies() {
140 let rule = ContextualRule::<str>::new("strict")
141 .when(|ctx| ctx.get_meta("mode") == Some("strict"))
142 .validate_with(|value: &str, _ctx| value.len() >= 5);
143
144 let strict_ctx = ValidationContext::new().with_meta("mode", "strict");
145
146 assert!(rule.validate("hello", &strict_ctx).is_ok());
147 assert!(rule.validate("hi", &strict_ctx).is_err());
148 }
149
150 #[test]
151 fn test_contextual_skips() {
152 let rule = ContextualRule::<str>::new("strict")
153 .when(|ctx| ctx.get_meta("mode") == Some("strict"))
154 .validate_with(|value: &str, _ctx| value.len() >= 5);
155
156 let normal_ctx = ValidationContext::new();
157
158 assert!(rule.validate("hi", &normal_ctx).is_ok());
160 }
161}