Skip to main content

rustrails_model/validations/
exclusion.rs

1use serde_json::Value;
2
3use super::{Validator, ValidatorOptions};
4use crate::errors::{ErrorType, Errors};
5
6/// Validates that a value does not belong to a forbidden set.
7#[derive(Debug, Clone, Default)]
8pub struct ExclusionValidator {
9    values: Vec<Value>,
10    message: Option<String>,
11    pub(crate) options: ValidatorOptions,
12}
13
14impl ExclusionValidator {
15    /// Creates a new exclusion validator.
16    #[must_use]
17    pub fn new<T>(values: T) -> Self
18    where
19        T: Into<Vec<Value>>,
20    {
21        Self {
22            values: values.into(),
23            message: None,
24            options: ValidatorOptions::default(),
25        }
26    }
27
28    crate::validations::impl_common_validator_methods!();
29
30    /// Overrides the default exclusion message.
31    #[must_use]
32    pub fn message(mut self, message: impl Into<String>) -> Self {
33        self.message = Some(message.into());
34        self
35    }
36
37    fn error_message(&self) -> String {
38        self.message
39            .clone()
40            .unwrap_or_else(|| String::from("is reserved"))
41    }
42}
43
44impl Validator for ExclusionValidator {
45    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
46        if value.is_some_and(|candidate| self.values.iter().any(|forbidden| forbidden == candidate))
47        {
48            errors.add(attribute, ErrorType::Exclusion, self.error_message());
49        }
50    }
51
52    fn name(&self) -> &str {
53        "exclusion"
54    }
55
56    fn options(&self) -> &ValidatorOptions {
57        &self.options
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use std::collections::HashMap;
64
65    use serde_json::json;
66
67    use super::ExclusionValidator;
68    use crate::{
69        errors::{ErrorType, Errors},
70        validations::{ValidationSet, Validator},
71    };
72
73    fn validate_exclusion(
74        validator: ExclusionValidator,
75        value: Option<serde_json::Value>,
76    ) -> Errors {
77        let mut errors = Errors::new();
78        validator.validate("field", value.as_ref(), &mut errors);
79        errors
80    }
81
82    #[test]
83    fn rejects_forbidden_value() {
84        let validator = ExclusionValidator::new(vec![json!("admin")]);
85        let mut errors = Errors::new();
86
87        validator.validate("role", Some(&json!("admin")), &mut errors);
88
89        assert_eq!(errors.on("role")[0].error_type, ErrorType::Exclusion);
90    }
91
92    #[test]
93    fn allows_non_member() {
94        let validator = ExclusionValidator::new(vec![json!("admin")]);
95        let mut errors = Errors::new();
96
97        validator.validate("role", Some(&json!("user")), &mut errors);
98
99        assert!(errors.is_empty());
100    }
101
102    #[test]
103    fn allows_nil_value() {
104        let validator = ExclusionValidator::new(vec![json!("admin")]);
105        let mut errors = Errors::new();
106
107        validator.validate("role", None, &mut errors);
108
109        assert!(errors.is_empty());
110    }
111
112    #[test]
113    fn custom_message_is_used() {
114        let validator = ExclusionValidator::new(vec![json!("root")]).message("blocked");
115        let mut errors = Errors::new();
116
117        validator.validate("username", Some(&json!("root")), &mut errors);
118
119        assert_eq!(errors.on("username")[0].message, "blocked");
120    }
121
122    #[test]
123    fn rejects_null_when_null_is_forbidden() {
124        let errors = validate_exclusion(
125            ExclusionValidator::new(vec![json!(null)]),
126            Some(json!(null)),
127        );
128
129        assert_eq!(errors.on("field")[0].error_type, ErrorType::Exclusion);
130    }
131
132    #[test]
133    fn allows_null_when_null_is_not_forbidden() {
134        let errors = validate_exclusion(ExclusionValidator::new(vec![json!(1)]), Some(json!(null)));
135
136        assert!(errors.is_empty());
137    }
138
139    #[test]
140    fn allows_any_value_when_forbidden_set_is_empty() {
141        let errors = validate_exclusion(ExclusionValidator::new(Vec::new()), Some(json!("guest")));
142
143        assert!(errors.is_empty());
144    }
145
146    #[test]
147    fn rejects_forbidden_object_by_exact_equality() {
148        let errors = validate_exclusion(
149            ExclusionValidator::new(vec![json!({ "kind": "vip", "level": 2 })]),
150            Some(json!({ "kind": "vip", "level": 2 })),
151        );
152
153        assert_eq!(errors.on("field")[0].error_type, ErrorType::Exclusion);
154    }
155
156    #[test]
157    fn rejects_forbidden_array_by_exact_equality() {
158        let errors = validate_exclusion(
159            ExclusionValidator::new(vec![json!([1, 2])]),
160            Some(json!([1, 2])),
161        );
162
163        assert_eq!(errors.on("field")[0].error_type, ErrorType::Exclusion);
164    }
165
166    #[test]
167    fn distinguishes_strings_from_numbers() {
168        let errors = validate_exclusion(ExclusionValidator::new(vec![json!(1)]), Some(json!("1")));
169
170        assert!(errors.is_empty());
171    }
172
173    #[test]
174    fn allow_nil_skips_missing_values_in_validation_set() {
175        let mut set = ValidationSet::new();
176        set.add(
177            "role",
178            ExclusionValidator::new(vec![json!("admin")]).allow_nil(),
179        );
180        let mut errors = Errors::new();
181
182        let _ = set.validate(&|_| None, &mut errors);
183
184        assert!(errors.is_empty());
185    }
186
187    #[test]
188    fn allow_blank_skips_blank_values_in_validation_set() {
189        let mut set = ValidationSet::new();
190        set.add(
191            "role",
192            ExclusionValidator::new(vec![json!("admin")]).allow_blank(),
193        );
194        let attrs = HashMap::from([("role".to_string(), json!("   "))]);
195        let mut errors = Errors::new();
196
197        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
198
199        assert!(errors.is_empty());
200    }
201
202    #[test]
203    fn allow_blank_does_not_skip_non_blank_forbidden_values() {
204        let mut set = ValidationSet::new();
205        set.add(
206            "role",
207            ExclusionValidator::new(vec![json!("admin")]).allow_blank(),
208        );
209        let attrs = HashMap::from([("role".to_string(), json!("admin"))]);
210        let mut errors = Errors::new();
211
212        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
213
214        assert_eq!(errors.on("role")[0].error_type, ErrorType::Exclusion);
215    }
216
217    #[test]
218    fn multiple_forbidden_values_still_report_single_error() {
219        let errors = validate_exclusion(
220            ExclusionValidator::new(vec![json!("admin"), json!("root")]),
221            Some(json!("root")),
222        );
223
224        assert_eq!(errors.count(), 1);
225    }
226
227    #[test]
228    fn full_message_humanizes_attribute_name() {
229        let mut errors = Errors::new();
230        ExclusionValidator::new(vec![json!("root")]).validate(
231            "user_role",
232            Some(&json!("root")),
233            &mut errors,
234        );
235
236        assert_eq!(
237            errors.full_messages(),
238            vec!["User role is reserved".to_string()]
239        );
240    }
241}