Skip to main content

rustrails_model/validations/
inclusion.rs

1use serde_json::Value;
2
3use super::{Validator, ValidatorOptions};
4use crate::errors::{ErrorType, Errors};
5
6/// Validates that a value belongs to an allowed set.
7#[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    /// Creates a new inclusion 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 inclusion 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 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}