Skip to main content

rustrails_model/validations/
custom.rs

1use std::fmt;
2
3use serde_json::Value;
4
5use super::{Validator, ValidatorOptions};
6use crate::errors::Errors;
7
8type CustomValidateFn = dyn Fn(&str, Option<&Value>, &mut Errors) + Send + Sync;
9
10/// Validates an attribute through a caller-provided function.
11pub struct CustomValidator {
12    /// Function invoked during validation.
13    pub validate_fn: Box<CustomValidateFn>,
14    pub(crate) options: ValidatorOptions,
15}
16
17impl fmt::Debug for CustomValidator {
18    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19        formatter
20            .debug_struct("CustomValidator")
21            .field("options", &self.options)
22            .finish()
23    }
24}
25
26impl CustomValidator {
27    /// Creates a new custom validator from `validate_fn`.
28    #[must_use]
29    pub fn new<F>(validate_fn: F) -> Self
30    where
31        F: Fn(&str, Option<&Value>, &mut Errors) + Send + Sync + 'static,
32    {
33        Self {
34            validate_fn: Box::new(validate_fn),
35            options: ValidatorOptions::default(),
36        }
37    }
38
39    crate::validations::impl_common_validator_methods!();
40}
41
42impl Validator for CustomValidator {
43    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
44        (self.validate_fn)(attribute, value, errors);
45    }
46
47    fn name(&self) -> &str {
48        "custom"
49    }
50
51    fn options(&self) -> &ValidatorOptions {
52        &self.options
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use std::collections::HashMap;
59    use std::sync::{
60        Arc,
61        atomic::{AtomicBool, Ordering},
62    };
63
64    use serde_json::json;
65
66    use super::CustomValidator;
67    use crate::{
68        errors::{ErrorType, Errors},
69        validations::{ValidationContext, ValidationSet, Validator},
70    };
71
72    #[test]
73    fn invokes_user_closure() {
74        let called = Arc::new(AtomicBool::new(false));
75        let called_clone = Arc::clone(&called);
76        let validator = CustomValidator::new(move |_attribute, _value, _errors| {
77            called_clone.store(true, Ordering::Relaxed);
78        });
79        let mut errors = Errors::new();
80
81        validator.validate("name", Some(&json!("Alice")), &mut errors);
82
83        assert!(called.load(Ordering::Relaxed));
84    }
85
86    #[test]
87    fn closure_can_add_errors() {
88        let validator = CustomValidator::new(|attribute, _value, errors| {
89            errors.add(
90                attribute,
91                ErrorType::Custom(String::from("custom")),
92                "failed",
93            );
94        });
95        let mut errors = Errors::new();
96
97        validator.validate("name", Some(&json!("Alice")), &mut errors);
98
99        assert_eq!(errors.on("name")[0].message, "failed");
100    }
101
102    #[test]
103    fn validation_set_can_skip_custom_validator_via_options() {
104        let called = Arc::new(AtomicBool::new(false));
105        let called_clone = Arc::clone(&called);
106        let validator = CustomValidator::new(move |_attribute, _value, _errors| {
107            called_clone.store(true, Ordering::Relaxed);
108        })
109        .allow_nil();
110        let mut set = ValidationSet::new();
111        set.add("nickname", validator);
112        let mut errors = Errors::new();
113
114        let _ = set.validate(&|_| None, &mut errors);
115
116        assert!(errors.is_empty());
117        assert!(!called.load(Ordering::Relaxed));
118    }
119
120    #[test]
121    fn closure_receives_attribute_name() {
122        let called = Arc::new(AtomicBool::new(false));
123        let called_clone = Arc::clone(&called);
124        let validator = CustomValidator::new(move |attribute, _value, _errors| {
125            called_clone.store(attribute == "email", Ordering::Relaxed);
126        });
127        let mut errors = Errors::new();
128
129        validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
130
131        assert!(called.load(Ordering::Relaxed));
132    }
133
134    #[test]
135    fn closure_receives_missing_values() {
136        let called = Arc::new(AtomicBool::new(false));
137        let called_clone = Arc::clone(&called);
138        let validator = CustomValidator::new(move |_attribute, value, _errors| {
139            called_clone.store(value.is_none(), Ordering::Relaxed);
140        });
141        let mut errors = Errors::new();
142
143        validator.validate("email", None, &mut errors);
144
145        assert!(called.load(Ordering::Relaxed));
146    }
147
148    #[test]
149    fn allow_blank_skips_blank_values_in_validation_set() {
150        let called = Arc::new(AtomicBool::new(false));
151        let called_clone = Arc::clone(&called);
152        let mut set = ValidationSet::new();
153        set.add(
154            "nickname",
155            CustomValidator::new(move |_attribute, _value, _errors| {
156                called_clone.store(true, Ordering::Relaxed);
157            })
158            .allow_blank(),
159        );
160        let attrs = HashMap::from([("nickname".to_string(), json!("   "))]);
161        let mut errors = Errors::new();
162
163        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
164
165        assert!(errors.is_empty());
166        assert!(!called.load(Ordering::Relaxed));
167    }
168
169    #[test]
170    fn on_context_runs_only_when_matching() {
171        let called = Arc::new(AtomicBool::new(false));
172        let called_clone = Arc::clone(&called);
173        let mut set = ValidationSet::new();
174        set.add(
175            "name",
176            CustomValidator::new(move |_attribute, _value, _errors| {
177                called_clone.store(true, Ordering::Relaxed);
178            })
179            .on(vec![ValidationContext::Update]),
180        );
181        let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
182        let mut errors = Errors::new();
183
184        let _ = set.validate_with_context(
185            &|name| attrs.get(name).cloned(),
186            &mut errors,
187            &ValidationContext::Create,
188        );
189        assert!(!called.load(Ordering::Relaxed));
190
191        let _ = set.validate_with_context(
192            &|name| attrs.get(name).cloned(),
193            &mut errors,
194            &ValidationContext::Update,
195        );
196        assert!(called.load(Ordering::Relaxed));
197    }
198
199    #[test]
200    fn if_condition_false_skips_custom_validator() {
201        let called = Arc::new(AtomicBool::new(false));
202        let called_clone = Arc::clone(&called);
203        let mut set = ValidationSet::new();
204        set.add(
205            "age",
206            CustomValidator::new(move |_attribute, _value, _errors| {
207                called_clone.store(true, Ordering::Relaxed);
208            })
209            .if_cond(|value| value == &json!(21)),
210        );
211        let attrs = HashMap::from([("age".to_string(), json!(18))]);
212        let mut errors = Errors::new();
213
214        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
215
216        assert!(!called.load(Ordering::Relaxed));
217    }
218
219    #[test]
220    fn unless_condition_true_skips_custom_validator() {
221        let called = Arc::new(AtomicBool::new(false));
222        let called_clone = Arc::clone(&called);
223        let mut set = ValidationSet::new();
224        set.add(
225            "age",
226            CustomValidator::new(move |_attribute, _value, _errors| {
227                called_clone.store(true, Ordering::Relaxed);
228            })
229            .unless_cond(|value| value == &json!(18)),
230        );
231        let attrs = HashMap::from([("age".to_string(), json!(18))]);
232        let mut errors = Errors::new();
233
234        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
235
236        assert!(!called.load(Ordering::Relaxed));
237    }
238
239    #[test]
240    fn allow_nil_does_not_skip_present_values() {
241        let called = Arc::new(AtomicBool::new(false));
242        let called_clone = Arc::clone(&called);
243        let mut set = ValidationSet::new();
244        set.add(
245            "name",
246            CustomValidator::new(move |_attribute, _value, _errors| {
247                called_clone.store(true, Ordering::Relaxed);
248            })
249            .allow_nil(),
250        );
251        let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
252        let mut errors = Errors::new();
253
254        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
255
256        assert!(called.load(Ordering::Relaxed));
257    }
258
259    #[test]
260    fn custom_errors_full_message_humanizes_attribute_name() {
261        let validator = CustomValidator::new(|attribute, _value, errors| {
262            errors.add(
263                attribute,
264                ErrorType::Custom(String::from("state")),
265                "is unavailable",
266            );
267        });
268        let mut errors = Errors::new();
269
270        validator.validate("display_name", Some(&json!("Alice")), &mut errors);
271
272        assert_eq!(
273            errors.full_messages(),
274            vec!["Display name is unavailable".to_string()]
275        );
276    }
277
278    #[test]
279    fn debug_output_hides_closure_details() {
280        let debug = format!("{:?}", CustomValidator::new(|_, _, _| {}));
281
282        assert!(debug.contains("CustomValidator"));
283        assert!(!debug.contains("validate_fn"));
284    }
285
286    #[test]
287    fn validation_set_passes_value_through_to_closure() {
288        let called = Arc::new(AtomicBool::new(false));
289        let called_clone = Arc::clone(&called);
290        let mut set = ValidationSet::new();
291        set.add(
292            "name",
293            CustomValidator::new(move |_attribute, value, _errors| {
294                called_clone.store(value == Some(&json!("Alice")), Ordering::Relaxed);
295            }),
296        );
297        let attrs = HashMap::from([("name".to_string(), json!("Alice"))]);
298        let mut errors = Errors::new();
299
300        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
301
302        assert!(called.load(Ordering::Relaxed));
303    }
304}