Skip to main content

rustlavel_validation/
input.rs

1//! The values a validator runs against.
2//!
3//! A JSON body, a urlencoded form, and a query string all arrive as the same
4//! thing here: a map of field name to [`Json`]. Form and query values are always
5//! strings, which is why rules like `integer` accept `"18"` as readily as `18`
6//! — whether a form validates should not depend on how the browser encoded it.
7
8use rustlavel_core::Json;
9use rustlavel_http::Request;
10use std::collections::BTreeMap;
11
12/// The named values under validation.
13#[derive(Debug, Clone, Default, PartialEq)]
14pub struct Input {
15    fields: BTreeMap<String, Json>,
16}
17
18impl Input {
19    pub fn new() -> Self {
20        Input::default()
21    }
22
23    /// Take the top level of a JSON object.
24    ///
25    /// A body that is not an object — an array, a bare string — has no named
26    /// fields, so it validates as empty and `required` rules report what is
27    /// missing. That is a better failure than a parse error the client cannot act on.
28    pub fn from_json(value: &Json) -> Self {
29        match value {
30            Json::Object(map) => Input { fields: map.clone() },
31            _ => Input::new(),
32        }
33    }
34
35    /// Build from name/value pairs — a form body or a query string.
36    ///
37    /// A repeated key becomes an array, because that is how a browser sends a
38    /// group of checkboxes, and it is what makes `array` and a counting `min`
39    /// work on one.
40    pub fn from_pairs<K: AsRef<str>, V: AsRef<str>>(
41        pairs: impl IntoIterator<Item = (K, V)>,
42    ) -> Self {
43        let mut input = Input::new();
44        for (key, value) in pairs {
45            input.push(key.as_ref(), value.as_ref());
46        }
47        input
48    }
49
50    /// Everything a request carries, in `Request::input`'s precedence order:
51    /// the query string first, then a form body, then a JSON body on top.
52    pub fn from_request(request: &mut Request) -> Self {
53        let query: Vec<(String, String)> = request.query_pairs().to_vec();
54        let mut input = Input::from_pairs(query);
55
56        let form: Vec<(String, String)> = request.form().to_vec();
57        input.merge(Input::from_pairs(form));
58
59        if let Some(body) = request.json() {
60            let body = Input::from_json(body);
61            input.merge(body);
62        }
63        input
64    }
65
66    /// Overlay another set of values, replacing any field they share.
67    pub fn merge(&mut self, other: Input) {
68        self.fields.extend(other.fields);
69    }
70
71    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Json>) {
72        self.fields.insert(name.into(), value.into());
73    }
74
75    /// The chaining form of [`Input::insert`], for building an input in a test
76    /// or from values the application already holds.
77    pub fn with(mut self, name: impl Into<String>, value: impl Into<Json>) -> Self {
78        self.insert(name, value);
79        self
80    }
81
82    pub fn get(&self, name: &str) -> Option<&Json> {
83        self.fields.get(name)
84    }
85
86    /// Whether the field was sent at all. A field sent as `null` is present.
87    pub fn has(&self, name: &str) -> bool {
88        self.fields.contains_key(name)
89    }
90
91    pub fn fields(&self) -> &BTreeMap<String, Json> {
92        &self.fields
93    }
94
95    pub fn len(&self) -> usize {
96        self.fields.len()
97    }
98
99    pub fn is_empty(&self) -> bool {
100        self.fields.is_empty()
101    }
102
103    /// Add a string value, promoting a repeated key to an array.
104    fn push(&mut self, name: &str, value: &str) {
105        match self.fields.get_mut(name) {
106            Some(Json::Array(items)) => items.push(Json::from(value)),
107            Some(existing) => {
108                let first = std::mem::replace(existing, Json::Null);
109                *existing = Json::Array(vec![first, Json::from(value)]);
110            }
111            None => {
112                self.fields.insert(name.to_string(), Json::from(value));
113            }
114        }
115    }
116}
117
118impl From<Json> for Input {
119    fn from(value: Json) -> Self {
120        Input::from_json(&value)
121    }
122}
123
124impl From<&Json> for Input {
125    fn from(value: &Json) -> Self {
126        Input::from_json(value)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use rustlavel_http::Method;
134
135    #[test]
136    fn takes_the_top_level_of_a_json_object() {
137        let body = Json::parse(r#"{"email":"ada@example.com","age":36,"tags":["a"]}"#).unwrap();
138        let input = Input::from_json(&body);
139
140        assert_eq!(input.get("email").unwrap().as_str(), Some("ada@example.com"));
141        assert_eq!(input.get("age").unwrap().as_i64(), Some(36));
142        assert_eq!(input.get("tags").unwrap().as_array().unwrap().len(), 1);
143        assert_eq!(input.len(), 3);
144    }
145
146    #[test]
147    fn a_json_body_that_is_not_an_object_validates_as_empty() {
148        assert!(Input::from_json(&Json::parse("[1,2]").unwrap()).is_empty());
149        assert!(Input::from_json(&Json::Null).is_empty());
150    }
151
152    #[test]
153    fn a_repeated_pair_key_becomes_an_array() {
154        let input = Input::from_pairs([("tag", "a"), ("tag", "b"), ("tag", "c"), ("name", "ada")]);
155
156        assert_eq!(input.get("tag").unwrap().as_array().unwrap().len(), 3);
157        assert_eq!(input.get("name").unwrap().as_str(), Some("ada"));
158    }
159
160    #[test]
161    fn a_request_body_wins_over_the_query_string() {
162        let mut request = Request::new(Method::Post, "/users?name=from-query&page=2")
163            .with_json(Json::object([("name", "from-body".into())]));
164        let input = Input::from_request(&mut request);
165
166        assert_eq!(input.get("name").unwrap().as_str(), Some("from-body"));
167        assert_eq!(input.get("page").unwrap().as_str(), Some("2"));
168    }
169
170    #[test]
171    fn a_form_body_is_read_alongside_the_query_string() {
172        let mut request = Request::new(Method::Post, "/login?next=/home")
173            .with_form(&[("email", "ada@example.com"), ("password", "s e c")]);
174        let input = Input::from_request(&mut request);
175
176        assert_eq!(input.get("email").unwrap().as_str(), Some("ada@example.com"));
177        assert_eq!(input.get("password").unwrap().as_str(), Some("s e c"));
178        assert_eq!(input.get("next").unwrap().as_str(), Some("/home"));
179    }
180
181    #[test]
182    fn merging_replaces_shared_fields_and_keeps_the_rest() {
183        let mut input = Input::new().with("a", 1).with("b", 2);
184        input.merge(Input::new().with("b", 20).with("c", 30));
185
186        assert_eq!(input.get("a").unwrap().as_i64(), Some(1));
187        assert_eq!(input.get("b").unwrap().as_i64(), Some(20));
188        assert_eq!(input.get("c").unwrap().as_i64(), Some(30));
189    }
190
191    #[test]
192    fn a_null_field_is_present_even_though_it_is_empty() {
193        let input = Input::new().with("nickname", Json::Null);
194
195        assert!(input.has("nickname"));
196        assert!(!input.has("missing"));
197        assert!(input.get("nickname").unwrap().is_null());
198    }
199}