rustrails_model/validations/
uniqueness.rs1use std::fmt;
2
3use serde_json::Value;
4
5use super::{Validator, ValidatorOptions};
6use crate::errors::{ErrorType, Errors};
7
8type UniquenessCheckFn = dyn Fn(&str, &Value) -> bool + Send + Sync;
9
10#[derive(Default)]
12pub struct UniquenessValidator {
13 pub scope: Vec<String>,
15 pub case_sensitive: bool,
17 pub message: Option<String>,
19 pub check: Option<Box<UniquenessCheckFn>>,
21 pub(crate) options: ValidatorOptions,
22}
23
24impl fmt::Debug for UniquenessValidator {
25 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
26 formatter
27 .debug_struct("UniquenessValidator")
28 .field("has_check", &self.check.is_some())
29 .field("message", &self.message)
30 .field("options", &self.options)
31 .finish()
32 }
33}
34
35impl UniquenessValidator {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 crate::validations::impl_common_validator_methods!();
43
44 #[must_use]
46 pub fn with_check<F>(mut self, check: F) -> Self
47 where
48 F: Fn(&str, &Value) -> bool + Send + Sync + 'static,
49 {
50 self.check = Some(Box::new(check));
51 self
52 }
53
54 #[must_use]
56 pub fn message(mut self, message: impl Into<String>) -> Self {
57 self.message = Some(message.into());
58 self
59 }
60
61 fn error_message(&self) -> String {
62 self.message
63 .clone()
64 .unwrap_or_else(|| String::from("has already been taken"))
65 }
66}
67
68impl Validator for UniquenessValidator {
69 fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
70 let Some(check) = &self.check else {
71 return;
72 };
73 let Some(value) = value else {
74 return;
75 };
76
77 if !check(attribute, value) {
78 errors.add(attribute, ErrorType::Taken, self.error_message());
79 }
80 }
81
82 fn name(&self) -> &str {
83 "uniqueness"
84 }
85
86 fn options(&self) -> &ValidatorOptions {
87 &self.options
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use std::collections::HashMap;
94 use std::sync::{
95 Arc,
96 atomic::{AtomicBool, Ordering},
97 };
98
99 use serde_json::json;
100
101 use super::UniquenessValidator;
102 use crate::{
103 errors::{ErrorType, Errors},
104 validations::{ValidationContext, ValidationSet, Validator},
105 };
106
107 #[test]
108 fn passes_without_check_function() {
109 let validator = UniquenessValidator::new();
110 let mut errors = Errors::new();
111
112 validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
113
114 assert!(errors.is_empty());
115 }
116
117 #[test]
118 fn failing_check_marks_value_as_taken() {
119 let validator = UniquenessValidator::new().with_check(|_, value| value != &json!("taken"));
120 let mut errors = Errors::new();
121
122 validator.validate("slug", Some(&json!("taken")), &mut errors);
123
124 assert_eq!(errors.on("slug")[0].error_type, ErrorType::Taken);
125 }
126
127 #[test]
128 fn successful_check_passes() {
129 let validator = UniquenessValidator::new().with_check(|_, value| value == &json!("free"));
130 let mut errors = Errors::new();
131
132 validator.validate("slug", Some(&json!("free")), &mut errors);
133
134 assert!(errors.is_empty());
135 }
136
137 #[test]
138 fn custom_message_is_used() {
139 let validator = UniquenessValidator::new()
140 .with_check(|_, _| false)
141 .message("already used");
142 let mut errors = Errors::new();
143
144 validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
145
146 assert_eq!(errors.on("email")[0].message, "already used");
147 }
148
149 #[test]
150 fn missing_values_are_ignored_even_with_a_check() {
151 let called = Arc::new(AtomicBool::new(false));
152 let called_clone = Arc::clone(&called);
153 let validator = UniquenessValidator::new().with_check(move |_, _| {
154 called_clone.store(true, Ordering::Relaxed);
155 true
156 });
157 let mut errors = Errors::new();
158
159 validator.validate("email", None, &mut errors);
160
161 assert!(errors.is_empty());
162 assert!(!called.load(Ordering::Relaxed));
163 }
164
165 #[test]
166 fn check_receives_attribute_name() {
167 let called = Arc::new(AtomicBool::new(false));
168 let called_clone = Arc::clone(&called);
169 let validator = UniquenessValidator::new().with_check(move |attribute, _| {
170 called_clone.store(attribute == "email", Ordering::Relaxed);
171 true
172 });
173 let mut errors = Errors::new();
174
175 validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
176
177 assert!(called.load(Ordering::Relaxed));
178 }
179
180 #[test]
181 fn check_receives_attribute_value() {
182 let called = Arc::new(AtomicBool::new(false));
183 let called_clone = Arc::clone(&called);
184 let validator = UniquenessValidator::new().with_check(move |_, value| {
185 called_clone.store(value == &json!("alice@example.com"), Ordering::Relaxed);
186 true
187 });
188 let mut errors = Errors::new();
189
190 validator.validate("email", Some(&json!("alice@example.com")), &mut errors);
191
192 assert!(called.load(Ordering::Relaxed));
193 }
194
195 #[test]
196 fn allow_nil_skips_null_values_in_validation_set() {
197 let called = Arc::new(AtomicBool::new(false));
198 let called_clone = Arc::clone(&called);
199 let mut set = ValidationSet::new();
200 set.add(
201 "email",
202 UniquenessValidator::new()
203 .with_check(move |_, _| {
204 called_clone.store(true, Ordering::Relaxed);
205 false
206 })
207 .allow_nil(),
208 );
209 let attrs = HashMap::from([("email".to_string(), json!(null))]);
210 let mut errors = Errors::new();
211
212 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
213
214 assert!(errors.is_empty());
215 assert!(!called.load(Ordering::Relaxed));
216 }
217
218 #[test]
219 fn allow_blank_skips_blank_values_in_validation_set() {
220 let called = Arc::new(AtomicBool::new(false));
221 let called_clone = Arc::clone(&called);
222 let mut set = ValidationSet::new();
223 set.add(
224 "email",
225 UniquenessValidator::new()
226 .with_check(move |_, _| {
227 called_clone.store(true, Ordering::Relaxed);
228 false
229 })
230 .allow_blank(),
231 );
232 let attrs = HashMap::from([("email".to_string(), json!(" "))]);
233 let mut errors = Errors::new();
234
235 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
236
237 assert!(errors.is_empty());
238 assert!(!called.load(Ordering::Relaxed));
239 }
240
241 #[test]
242 fn allow_blank_does_not_skip_present_values_in_validation_set() {
243 let mut set = ValidationSet::new();
244 set.add(
245 "email",
246 UniquenessValidator::new()
247 .with_check(|_, _| false)
248 .allow_blank(),
249 );
250 let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
251 let mut errors = Errors::new();
252
253 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
254
255 assert_eq!(errors.on("email")[0].error_type, ErrorType::Taken);
256 }
257
258 #[test]
259 fn on_context_filters_validation_set_execution() {
260 let mut set = ValidationSet::new();
261 set.add(
262 "email",
263 UniquenessValidator::new()
264 .with_check(|_, _| false)
265 .on(vec![ValidationContext::Update]),
266 );
267 let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
268 let mut errors = Errors::new();
269
270 let _ = set.validate_with_context(
271 &|name| attrs.get(name).cloned(),
272 &mut errors,
273 &ValidationContext::Create,
274 );
275 assert!(errors.is_empty());
276
277 let _ = set.validate_with_context(
278 &|name| attrs.get(name).cloned(),
279 &mut errors,
280 &ValidationContext::Update,
281 );
282 assert_eq!(errors.on("email")[0].error_type, ErrorType::Taken);
283 }
284
285 #[test]
286 fn if_condition_false_skips_uniqueness_check() {
287 let called = Arc::new(AtomicBool::new(false));
288 let called_clone = Arc::clone(&called);
289 let mut set = ValidationSet::new();
290 set.add(
291 "email",
292 UniquenessValidator::new()
293 .with_check(move |_, _| {
294 called_clone.store(true, Ordering::Relaxed);
295 false
296 })
297 .if_cond(|value| value == &json!("other@example.com")),
298 );
299 let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
300 let mut errors = Errors::new();
301
302 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
303
304 assert!(errors.is_empty());
305 assert!(!called.load(Ordering::Relaxed));
306 }
307
308 #[test]
309 fn unless_condition_true_skips_uniqueness_check() {
310 let called = Arc::new(AtomicBool::new(false));
311 let called_clone = Arc::clone(&called);
312 let mut set = ValidationSet::new();
313 set.add(
314 "email",
315 UniquenessValidator::new()
316 .with_check(move |_, _| {
317 called_clone.store(true, Ordering::Relaxed);
318 false
319 })
320 .unless_cond(|value| value == &json!("alice@example.com")),
321 );
322 let attrs = HashMap::from([("email".to_string(), json!("alice@example.com"))]);
323 let mut errors = Errors::new();
324
325 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
326
327 assert!(errors.is_empty());
328 assert!(!called.load(Ordering::Relaxed));
329 }
330
331 #[test]
332 fn full_message_humanizes_attribute_name() {
333 let validator = UniquenessValidator::new().with_check(|_, _| false);
334 let mut errors = Errors::new();
335
336 validator.validate(
337 "email_address",
338 Some(&json!("alice@example.com")),
339 &mut errors,
340 );
341
342 assert_eq!(
343 errors.full_messages(),
344 vec!["Email address has already been taken".to_string()],
345 );
346 }
347
348 #[test]
349 fn debug_output_reports_check_presence_without_closure_details() {
350 let debug = format!("{:?}", UniquenessValidator::new().with_check(|_, _| true));
351
352 assert!(debug.contains("has_check"));
353 assert!(!debug.contains("0x"));
354 }
355}