rustrails_model/validations/
exclusion.rs1use serde_json::Value;
2
3use super::{Validator, ValidatorOptions};
4use crate::errors::{ErrorType, Errors};
5
6#[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 #[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 #[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}