rustrails_model/validations/
custom.rs1use 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
10pub struct CustomValidator {
12 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 #[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}