rustrails_model/validations/
inclusion.rs1use serde_json::Value;
2
3use super::{Validator, ValidatorOptions};
4use crate::errors::{ErrorType, Errors};
5
6#[derive(Debug, Clone, Default)]
8pub struct InclusionValidator {
9 values: Vec<Value>,
10 message: Option<String>,
11 pub(crate) options: ValidatorOptions,
12}
13
14impl InclusionValidator {
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 not included in the list"))
41 }
42}
43
44impl Validator for InclusionValidator {
45 fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
46 if value.is_none_or(|candidate| !self.values.iter().any(|allowed| allowed == candidate)) {
47 errors.add(attribute, ErrorType::Inclusion, self.error_message());
48 }
49 }
50
51 fn name(&self) -> &str {
52 "inclusion"
53 }
54
55 fn options(&self) -> &ValidatorOptions {
56 &self.options
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use std::collections::HashMap;
63
64 use serde_json::json;
65
66 use super::InclusionValidator;
67 use crate::{
68 errors::{ErrorType, Errors},
69 validations::{ValidationSet, Validator},
70 };
71
72 fn validate_inclusion(
73 validator: InclusionValidator,
74 value: Option<serde_json::Value>,
75 ) -> Errors {
76 let mut errors = Errors::new();
77 validator.validate("field", value.as_ref(), &mut errors);
78 errors
79 }
80
81 #[test]
82 fn allows_member_of_set() {
83 let validator = InclusionValidator::new(vec![json!("admin"), json!("editor")]);
84 let mut errors = Errors::new();
85
86 validator.validate("role", Some(&json!("admin")), &mut errors);
87
88 assert!(errors.is_empty());
89 }
90
91 #[test]
92 fn rejects_value_outside_set() {
93 let validator = InclusionValidator::new(vec![json!(1), json!(2)]);
94 let mut errors = Errors::new();
95
96 validator.validate("priority", Some(&json!(3)), &mut errors);
97
98 assert_eq!(errors.on("priority")[0].error_type, ErrorType::Inclusion);
99 }
100
101 #[test]
102 fn rejects_nil_value() {
103 let validator = InclusionValidator::new(vec![json!(true), json!(false)]);
104 let mut errors = Errors::new();
105
106 validator.validate("published", None, &mut errors);
107
108 assert_eq!(
109 errors.on("published")[0].message,
110 "is not included in the list"
111 );
112 }
113
114 #[test]
115 fn custom_message_is_used() {
116 let validator = InclusionValidator::new(vec![json!("usd")]).message("unsupported");
117 let mut errors = Errors::new();
118
119 validator.validate("currency", Some(&json!("eur")), &mut errors);
120
121 assert_eq!(errors.on("currency")[0].message, "unsupported");
122 }
123
124 #[test]
125 fn rejects_empty_allowed_list() {
126 let errors = validate_inclusion(InclusionValidator::new(Vec::new()), Some(json!("usd")));
127
128 assert_eq!(errors.on("field")[0].error_type, ErrorType::Inclusion);
129 }
130
131 #[test]
132 fn allows_null_when_null_is_in_allowed_list() {
133 let errors = validate_inclusion(
134 InclusionValidator::new(vec![json!(null)]),
135 Some(json!(null)),
136 );
137
138 assert!(errors.is_empty());
139 }
140
141 #[test]
142 fn rejects_null_when_null_is_not_in_allowed_list() {
143 let errors = validate_inclusion(InclusionValidator::new(vec![json!(1)]), Some(json!(null)));
144
145 assert_eq!(errors.on("field")[0].error_type, ErrorType::Inclusion);
146 }
147
148 #[test]
149 fn allows_object_members_by_exact_equality() {
150 let errors = validate_inclusion(
151 InclusionValidator::new(vec![json!({ "kind": "vip", "level": 2 })]),
152 Some(json!({ "kind": "vip", "level": 2 })),
153 );
154
155 assert!(errors.is_empty());
156 }
157
158 #[test]
159 fn allows_array_members_by_exact_equality() {
160 let errors = validate_inclusion(
161 InclusionValidator::new(vec![json!([1, 2]), json!([3, 4])]),
162 Some(json!([1, 2])),
163 );
164
165 assert!(errors.is_empty());
166 }
167
168 #[test]
169 fn distinguishes_strings_from_numbers() {
170 let errors = validate_inclusion(InclusionValidator::new(vec![json!(1)]), Some(json!("1")));
171
172 assert_eq!(errors.on("field")[0].error_type, ErrorType::Inclusion);
173 }
174
175 #[test]
176 fn allow_nil_skips_missing_values_in_validation_set() {
177 let mut set = ValidationSet::new();
178 set.add(
179 "role",
180 InclusionValidator::new(vec![json!("admin")]).allow_nil(),
181 );
182 let mut errors = Errors::new();
183
184 let _ = set.validate(&|_| None, &mut errors);
185
186 assert!(errors.is_empty());
187 }
188
189 #[test]
190 fn allow_blank_skips_blank_values_in_validation_set() {
191 let mut set = ValidationSet::new();
192 set.add(
193 "role",
194 InclusionValidator::new(vec![json!("admin")]).allow_blank(),
195 );
196 let attrs = HashMap::from([("role".to_string(), json!(" "))]);
197 let mut errors = Errors::new();
198
199 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
200
201 assert!(errors.is_empty());
202 }
203
204 #[test]
205 fn allow_blank_does_not_skip_non_blank_non_members() {
206 let mut set = ValidationSet::new();
207 set.add(
208 "role",
209 InclusionValidator::new(vec![json!("admin")]).allow_blank(),
210 );
211 let attrs = HashMap::from([("role".to_string(), json!("guest"))]);
212 let mut errors = Errors::new();
213
214 let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
215
216 assert_eq!(errors.on("role")[0].error_type, ErrorType::Inclusion);
217 }
218
219 #[test]
220 fn range_like_value_lists_are_supported() {
221 let errors = validate_inclusion(
222 InclusionValidator::new(vec![json!(1), json!(2), json!(3), json!(4)]),
223 Some(json!(3)),
224 );
225
226 assert!(errors.is_empty());
227 }
228
229 #[test]
230 fn full_message_humanizes_attribute_name() {
231 let mut errors = Errors::new();
232 InclusionValidator::new(vec![json!("usd")]).validate(
233 "preferred_currency",
234 Some(&json!("eur")),
235 &mut errors,
236 );
237
238 assert_eq!(
239 errors.full_messages(),
240 vec!["Preferred currency is not included in the list".to_string()],
241 );
242 }
243}