Skip to main content

rustlavel_validation/
validator.rs

1//! Running rules against input, and what comes back when they all pass.
2//!
3//! The semantics follow Laravel closely, including the two that surprise people
4//! if they are not written down:
5//!
6//! - A field that was never sent only trips `required`. Every other rule is
7//!   skipped, so `nullable|integer` on an absent field is silence, not an error.
8//! - A blank string — an untouched text input, which a browser submits as `""`
9//!   rather than omitting — is treated as absent for the same reason.
10//!
11//! On success the validator hands back a [`Validated`] containing *only* the
12//! fields that had rules. That is the point of validating: what comes out is
13//! the subset that was actually checked, so an unexpected extra field in the
14//! body cannot ride along into a database write.
15
16use crate::check;
17use crate::errors::Errors;
18use crate::input::Input;
19use crate::messages::{self, Messages, SizeKind};
20use crate::rule::{IntoRules, Rule, Rules};
21use rustlavel_core::Json;
22use rustlavel_http::Request;
23use std::collections::BTreeMap;
24
25/// A set of fields, their rules, and the values to run them against.
26#[derive(Debug, Clone, Default)]
27pub struct Validator {
28    input: Input,
29    fields: Vec<(String, Rules)>,
30    messages: Messages,
31    wants_json: bool,
32}
33
34impl Validator {
35    /// Validate a bag of values — a JSON body, a form, or an [`Input`] built by hand.
36    pub fn new(input: impl Into<Input>) -> Self {
37        Validator { input: input.into(), ..Validator::default() }
38    }
39
40    /// Validate a request, remembering whether the client wants JSON back so
41    /// the failure response can be negotiated later, when the request is gone.
42    pub fn from_request(request: &mut Request) -> Self {
43        let wants_json = request.wants_json();
44        Validator { input: Input::from_request(request), wants_json, ..Validator::default() }
45    }
46
47    /// Add one field's rules, as a Laravel string or as a built rule set.
48    pub fn rule(mut self, field: impl Into<String>, rules: impl IntoRules) -> Self {
49        self.fields.push((field.into(), rules.into_rules()));
50        self
51    }
52
53    /// Add every field at once: `.rules(&[("email", "required|email")])`.
54    pub fn rules(mut self, specs: &[(&str, &str)]) -> Self {
55        for (field, spec) in specs {
56            self = self.rule(*field, *spec);
57        }
58        self
59    }
60
61    /// Override one message. The key is `"field.rule"` or just `"rule"`.
62    pub fn message(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
63        self.messages.set(key, message);
64        self
65    }
66
67    /// Rename a field for display: `.attribute("dob", "date of birth")`.
68    pub fn attribute(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
69        self.messages.set_attribute(field, label);
70        self
71    }
72
73    /// Replace the whole message bag, for an application that keeps its own.
74    pub fn with_messages(mut self, messages: Messages) -> Self {
75        self.messages = messages;
76        self
77    }
78
79    /// Force the failure response to be JSON (or not), overriding what the
80    /// request negotiated.
81    pub fn with_json(mut self, wants_json: bool) -> Self {
82        self.wants_json = wants_json;
83        self
84    }
85
86    pub fn input(&self) -> &Input {
87        &self.input
88    }
89
90    /// Every message the rules produce. Empty when the input is valid.
91    pub fn errors(&self) -> Errors {
92        let mut errors = Errors::new().with_json(self.wants_json);
93        for (field, rules) in &self.fields {
94            self.check(field, rules, &mut errors);
95        }
96        errors
97    }
98
99    pub fn passes(&self) -> bool {
100        self.errors().is_empty()
101    }
102
103    pub fn fails(&self) -> bool {
104        !self.passes()
105    }
106
107    /// The validated subset of the input, or every message that failed.
108    pub fn validate(&self) -> Result<Validated, Errors> {
109        let errors = self.errors();
110        if !errors.is_empty() {
111            return Err(errors);
112        }
113        let mut fields = BTreeMap::new();
114        for (field, _) in &self.fields {
115            if let Some(value) = self.input.get(field) {
116                fields.insert(field.clone(), value.clone());
117            }
118        }
119        Ok(Validated { fields })
120    }
121
122    fn check(&self, field: &str, rules: &Rules, errors: &mut Errors) {
123        let required = rules.has("required");
124
125        let Some(value) = self.input.get(field) else {
126            if required {
127                self.fail(field, &Rule::Required, SizeKind::String, errors);
128            }
129            return;
130        };
131
132        if required && is_blank(value) {
133            self.fail(field, &Rule::Required, SizeKind::String, errors);
134            return;
135        }
136
137        // A blank string is what a browser sends for an untouched input. Laravel
138        // treats it as absent for every rule but the presence rules, so
139        // `nullable|email` on an empty box is silence rather than a complaint.
140        if matches!(value, Json::String(text) if text.trim().is_empty()) {
141            return;
142        }
143
144        // An explicit `null` stops here only when the field opted into it;
145        // otherwise it falls through so `integer` can say what is wrong.
146        if value.is_null() && rules.has("nullable") {
147            return;
148        }
149
150        for rule in rules.rules() {
151            if matches!(rule, Rule::Required | Rule::Nullable) {
152                continue;
153            }
154            if !self.satisfied(field, rule, value, rules) {
155                self.fail(field, rule, size_kind(value, rules), errors);
156            }
157        }
158    }
159
160    fn satisfied(&self, field: &str, rule: &Rule, value: &Json, rules: &Rules) -> bool {
161        match rule {
162            Rule::Required | Rule::Nullable => true,
163            Rule::String => matches!(value, Json::String(_)),
164            Rule::Integer => as_number(value).is_some_and(|n| n.fract() == 0.0),
165            Rule::Numeric => as_number(value).is_some(),
166            Rule::Boolean => is_boolean(value),
167            Rule::Email => text(value).is_some_and(|t| check::is_email(&t)),
168            Rule::Url => text(value).is_some_and(|t| check::is_url(&t)),
169            Rule::Alpha => text(value).is_some_and(|t| check::is_alpha(&t)),
170            Rule::AlphaNum => text(value).is_some_and(|t| check::is_alpha_num(&t)),
171            Rule::AlphaDash => text(value).is_some_and(|t| check::is_alpha_dash(&t)),
172            Rule::Date => text(value).is_some_and(|t| check::is_date(&t)),
173            Rule::Uuid => text(value).is_some_and(|t| check::is_uuid(&t)),
174            Rule::Array => matches!(value, Json::Array(_)),
175            Rule::Min(bound) => measure(value, rules).is_some_and(|(size, _)| size >= *bound),
176            Rule::Max(bound) => measure(value, rules).is_some_and(|(size, _)| size <= *bound),
177            Rule::Between(low, high) => {
178                measure(value, rules).is_some_and(|(size, _)| size >= *low && size <= *high)
179            }
180            Rule::Size(exact) => measure(value, rules).is_some_and(|(size, _)| size == *exact),
181            Rule::In(allowed) => text(value).is_some_and(|t| allowed.contains(&t)),
182            Rule::NotIn(denied) => text(value).is_some_and(|t| !denied.contains(&t)),
183            Rule::StartsWith(prefixes) => {
184                text(value).is_some_and(|t| prefixes.iter().any(|p| t.starts_with(p)))
185            }
186            Rule::EndsWith(suffixes) => {
187                text(value).is_some_and(|t| suffixes.iter().any(|s| t.ends_with(s)))
188            }
189            Rule::Confirmed => self.compares(value, &format!("{field}_confirmation"), true),
190            Rule::Same(other) => self.compares(value, other, true),
191            Rule::Different(other) => self.compares(value, other, false),
192        }
193    }
194
195    /// Compare against another field. The other field must exist either way:
196    /// "must be different from a field you did not send" is not something a
197    /// user can act on, so it fails rather than silently passing.
198    fn compares(&self, value: &Json, other: &str, want_equal: bool) -> bool {
199        self.input.get(other).is_some_and(|found| equivalent(value, found) == want_equal)
200    }
201
202    fn fail(&self, field: &str, rule: &Rule, kind: SizeKind, errors: &mut Errors) {
203        let template = self.messages.template(field, rule, kind);
204        let mut values = vec![("attribute", self.messages.label(field))];
205        match rule {
206            Rule::Min(bound) => values.push(("min", messages::format_number(*bound))),
207            Rule::Max(bound) => values.push(("max", messages::format_number(*bound))),
208            Rule::Between(low, high) => {
209                values.push(("min", messages::format_number(*low)));
210                values.push(("max", messages::format_number(*high)));
211            }
212            Rule::Size(exact) => values.push(("size", messages::format_number(*exact))),
213            Rule::In(list) | Rule::NotIn(list) | Rule::StartsWith(list) | Rule::EndsWith(list) => {
214                values.push(("values", messages::format_values(list)));
215            }
216            Rule::Same(other) | Rule::Different(other) => {
217                values.push(("other", self.messages.label(other)));
218            }
219            _ => {}
220        }
221        errors.add(field, messages::interpolate(&template, &values));
222    }
223}
224
225/// The fields that had rules and passed them.
226#[derive(Debug, Clone, Default, PartialEq)]
227pub struct Validated {
228    fields: BTreeMap<String, Json>,
229}
230
231impl Validated {
232    pub fn get(&self, name: &str) -> Option<&Json> {
233        self.fields.get(name)
234    }
235
236    pub fn has(&self, name: &str) -> bool {
237        self.fields.contains_key(name)
238    }
239
240    /// A scalar as text. A number comes back as its digits, matching what
241    /// `Request::input` hands back for the same field sent through a form.
242    pub fn string(&self, name: &str) -> Option<String> {
243        text(self.get(name)?)
244    }
245
246    pub fn integer(&self, name: &str) -> Option<i64> {
247        let number = as_number(self.get(name)?)?;
248        (number.fract() == 0.0).then_some(number as i64)
249    }
250
251    pub fn number(&self, name: &str) -> Option<f64> {
252        as_number(self.get(name)?)
253    }
254
255    /// A boolean, accepting the `"1"`/`"0"` and `"true"`/`"false"` a form sends.
256    pub fn boolean(&self, name: &str) -> Option<bool> {
257        match self.get(name)? {
258            Json::Bool(value) => Some(*value),
259            Json::Number(number) => Some(*number != 0.0),
260            Json::String(text) => match text.as_str() {
261                "1" | "true" => Some(true),
262                "0" | "false" => Some(false),
263                _ => None,
264            },
265            _ => None,
266        }
267    }
268
269    pub fn array(&self, name: &str) -> Option<&[Json]> {
270        self.get(name)?.as_array()
271    }
272
273    pub fn fields(&self) -> &BTreeMap<String, Json> {
274        &self.fields
275    }
276
277    pub fn len(&self) -> usize {
278        self.fields.len()
279    }
280
281    pub fn is_empty(&self) -> bool {
282        self.fields.is_empty()
283    }
284
285    /// The validated data as a JSON object, ready to echo back or store.
286    pub fn into_json(self) -> Json {
287        Json::Object(self.fields)
288    }
289}
290
291impl From<Validated> for Json {
292    fn from(validated: Validated) -> Self {
293        validated.into_json()
294    }
295}
296
297/// What `required` considers missing: null, a blank string, an empty array.
298fn is_blank(value: &Json) -> bool {
299    match value {
300        Json::Null => true,
301        Json::String(text) => text.trim().is_empty(),
302        Json::Array(items) => items.is_empty(),
303        _ => false,
304    }
305}
306
307/// A scalar rendered as text, so rules written for strings still work on the
308/// numbers and booleans a JSON body carries.
309fn text(value: &Json) -> Option<String> {
310    match value {
311        Json::String(found) => Some(found.clone()),
312        Json::Number(_) | Json::Bool(_) => Some(value.to_string()),
313        Json::Null | Json::Array(_) | Json::Object(_) => None,
314    }
315}
316
317/// A number, whether it came from a JSON body or as the text of a form field.
318///
319/// Non-finite values are refused: Rust parses `"inf"` and `"NaN"` happily, and
320/// neither is a number a user meant to type.
321fn as_number(value: &Json) -> Option<f64> {
322    match value {
323        Json::Number(number) => Some(*number).filter(|n| n.is_finite()),
324        Json::String(found) => found.trim().parse::<f64>().ok().filter(|n| n.is_finite()),
325        _ => None,
326    }
327}
328
329fn is_boolean(value: &Json) -> bool {
330    match value {
331        Json::Bool(_) => true,
332        Json::Number(number) => *number == 0.0 || *number == 1.0,
333        Json::String(found) => matches!(found.as_str(), "0" | "1" | "true" | "false"),
334        _ => false,
335    }
336}
337
338/// Equality for `confirmed`, `same` and `different`.
339///
340/// Compared as text first, so `18` from a JSON body and `"18"` from the form
341/// that re-submitted it are the same value.
342fn equivalent(left: &Json, right: &Json) -> bool {
343    match (text(left), text(right)) {
344        (Some(left), Some(right)) => left == right,
345        _ => left == right,
346    }
347}
348
349/// How big a value is, and which reading of "big" applied.
350///
351/// An array counts items. A field carrying `integer` or `numeric` is compared
352/// by value, which is what makes `age|integer|min:18` accept the string `"18"`
353/// a form sends. Anything else is measured in characters.
354fn measure(value: &Json, rules: &Rules) -> Option<(f64, SizeKind)> {
355    if let Json::Array(items) = value {
356        return Some((items.len() as f64, SizeKind::Array));
357    }
358    if rules.has("integer") || rules.has("numeric") {
359        return as_number(value).map(|number| (number, SizeKind::Numeric));
360    }
361    match value {
362        Json::Number(number) => Some((*number, SizeKind::Numeric)),
363        Json::String(found) => Some((found.chars().count() as f64, SizeKind::String)),
364        _ => None,
365    }
366}
367
368/// The reading a message should use, even for a value too odd to measure.
369fn size_kind(value: &Json, rules: &Rules) -> SizeKind {
370    if let Some((_, kind)) = measure(value, rules) {
371        return kind;
372    }
373    if rules.has("integer") || rules.has("numeric") { SizeKind::Numeric } else { SizeKind::String }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use rustlavel_http::Method;
380
381    /// Run one field's rules over one value and report the first message.
382    fn message_for(value: Json, spec: &str) -> Option<String> {
383        Validator::new(Input::new().with("field", value))
384            .rule("field", spec)
385            .errors()
386            .first("field")
387            .map(str::to_string)
388    }
389
390    fn passes(value: Json, spec: &str) -> bool {
391        message_for(value, spec).is_none()
392    }
393
394    #[test]
395    fn required_accepts_a_value_and_rejects_every_shape_of_emptiness() {
396        assert!(passes(Json::from("ada"), "required"));
397        assert!(passes(Json::from(0), "required"), "zero is a value");
398        assert!(passes(Json::from(false), "required"), "false is a value");
399
400        assert!(!passes(Json::Null, "required"));
401        assert!(!passes(Json::from(""), "required"));
402        assert!(!passes(Json::from("   "), "required"));
403        assert!(!passes(Json::Array(vec![]), "required"));
404
405        let missing = Validator::new(Input::new()).rule("field", "required").errors();
406        assert_eq!(missing.first("field"), Some("The field field is required."));
407    }
408
409    #[test]
410    fn an_absent_field_without_required_trips_nothing() {
411        let errors = Validator::new(Input::new()).rule("age", "integer|min:18").errors();
412        assert!(errors.is_empty());
413    }
414
415    #[test]
416    fn nullable_lets_an_explicit_null_through_where_a_bare_rule_would_not() {
417        assert!(passes(Json::Null, "nullable|integer"));
418        assert!(!passes(Json::Null, "integer"));
419    }
420
421    #[test]
422    fn nullable_and_required_together_still_demand_a_value() {
423        assert!(!passes(Json::Null, "required|nullable|string"));
424    }
425
426    #[test]
427    fn a_blank_string_is_treated_as_absent_by_every_rule_but_required() {
428        assert!(passes(Json::from(""), "email|min:5"));
429        assert!(!passes(Json::from(""), "required|email"));
430    }
431
432    #[test]
433    fn string_accepts_text_and_rejects_other_json_types() {
434        assert!(passes(Json::from("ada"), "string"));
435        assert!(!passes(Json::from(7), "string"));
436        assert!(!passes(Json::from(true), "string"));
437        assert_eq!(
438            message_for(Json::from(7), "string").unwrap(),
439            "The field field must be a string."
440        );
441    }
442
443    #[test]
444    fn integer_accepts_a_whole_number_however_it_was_encoded() {
445        assert!(passes(Json::from(18), "integer"));
446        assert!(passes(Json::from("18"), "integer"), "a form sends numbers as text");
447        assert!(passes(Json::from(-3), "integer"));
448
449        assert!(!passes(Json::from(1.5), "integer"));
450        assert!(!passes(Json::from("1.5"), "integer"));
451        assert!(!passes(Json::from("eighteen"), "integer"));
452        assert_eq!(
453            message_for(Json::from("x"), "integer").unwrap(),
454            "The field field must be an integer."
455        );
456    }
457
458    #[test]
459    fn numeric_accepts_fractions_but_not_words_or_infinities() {
460        assert!(passes(Json::from(1.5), "numeric"));
461        assert!(passes(Json::from("-2.75"), "numeric"));
462
463        assert!(!passes(Json::from("abc"), "numeric"));
464        assert!(!passes(Json::from("inf"), "numeric"), "`inf` parses in Rust but is not a number a user typed");
465        assert!(!passes(Json::from(true), "numeric"));
466    }
467
468    #[test]
469    fn boolean_accepts_the_forms_a_checkbox_arrives_in() {
470        for value in [Json::from(true), Json::from(false), Json::from(1), Json::from(0)] {
471            assert!(passes(value.clone(), "boolean"), "{value} should be boolean");
472        }
473        for value in ["1", "0", "true", "false"] {
474            assert!(passes(Json::from(value), "boolean"), "{value} should be boolean");
475        }
476
477        assert!(!passes(Json::from("yes"), "boolean"));
478        assert!(!passes(Json::from(2), "boolean"));
479        assert_eq!(
480            message_for(Json::from("yes"), "boolean").unwrap(),
481            "The field field must be true or false."
482        );
483    }
484
485    #[test]
486    fn email_and_url_delegate_to_the_format_checks() {
487        assert!(passes(Json::from("ada@example.com"), "email"));
488        assert!(!passes(Json::from("ada@example"), "email"));
489        assert_eq!(
490            message_for(Json::from("nope"), "email").unwrap(),
491            "The field field must be a valid email address."
492        );
493
494        assert!(passes(Json::from("https://example.com"), "url"));
495        assert!(!passes(Json::from("example.com"), "url"));
496        assert_eq!(
497            message_for(Json::from("example.com"), "url").unwrap(),
498            "The field field must be a valid URL."
499        );
500    }
501
502    #[test]
503    fn min_counts_characters_for_a_string() {
504        assert!(passes(Json::from("abc"), "min:3"));
505        assert!(!passes(Json::from("ab"), "min:3"));
506        assert_eq!(
507            message_for(Json::from("ab"), "min:3").unwrap(),
508            "The field field must be at least 3 characters."
509        );
510    }
511
512    #[test]
513    fn min_compares_values_when_the_field_is_a_number() {
514        assert!(passes(Json::from(18), "integer|min:18"));
515        assert!(passes(Json::from("18"), "integer|min:18"), "a form value is still a number");
516        assert!(!passes(Json::from(17), "integer|min:18"));
517        assert_eq!(
518            message_for(Json::from(17), "integer|min:18").unwrap(),
519            "The field field must be at least 18."
520        );
521    }
522
523    #[test]
524    fn min_counts_items_for_an_array() {
525        assert!(passes(Json::Array(vec![Json::from("a"), Json::from("b")]), "array|min:2"));
526        assert_eq!(
527            message_for(Json::Array(vec![Json::from("a")]), "array|min:2").unwrap(),
528            "The field field must have at least 2 items."
529        );
530    }
531
532    #[test]
533    fn max_mirrors_min_across_the_same_three_readings() {
534        assert!(passes(Json::from("abc"), "max:3"));
535        assert!(!passes(Json::from("abcd"), "max:3"));
536        assert_eq!(
537            message_for(Json::from("abcd"), "max:3").unwrap(),
538            "The field field must not be greater than 3 characters."
539        );
540
541        assert!(passes(Json::from(3), "integer|max:3"));
542        assert_eq!(
543            message_for(Json::from(4), "integer|max:3").unwrap(),
544            "The field field must not be greater than 3."
545        );
546
547        assert_eq!(
548            message_for(Json::Array(vec![Json::from(1), Json::from(2)]), "array|max:1").unwrap(),
549            "The field field must not have more than 1 items."
550        );
551    }
552
553    #[test]
554    fn between_is_inclusive_on_both_ends() {
555        assert!(passes(Json::from(1), "integer|between:1,10"));
556        assert!(passes(Json::from(10), "integer|between:1,10"));
557        assert!(!passes(Json::from(11), "integer|between:1,10"));
558        assert_eq!(
559            message_for(Json::from(0), "integer|between:1,10").unwrap(),
560            "The field field must be between 1 and 10."
561        );
562        assert_eq!(
563            message_for(Json::from("ab"), "between:3,5").unwrap(),
564            "The field field must be between 3 and 5 characters."
565        );
566    }
567
568    #[test]
569    fn size_demands_an_exact_length_value_or_count() {
570        assert!(passes(Json::from("abcdef"), "size:6"));
571        assert!(!passes(Json::from("abcde"), "size:6"));
572        assert_eq!(
573            message_for(Json::from("abcde"), "size:6").unwrap(),
574            "The field field must be 6 characters."
575        );
576        assert!(passes(Json::from(6), "integer|size:6"));
577        assert!(passes(Json::Array(vec![Json::from(1)]), "array|size:1"));
578    }
579
580    #[test]
581    fn in_and_not_in_match_against_the_listed_values() {
582        assert!(passes(Json::from("draft"), "in:draft,published"));
583        assert!(!passes(Json::from("deleted"), "in:draft,published"));
584        assert_eq!(
585            message_for(Json::from("deleted"), "in:draft,published").unwrap(),
586            "The selected field is invalid."
587        );
588
589        assert!(passes(Json::from("ada"), "not_in:admin,root"));
590        assert!(!passes(Json::from("root"), "not_in:admin,root"));
591    }
592
593    #[test]
594    fn alpha_families_reject_the_characters_they_exclude() {
595        assert!(passes(Json::from("Ada"), "alpha"));
596        assert!(!passes(Json::from("Ada2"), "alpha"));
597        assert_eq!(
598            message_for(Json::from("Ada2"), "alpha").unwrap(),
599            "The field field must only contain letters."
600        );
601
602        assert!(passes(Json::from("Ada2"), "alpha_num"));
603        assert!(!passes(Json::from("Ada-2"), "alpha_num"));
604
605        assert!(passes(Json::from("ada-2_x"), "alpha_dash"));
606        assert!(!passes(Json::from("ada 2"), "alpha_dash"));
607    }
608
609    #[test]
610    fn starts_with_and_ends_with_accept_any_of_their_options() {
611        assert!(passes(Json::from("https://x.dev"), "starts_with:http,https"));
612        assert!(!passes(Json::from("ftp://x.dev"), "starts_with:http,https"));
613        assert_eq!(
614            message_for(Json::from("ftp://x.dev"), "starts_with:http,https").unwrap(),
615            "The field field must start with one of the following: http, https."
616        );
617
618        assert!(passes(Json::from("a@b.dev"), "ends_with:.com,.dev"));
619        assert!(!passes(Json::from("a@b.net"), "ends_with:.com,.dev"));
620        assert_eq!(
621            message_for(Json::from("a@b.net"), "ends_with:.com,.dev").unwrap(),
622            "The field field must end with one of the following: .com, .dev."
623        );
624    }
625
626    #[test]
627    fn date_uuid_and_array_report_their_own_shapes() {
628        assert!(passes(Json::from("2024-02-29"), "date"));
629        assert!(!passes(Json::from("2023-02-29"), "date"));
630        assert_eq!(
631            message_for(Json::from("nope"), "date").unwrap(),
632            "The field field must be a valid date in the format YYYY-MM-DD."
633        );
634
635        assert!(passes(Json::from("9f8b2c1a-4d3e-4f5a-8b7c-1d2e3f4a5b6c"), "uuid"));
636        assert!(!passes(Json::from("not-a-uuid"), "uuid"));
637        assert_eq!(
638            message_for(Json::from("x"), "uuid").unwrap(),
639            "The field field must be a valid UUID."
640        );
641
642        assert!(passes(Json::Array(vec![Json::from(1)]), "array"));
643        assert!(!passes(Json::from("a,b"), "array"));
644        assert_eq!(
645            message_for(Json::from("a"), "array").unwrap(),
646            "The field field must be an array."
647        );
648    }
649
650    #[test]
651    fn confirmed_looks_for_the_matching_confirmation_field() {
652        let input = Input::new().with("password", "secret").with("password_confirmation", "secret");
653        assert!(Validator::new(input).rule("password", "confirmed").passes());
654
655        let wrong = Input::new().with("password", "secret").with("password_confirmation", "typo");
656        let errors = Validator::new(wrong).rule("password", "confirmed").errors();
657        assert_eq!(errors.first("password"), Some("The password field confirmation does not match."));
658
659        // A confirmation that was never sent fails just as a mismatch does.
660        let absent = Input::new().with("password", "secret");
661        assert!(Validator::new(absent).rule("password", "confirmed").fails());
662    }
663
664    #[test]
665    fn same_and_different_compare_against_another_field() {
666        let input = Input::new().with("password", "secret").with("repeat", "secret");
667        assert!(Validator::new(input.clone()).rule("repeat", "same:password").passes());
668
669        let errors = Validator::new(input).rule("repeat", "different:password").errors();
670        assert_eq!(errors.first("repeat"), Some("The repeat field and password must be different."));
671
672        let differing = Input::new().with("username", "ada").with("password", "secret");
673        assert!(Validator::new(differing.clone()).rule("password", "different:username").passes());
674
675        let errors = Validator::new(differing).rule("password", "same:username").errors();
676        assert_eq!(errors.first("password"), Some("The password field must match username."));
677    }
678
679    #[test]
680    fn same_compares_a_json_number_with_the_text_a_form_would_resend() {
681        let input = Input::new().with("total", 42).with("confirm_total", "42");
682        assert!(Validator::new(input).rule("confirm_total", "same:total").passes());
683    }
684
685    #[test]
686    fn every_failing_rule_on_a_field_is_reported() {
687        let errors = Validator::new(Input::new().with("email", "nope"))
688            .rule("email", "email|min:20")
689            .errors();
690
691        assert_eq!(errors.get("email").len(), 2);
692        assert_eq!(errors.len(), 2);
693    }
694
695    #[test]
696    fn a_custom_message_replaces_the_default_for_one_field_and_rule() {
697        let errors = Validator::new(Input::new())
698            .rule("email", "required")
699            .rule("name", "required")
700            .message("email.required", "We cannot reach you without an email.")
701            .errors();
702
703        assert_eq!(errors.first("email"), Some("We cannot reach you without an email."));
704        assert_eq!(errors.first("name"), Some("The name field is required."));
705    }
706
707    #[test]
708    fn a_custom_message_keeps_its_placeholders() {
709        let errors = Validator::new(Input::new().with("age", 12))
710            .rule("age", "integer|min:18")
711            .message("min", "You must be :min or older to sign up (:attribute).")
712            .errors();
713
714        assert_eq!(errors.first("age"), Some("You must be 18 or older to sign up (age)."));
715    }
716
717    #[test]
718    fn an_attribute_override_reaches_the_rendered_message() {
719        let errors = Validator::new(Input::new())
720            .rule("dob", "required")
721            .attribute("dob", "date of birth")
722            .errors();
723
724        assert_eq!(errors.first("dob"), Some("The date of birth field is required."));
725    }
726
727    #[test]
728    fn a_snake_case_field_reads_as_words_without_any_configuration() {
729        let errors = Validator::new(Input::new()).rule("email_address", "required").errors();
730        assert_eq!(errors.first("email_address"), Some("The email address field is required."));
731    }
732
733    #[test]
734    fn validated_data_is_the_checked_subset_and_nothing_else() {
735        let input = Input::new()
736            .with("email", "ada@example.com")
737            .with("age", 36)
738            .with("is_admin", true);
739
740        let data = Validator::new(input)
741            .rules(&[("email", "required|email"), ("age", "integer")])
742            .validate()
743            .unwrap();
744
745        assert_eq!(data.len(), 2);
746        assert!(!data.has("is_admin"), "a field with no rules must not ride along");
747        assert_eq!(data.string("email").as_deref(), Some("ada@example.com"));
748        assert_eq!(data.integer("age"), Some(36));
749    }
750
751    #[test]
752    fn validated_accessors_read_the_forms_a_wire_value_arrives_in() {
753        let input = Input::new()
754            .with("age", "36")
755            .with("rate", "1.5")
756            .with("active", "true")
757            .with("tags", Json::Array(vec![Json::from("a")]));
758
759        let data = Validator::new(input)
760            .rules(&[("age", "integer"), ("rate", "numeric"), ("active", "boolean"), ("tags", "array")])
761            .validate()
762            .unwrap();
763
764        assert_eq!(data.integer("age"), Some(36));
765        assert_eq!(data.number("rate"), Some(1.5));
766        assert_eq!(data.boolean("active"), Some(true));
767        assert_eq!(data.array("tags").unwrap().len(), 1);
768        assert_eq!(data.string("age").as_deref(), Some("36"));
769        assert_eq!(data.integer("missing"), None);
770    }
771
772    #[test]
773    fn validated_data_converts_to_a_json_object() {
774        let data = Validator::new(Input::new().with("name", "ada"))
775            .rule("name", "required|string")
776            .validate()
777            .unwrap();
778
779        assert_eq!(Json::from(data).to_string(), r#"{"name":"ada"}"#);
780    }
781
782    #[test]
783    fn a_nullable_field_that_was_sent_as_null_is_still_returned() {
784        let data = Validator::new(Input::new().with("nickname", Json::Null))
785            .rule("nickname", "nullable|string")
786            .validate()
787            .unwrap();
788
789        assert!(data.has("nickname"));
790        assert!(data.get("nickname").unwrap().is_null());
791        assert_eq!(data.string("nickname"), None);
792    }
793
794    #[test]
795    fn the_builder_and_the_string_syntax_validate_identically() {
796        let input = Input::new().with("email", "nope").with("age", 12);
797        let specs = Validator::new(input.clone())
798            .rules(&[("email", "required|email"), ("age", "integer|min:18")])
799            .errors();
800        let built = Validator::new(input)
801            .rule("email", Rule::required().email())
802            .rule("age", Rule::integer().min(18))
803            .errors();
804
805        assert_eq!(specs, built);
806    }
807
808    #[test]
809    fn validating_a_request_reads_a_json_body() {
810        let mut request = Request::new(Method::Post, "/users")
811            .with_json(Json::object([("email", "ada@example.com".into()), ("age", 36.into())]));
812
813        let data = Validator::from_request(&mut request)
814            .rules(&[("email", "required|email"), ("age", "required|integer|min:18")])
815            .validate()
816            .unwrap();
817
818        assert_eq!(data.string("email").as_deref(), Some("ada@example.com"));
819        assert_eq!(data.integer("age"), Some(36));
820    }
821
822    #[test]
823    fn validating_a_request_reads_a_form_body() {
824        let mut request = Request::new(Method::Post, "/register").with_form(&[
825            ("email", "ada@example.com"),
826            ("password", "secret123"),
827            ("password_confirmation", "secret123"),
828        ]);
829
830        let data = Validator::from_request(&mut request)
831            .rules(&[("email", "required|email"), ("password", "required|min:8|confirmed")])
832            .validate()
833            .unwrap();
834
835        assert_eq!(data.len(), 2);
836        assert_eq!(data.string("password").as_deref(), Some("secret123"));
837    }
838
839    #[test]
840    fn a_failing_form_request_reports_every_field() {
841        let mut request = Request::new(Method::Post, "/register")
842            .with_form(&[("email", "nope"), ("password", "short"), ("password_confirmation", "other")]);
843
844        let errors = Validator::from_request(&mut request)
845            .rules(&[("email", "required|email"), ("password", "required|min:8|confirmed")])
846            .validate()
847            .unwrap_err();
848
849        assert_eq!(errors.first("email"), Some("The email field must be a valid email address."));
850        assert_eq!(errors.get("password").len(), 2);
851        assert!(!errors.wants_json(), "a form post is a browser, not an API client");
852    }
853
854    #[test]
855    fn a_json_request_is_remembered_as_wanting_a_json_failure() {
856        let mut request =
857            Request::new(Method::Post, "/api/users").with_json(Json::object([("email", "no".into())]));
858
859        let errors = Validator::from_request(&mut request).rule("email", "email").validate().unwrap_err();
860        assert!(errors.wants_json());
861    }
862
863    #[test]
864    fn query_string_values_validate_like_any_other_input() {
865        let mut request = Request::new(Method::Get, "/search?page=2&per_page=500");
866
867        let errors = Validator::from_request(&mut request)
868            .rules(&[("page", "integer|min:1"), ("per_page", "integer|max:100")])
869            .validate()
870            .unwrap_err();
871
872        assert!(!errors.has("page"));
873        assert_eq!(errors.first("per_page"), Some("The per page field must not be greater than 100."));
874    }
875}