Skip to main content

sofie_db/
input.rs

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
13/// The fields an input reads, so a query string's other keys are told apart before reading.
14pub trait FieldNames {
15    const FIELD_NAMES: &'static [&'static str];
16}
17
18/// No fields: reading it refuses every field given.
19impl 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    /// A form posts every value as a string, so one that is not a `T` is reread as what it spells.
79    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
127/// Every missing, invalid or unknown field is reported before the refusal of `try_new`.
128pub 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}