Skip to main content

rustrails_model/validations/
acceptance.rs

1use serde_json::Value;
2
3use super::{Validator, ValidatorOptions};
4use crate::errors::{ErrorType, Errors};
5
6/// Validates that a value is explicitly accepted.
7#[derive(Debug, Clone)]
8pub struct AcceptanceValidator {
9    accepted_values: Vec<Value>,
10    message: Option<String>,
11    pub(crate) options: ValidatorOptions,
12}
13
14impl Default for AcceptanceValidator {
15    fn default() -> Self {
16        Self {
17            accepted_values: vec![
18                Value::Bool(true),
19                Value::String("1".to_string()),
20                Value::String("yes".to_string()),
21            ],
22            message: None,
23            options: ValidatorOptions {
24                allow_nil: true,
25                ..ValidatorOptions::default()
26            },
27        }
28    }
29}
30
31impl AcceptanceValidator {
32    /// Creates a new acceptance validator with the default accepted values.
33    #[must_use]
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    crate::validations::impl_common_validator_methods!();
39
40    /// Replaces the accepted value list.
41    #[must_use]
42    pub fn accept<T>(mut self, values: T) -> Self
43    where
44        T: Into<Vec<Value>>,
45    {
46        self.accepted_values = values.into();
47        self
48    }
49
50    /// Overrides the default acceptance message.
51    #[must_use]
52    pub fn message(mut self, message: impl Into<String>) -> Self {
53        self.message = Some(message.into());
54        self
55    }
56
57    fn error_message(&self) -> String {
58        self.message
59            .clone()
60            .unwrap_or_else(|| String::from("must be accepted"))
61    }
62}
63
64impl Validator for AcceptanceValidator {
65    fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
66        if value.is_some_and(|candidate| {
67            !self
68                .accepted_values
69                .iter()
70                .any(|accepted| accepted == candidate)
71        }) {
72            errors.add(attribute, ErrorType::Accepted, self.error_message());
73        }
74    }
75
76    fn name(&self) -> &str {
77        "acceptance"
78    }
79
80    fn options(&self) -> &ValidatorOptions {
81        &self.options
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use std::collections::HashMap;
88
89    use serde_json::json;
90
91    use super::AcceptanceValidator;
92    use crate::{
93        errors::{ErrorType, Errors},
94        validations::{ValidationSet, Validator},
95    };
96
97    fn validate_acceptance(
98        validator: AcceptanceValidator,
99        value: Option<serde_json::Value>,
100    ) -> Errors {
101        let mut errors = Errors::new();
102        validator.validate("field", value.as_ref(), &mut errors);
103        errors
104    }
105
106    #[test]
107    fn nil_value_is_ignored() {
108        let validator = AcceptanceValidator::new();
109        let mut errors = Errors::new();
110
111        validator.validate("terms", None, &mut errors);
112
113        assert!(errors.is_empty());
114    }
115
116    #[test]
117    fn default_true_is_accepted() {
118        let validator = AcceptanceValidator::new();
119        let mut errors = Errors::new();
120
121        validator.validate("terms", Some(&json!(true)), &mut errors);
122
123        assert!(errors.is_empty());
124    }
125
126    #[test]
127    fn default_yes_is_accepted() {
128        let validator = AcceptanceValidator::new();
129        let mut errors = Errors::new();
130
131        validator.validate("terms", Some(&json!("yes")), &mut errors);
132
133        assert!(errors.is_empty());
134    }
135
136    #[test]
137    fn blank_string_is_rejected() {
138        let validator = AcceptanceValidator::new();
139        let mut errors = Errors::new();
140
141        validator.validate("terms", Some(&json!("")), &mut errors);
142
143        assert_eq!(errors.on("terms")[0].error_type, ErrorType::Accepted);
144    }
145
146    #[test]
147    fn custom_accept_values_are_used() {
148        let validator = AcceptanceValidator::new().accept(vec![json!(1), json!("I agree")]);
149        let mut errors = Errors::new();
150
151        validator.validate("terms", Some(&json!(1)), &mut errors);
152        validator.validate("terms", Some(&json!("nope")), &mut errors);
153
154        assert_eq!(errors.count(), 1);
155    }
156
157    #[test]
158    fn validation_set_skips_nil_when_default_allow_nil_applies() {
159        let mut set = ValidationSet::new();
160        set.add("terms", AcceptanceValidator::new());
161        let mut errors = Errors::new();
162
163        let _ = set.validate(&|_| None, &mut errors);
164
165        assert!(errors.is_empty());
166    }
167
168    #[test]
169    fn default_string_one_is_accepted() {
170        let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!("1")));
171
172        assert!(errors.is_empty());
173    }
174
175    #[test]
176    fn false_is_rejected_by_default() {
177        let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!(false)));
178
179        assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
180    }
181
182    #[test]
183    fn no_is_rejected_by_default() {
184        let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!("no")));
185
186        assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
187    }
188
189    #[test]
190    fn numeric_one_is_not_equal_to_default_string_one() {
191        let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!(1)));
192
193        assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
194    }
195
196    #[test]
197    fn custom_accept_values_replace_defaults() {
198        let errors = validate_acceptance(
199            AcceptanceValidator::new().accept(vec![json!("I agree")]),
200            Some(json!(true)),
201        );
202
203        assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
204    }
205
206    #[test]
207    fn custom_message_is_used() {
208        let errors = validate_acceptance(
209            AcceptanceValidator::new().message("must be accepted explicitly"),
210            Some(json!(false)),
211        );
212
213        assert_eq!(errors.on("field")[0].message, "must be accepted explicitly");
214    }
215
216    #[test]
217    fn allow_blank_skips_blank_values_in_validation_set() {
218        let mut set = ValidationSet::new();
219        set.add("terms", AcceptanceValidator::new().allow_blank());
220        let attrs = HashMap::from([("terms".to_string(), json!("   "))]);
221        let mut errors = Errors::new();
222
223        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
224
225        assert!(errors.is_empty());
226    }
227
228    #[test]
229    fn custom_boolean_accept_values_are_supported() {
230        let errors = validate_acceptance(
231            AcceptanceValidator::new().accept(vec![json!(false)]),
232            Some(json!(false)),
233        );
234
235        assert!(errors.is_empty());
236    }
237
238    #[test]
239    fn validation_set_adds_error_for_rejected_value() {
240        let mut set = ValidationSet::new();
241        set.add("terms", AcceptanceValidator::new());
242        let attrs = HashMap::from([("terms".to_string(), json!("no"))]);
243        let mut errors = Errors::new();
244
245        let _ = set.validate(&|name| attrs.get(name).cloned(), &mut errors);
246
247        assert_eq!(errors.on("terms")[0].error_type, ErrorType::Accepted);
248    }
249
250    #[test]
251    fn full_message_humanizes_attribute_name() {
252        let mut errors = Errors::new();
253        AcceptanceValidator::new().validate("terms_of_service", Some(&json!(false)), &mut errors);
254
255        assert_eq!(
256            errors.full_messages(),
257            vec!["Terms of service must be accepted".to_string()],
258        );
259    }
260
261    #[test]
262    fn explicit_null_values_are_rejected_by_direct_validation() {
263        let errors = validate_acceptance(AcceptanceValidator::new(), Some(json!(null)));
264
265        assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
266    }
267
268    #[test]
269    fn empty_accept_list_rejects_present_values() {
270        let errors = validate_acceptance(
271            AcceptanceValidator::new().accept(Vec::new()),
272            Some(json!(true)),
273        );
274
275        assert_eq!(errors.on("field")[0].error_type, ErrorType::Accepted);
276    }
277}