Skip to main content

rustrails_model/validations/
uniqueness.rs

1use 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/// Validates that a value is unique via an injected lookup function.
11#[derive(Default)]
12pub struct UniquenessValidator {
13    /// Additional attributes that define uniqueness scope.
14    pub scope: Vec<String>,
15    /// Whether string comparisons should preserve case.
16    pub case_sensitive: bool,
17    /// Custom message used when the value is taken.
18    pub message: Option<String>,
19    /// Lookup function supplied by higher layers with storage access.
20    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    /// Creates a new uniqueness validator.
37    #[must_use]
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    crate::validations::impl_common_validator_methods!();
43
44    /// Registers a uniqueness check where `true` means the value is unique.
45    #[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    /// Overrides the default taken message.
55    #[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}