Skip to main content

rainier_validation/
lib.rs

1//! # rainier-validation
2//!
3//! Validation and **request contracts**: the [`Rule`] set, the [`Validator`]
4//! that applies it, and [`FormRequest`] — the contract a controller action
5//! declares instead of checking input by hand.
6//!
7//! ```
8//! use rainier_validation::{Rule, Validator};
9//! use serde_json::json;
10//!
11//! let validator = Validator::new()
12//!     .rules("email", vec![Rule::Required, Rule::Email])
13//!     .rules("password", vec![Rule::Required, Rule::Min(8.0), Rule::Confirmed]);
14//!
15//! let errors = validator
16//!     .validate(&json!({ "email": "ada@example.com", "password": "short" }))
17//!     .unwrap_err();
18//!
19//! assert!(errors.first("password").unwrap().contains("at least 8"));
20//! ```
21//!
22//! ## The three states of a field
23//!
24//! Absent, null and empty are distinct, and only the presence rules
25//! (`Required`, `Present`, `NotNull`) look at the first two. Every other rule
26//! skips a field that is not there — so `[Rule::Email]` means "if supplied it
27//! must be an email" and `[Rule::Required, Rule::Email]` means "must be
28//! supplied, and must be an email". See [`rule`] for the full argument.
29
30#![deny(missing_docs)]
31#![deny(rustdoc::broken_intra_doc_links)]
32
33pub mod form_request;
34pub mod rule;
35pub mod validator;
36
37pub use form_request::{field, FormRequest, Validated};
38pub use rule::{Outcome, Rule};
39pub use validator::{RuleSet, ValidationErrors, Validator};
40
41use serde_json::Value;
42
43/// Read a dotted path out of a JSON tree, indexing arrays numerically.
44///
45/// Shared by the validator (to find a field) and the cross-field rules (to
46/// find the field they compare against).
47pub fn lookup<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
48    let mut cursor = root;
49    for segment in path.split('.') {
50        cursor = match cursor {
51            Value::Object(map) => map.get(segment)?,
52            Value::Array(items) => items.get(segment.parse::<usize>().ok()?)?,
53            _ => return None,
54        };
55    }
56    Some(cursor)
57}
58
59// Re-exported so contracts get the attribute macro without adding the
60// dependency themselves.
61pub use async_trait::async_trait;
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use serde_json::json;
67
68    #[test]
69    fn lookup_walks_objects_and_arrays() {
70        let value = json!({ "items": [{ "sku": "a" }] });
71        assert_eq!(lookup(&value, "items.0.sku"), Some(&json!("a")));
72        assert_eq!(lookup(&value, "items.1.sku"), None);
73        assert_eq!(lookup(&value, "missing"), None);
74        assert_eq!(lookup(&value, "items.0.sku.deeper"), None);
75    }
76}