1use std::fmt::Display;
2
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Deserializer};
5use serde_json::{Map, Value};
6
7use sofie_core::{Error, FieldError, Result};
8
9pub trait Input: Sized {
10 fn from_json(value: Value) -> Result<Self>;
11}
12
13pub trait FieldNames {
15 const FIELD_NAMES: &'static [&'static str];
16}
17
18impl Input for () {
20 fn from_json(value: Value) -> Result<Self> {
21 InputFields::new(value)?.finish()
22 }
23}
24
25impl FieldNames for () {
26 const FIELD_NAMES: &'static [&'static str] = &[];
27}
28
29pub trait Validate {
30 fn validate(&self) -> Result<(), Vec<FieldError>>;
31}
32
33pub struct InputFields {
34 object: Map<String, Value>,
35 errors: Vec<FieldError>,
36}
37
38impl InputFields {
39 pub fn new(value: Value) -> Result<Self> {
40 let Value::Object(object) = value else {
41 return Err(Error::BadRequest(
42 "the input must be a JSON object".to_owned(),
43 ));
44 };
45 Ok(Self {
46 object,
47 errors: Vec::new(),
48 })
49 }
50
51 pub fn take<T: DeserializeOwned>(&mut self, name: &str) -> Result<T> {
52 match self.object.remove(name) {
53 Some(value) => self.read(name, &value),
54 None => Err(self.refuse(FieldError::required(name))),
55 }
56 }
57
58 pub fn optional<T: DeserializeOwned>(&mut self, name: &str) -> Result<Option<T>> {
59 match self.object.remove(name) {
60 Some(value) => self.read(name, &value),
61 None => Ok(None),
62 }
63 }
64
65 pub fn finish(self) -> Result<()> {
66 let Self { object, mut errors } = self;
67 errors.extend(
68 object
69 .into_iter()
70 .map(|(field, _)| FieldError::new(field, "validation.unknown", "is not accepted")),
71 );
72 if errors.is_empty() {
73 return Ok(());
74 }
75 Err(Error::Invalid(errors))
76 }
77
78 fn read<T: DeserializeOwned>(&mut self, name: &str, value: &Value) -> Result<T> {
80 let error = match T::deserialize(value) {
81 Ok(read) => return Ok(read),
82 Err(error) => error,
83 };
84 if let Some(Ok(read)) = scalar_literal(value).map(T::deserialize) {
85 return Ok(read);
86 }
87 Err(self.refuse(FieldError::invalid(name, error.to_string())))
88 }
89
90 pub fn checked<T>(
91 &mut self,
92 read: Result<T>,
93 refusal: impl FnOnce(&T) -> Option<FieldError>,
94 ) -> Result<T> {
95 let value = read?;
96 match refusal(&value) {
97 Some(error) => Err(self.refuse(error)),
98 None => Ok(value),
99 }
100 }
101
102 fn refuse(&mut self, error: FieldError) -> Error {
103 self.errors.push(error.clone());
104 Error::Invalid(vec![error])
105 }
106}
107
108pub trait InputValue: DeserializeOwned {
109 const FIELD: &'static str;
110}
111
112pub fn lone_value<V: InputValue>(input: Value) -> Result<V> {
113 read_alone(input, |fields| fields.take(V::FIELD))
114}
115
116pub fn optional_lone_value<V: InputValue>(input: Value) -> Result<Option<V>> {
117 read_alone(input, |fields| fields.optional(V::FIELD))
118}
119
120fn read_alone<T>(input: Value, read: impl FnOnce(&mut InputFields) -> Result<T>) -> Result<T> {
121 let mut fields = InputFields::new(input)?;
122 let value = read(&mut fields);
123 fields.finish()?;
124 value
125}
126
127pub fn deserialize_fields<'de, V, E: Display, D: Deserializer<'de>>(
129 deserializer: D,
130 build: impl FnOnce(&mut InputFields) -> Result<Result<V, E>>,
131) -> Result<V, D::Error> {
132 let object = serde_json::Value::deserialize(deserializer)?;
133 match read_alone(object, build) {
134 Ok(Ok(value)) => Ok(value),
135 Ok(Err(refusal)) => Err(serde::de::Error::custom(format_args!("{refusal:#}"))),
136 Err(error) => Err(error.into_deserialize_error()),
137 }
138}
139
140fn scalar_literal(value: &Value) -> Option<Value> {
141 let Value::String(text) = value else {
142 return None;
143 };
144 serde_json::from_str(text)
145 .ok()
146 .filter(|literal| matches!(literal, Value::Number(_) | Value::Bool(_)))
147}
148
149#[cfg(test)]
150mod tests {
151 use serde_json::json;
152
153 use super::*;
154 use sofie_core::{Constraint, FieldRules};
155
156 fn invalid_fields(result: Result<()>) -> Vec<(String, String)> {
157 match result {
158 Err(Error::Invalid(errors)) => errors
159 .into_iter()
160 .map(|error| (error.field, error.message))
161 .collect(),
162 other => panic!("expected invalid input, got {other:?}"),
163 }
164 }
165
166 #[test]
167 fn reads_required_and_optional_fields() {
168 let mut fields = InputFields::new(json!({ "name": "Ada", "age": null })).unwrap();
169
170 assert_eq!(fields.take::<String>("name").unwrap(), "Ada");
171 assert_eq!(fields.optional::<i64>("age").unwrap(), None);
172 assert_eq!(fields.optional::<i64>("height").unwrap(), None);
173 assert!(fields.finish().is_ok());
174 }
175
176 #[test]
177 fn collects_missing_unacceptable_and_unknown_fields() {
178 let mut fields = InputFields::new(json!({ "age": "old", "colour": "red" })).unwrap();
179
180 assert!(fields.take::<String>("name").is_err());
181 assert!(fields.optional::<i64>("age").is_err());
182
183 let errors = invalid_fields(fields.finish());
184 let names: Vec<&str> = errors.iter().map(|(field, _)| field.as_str()).collect();
185 assert_eq!(names, ["name", "age", "colour"]);
186 assert_eq!(errors[0].1, "is required");
187 assert_eq!(errors[2].1, "is not accepted");
188 }
189
190 #[test]
191 fn reads_numbers_and_booleans_a_form_posts_as_strings() {
192 let posted = json!({ "count": "3", "gift": "true", "ratio": "0.5", "name": "7" });
193 let mut fields = InputFields::new(posted).unwrap();
194
195 assert_eq!(fields.take::<u32>("count").unwrap(), 3);
196 assert!(fields.take::<bool>("gift").unwrap());
197 assert_eq!(fields.optional::<f64>("ratio").unwrap(), Some(0.5));
198 assert_eq!(fields.take::<String>("name").unwrap(), "7");
199 assert!(fields.finish().is_ok());
200 }
201
202 #[test]
203 fn a_string_spelling_anything_else_keeps_its_first_error() {
204 let posted = json!({ "count": "[3]", "gift": "null", "size": "-1" });
205 let mut fields = InputFields::new(posted).unwrap();
206
207 assert!(fields.take::<Vec<u32>>("count").is_err());
208 assert!(fields.optional::<bool>("gift").is_err());
209 assert!(fields.take::<u32>("size").is_err());
210
211 let messages: Vec<String> = invalid_fields(fields.finish())
212 .into_iter()
213 .map(|(_, message)| message)
214 .collect();
215 assert_eq!(
216 messages,
217 [
218 "invalid type: string \"[3]\", expected a sequence",
219 "invalid type: string \"null\", expected a boolean",
220 "invalid type: string \"-1\", expected u32",
221 ]
222 );
223 }
224
225 #[test]
226 fn a_value_refusing_its_input_reports_only_its_messages() {
227 struct Even;
228 impl<'de> Deserialize<'de> for Even {
229 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
230 let number = u32::deserialize(deserializer)?;
231 if number % 2 == 1 {
232 return Err(Error::invalid("even", "must be even").into_deserialize_error());
233 }
234 Ok(Self)
235 }
236 }
237 let mut fields = InputFields::new(json!({ "count": 3 })).unwrap();
238
239 assert!(fields.take::<Even>("count").is_err());
240
241 assert_eq!(
242 invalid_fields(fields.finish()),
243 [("count".to_owned(), "must be even".to_owned())]
244 );
245 }
246
247 #[test]
248 fn keys_each_error_for_translation() {
249 let mut fields = InputFields::new(json!({ "age": "old", "colour": "red" })).unwrap();
250
251 assert!(fields.take::<String>("name").is_err());
252 assert!(fields.take::<i64>("age").is_err());
253
254 let Err(Error::Invalid(errors)) = fields.finish() else {
255 panic!("expected invalid input");
256 };
257 let keys: Vec<&str> = errors.iter().map(|error| error.key.as_ref()).collect();
258 assert_eq!(
259 keys,
260 [
261 "validation.required",
262 "validation.invalid",
263 "validation.unknown"
264 ]
265 );
266 }
267
268 #[test]
269 fn a_field_breaking_a_constraint_is_refused_with_the_others() {
270 let mut fields = InputFields::new(json!({ "name": "Ada Lovelace", "age": 3 })).unwrap();
271
272 let name_rules = FieldRules::<String>::new(&[Constraint::MaxLength(4)]);
273 let age_rules = FieldRules::<i64>::new(&[Constraint::Min(1)]);
274
275 let name = fields.take::<String>("name");
276 let name = fields.checked(name, |value| name_rules.refusal("name", value));
277 let age = fields.take::<i64>("age");
278 let age = fields.checked(age, |value| age_rules.refusal("age", value));
279
280 assert!(name.is_err());
281 assert_eq!(age.unwrap(), 3);
282 assert_eq!(
283 invalid_fields(fields.finish()),
284 [("name".to_owned(), "must be at most 4 characters".to_owned())]
285 );
286 }
287
288 #[test]
289 fn refuses_anything_but_an_object() {
290 assert!(matches!(
291 InputFields::new(json!([1])),
292 Err(Error::BadRequest(_))
293 ));
294 }
295
296 #[derive(Debug, PartialEq)]
297 struct Span {
298 low: i64,
299 high: i64,
300 }
301
302 fn read_span(value: serde_json::Value) -> Result<Span, serde_json::Error> {
303 deserialize_fields(value, |fields| {
304 let low = fields.take("low");
305 let high = fields.take("high");
306 let (low, high) = (low?, high?);
307 if low > high {
308 return Ok(Err("low must not exceed high"));
309 }
310 Ok(Ok(Span { low, high }))
311 })
312 }
313
314 #[test]
315 fn reads_several_fields_through_the_check() {
316 assert_eq!(
317 read_span(json!({"low": 1, "high": "2"})).unwrap(),
318 Span { low: 1, high: 2 }
319 );
320 let refused = read_span(json!({"low": 3, "high": 2})).unwrap_err();
321 assert_eq!(refused.to_string(), "low must not exceed high");
322 }
323
324 #[test]
325 fn reports_every_field_before_the_check() {
326 let missing = read_span(json!({"low": "x", "width": 2})).unwrap_err();
327
328 assert_eq!(
329 missing.to_string(),
330 "invalid type: string \"x\", expected i64; is required; is not accepted"
331 );
332 assert!(read_span(json!([1, 2])).is_err());
333 }
334}