rustlavel_validation/
input.rs1use rustlavel_core::Json;
9use rustlavel_http::Request;
10use std::collections::BTreeMap;
11
12#[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 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 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 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 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 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 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 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}