Skip to main content

rainier_http/
coerce.rs

1//! A `serde` deserialiser that coerces strings into scalars.
2//!
3//! Query strings, urlencoded forms and route parameters are **all strings** —
4//! `?page=2` carries no clue that `2` is a number. Feeding that straight to
5//! `serde_json::from_value` means `Query<Pagination>` with a `page: u32` field
6//! fails on every request, which would make typed extractors useless for
7//! exactly the inputs they are most wanted for.
8//!
9//! So this deserialiser sits in front: when the target type asks for a number,
10//! a bool or a char and the value is a string, it parses. When the target asks
11//! for a string and the value is a number or bool, it stringifies. Everything
12//! else behaves as `serde_json` does.
13//!
14//! It deliberately does **not** treat `""` as absent. That conversion is
15//! policy, not encoding — Rainier makes it an explicit middleware
16//! (`ConvertEmptyStringsToNull`), so an application can
17//! decide whether an empty text input means "null" or "empty string".
18
19use std::fmt;
20
21use serde::de::{
22    self, DeserializeOwned, DeserializeSeed, EnumAccess, IntoDeserializer, MapAccess, SeqAccess,
23    VariantAccess, Visitor,
24};
25use serde::forward_to_deserialize_any;
26use serde_json::{Map, Value};
27
28/// Deserialise `value` into `T`, coercing string scalars to the requested type.
29pub fn from_value<T: DeserializeOwned>(value: &Value) -> Result<T, CoerceError> {
30    T::deserialize(Coercing::new(value))
31}
32
33/// A deserialisation failure.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct CoerceError {
36    message: String,
37}
38
39impl CoerceError {
40    fn new(message: impl Into<String>) -> Self {
41        Self { message: message.into() }
42    }
43
44    /// The failure message.
45    pub fn message(&self) -> &str {
46        &self.message
47    }
48}
49
50impl fmt::Display for CoerceError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str(&self.message)
53    }
54}
55
56impl std::error::Error for CoerceError {}
57
58impl de::Error for CoerceError {
59    fn custom<T: fmt::Display>(message: T) -> Self {
60        Self::new(message.to_string())
61    }
62}
63
64/// A `serde` deserialiser over a borrowed [`Value`].
65pub struct Coercing<'de> {
66    value: &'de Value,
67}
68
69impl<'de> Coercing<'de> {
70    /// Wrap a value.
71    pub fn new(value: &'de Value) -> Self {
72        Self { value }
73    }
74
75    fn kind(&self) -> &'static str {
76        match self.value {
77            Value::Null => "null",
78            Value::Bool(_) => "a boolean",
79            Value::Number(_) => "a number",
80            Value::String(_) => "a string",
81            Value::Array(_) => "an array",
82            Value::Object(_) => "an object",
83        }
84    }
85
86    fn invalid(&self, wanted: &str) -> CoerceError {
87        CoerceError::new(format!("expected {wanted}, found {}", self.kind()))
88    }
89}
90
91/// Implements one numeric `deserialize_*`: parse from a string, narrow from a
92/// JSON number, otherwise fail.
93macro_rules! coerce_number {
94    ($method:ident, $ty:ty, $visit:ident) => {
95        fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
96            let parsed: $ty = match self.value {
97                Value::String(raw) => raw.trim().parse().map_err(|_| {
98                    CoerceError::new(format!(
99                        "`{raw}` is not a valid {}",
100                        std::any::type_name::<$ty>()
101                    ))
102                })?,
103                Value::Number(n) => n.to_string().parse().map_err(|_| {
104                    CoerceError::new(format!(
105                        "{n} does not fit in a {}",
106                        std::any::type_name::<$ty>()
107                    ))
108                })?,
109                // A bool coerces to 0/1, which is what `?flag=true` into a
110                // numeric field should mean if anyone writes it.
111                Value::Bool(b) => {
112                    if *b {
113                        1 as $ty
114                    } else {
115                        0 as $ty
116                    }
117                }
118                _ => return Err(self.invalid("a number")),
119            };
120            visitor.$visit(parsed)
121        }
122    };
123}
124
125impl<'de> de::Deserializer<'de> for Coercing<'de> {
126    type Error = CoerceError;
127
128    fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
129        match self.value {
130            Value::Null => visitor.visit_unit(),
131            Value::Bool(b) => visitor.visit_bool(*b),
132            Value::String(s) => visitor.visit_str(s),
133            Value::Number(n) => {
134                if let Some(i) = n.as_i64() {
135                    visitor.visit_i64(i)
136                } else if let Some(u) = n.as_u64() {
137                    visitor.visit_u64(u)
138                } else if let Some(f) = n.as_f64() {
139                    visitor.visit_f64(f)
140                } else {
141                    Err(CoerceError::new("unrepresentable number"))
142                }
143            }
144            Value::Array(items) => visitor.visit_seq(SeqReader { items, index: 0 }),
145            Value::Object(map) => visitor.visit_map(MapReader {
146                entries: map.iter().collect(),
147                index: 0,
148                value: None,
149            }),
150        }
151    }
152
153    fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
154        let parsed = match self.value {
155            Value::Bool(b) => *b,
156            // The set a checkbox or a query flag actually arrives as.
157            Value::String(raw) => match raw.trim().to_ascii_lowercase().as_str() {
158                "true" | "1" | "on" | "yes" => true,
159                "false" | "0" | "off" | "no" => false,
160                other => return Err(CoerceError::new(format!("`{other}` is not a valid boolean"))),
161            },
162            Value::Number(n) => n.as_i64().is_some_and(|i| i != 0),
163            _ => return Err(self.invalid("a boolean")),
164        };
165        visitor.visit_bool(parsed)
166    }
167
168    coerce_number!(deserialize_i8, i8, visit_i8);
169    coerce_number!(deserialize_i16, i16, visit_i16);
170    coerce_number!(deserialize_i32, i32, visit_i32);
171    coerce_number!(deserialize_i64, i64, visit_i64);
172    coerce_number!(deserialize_u8, u8, visit_u8);
173    coerce_number!(deserialize_u16, u16, visit_u16);
174    coerce_number!(deserialize_u32, u32, visit_u32);
175    coerce_number!(deserialize_u64, u64, visit_u64);
176    coerce_number!(deserialize_f32, f32, visit_f32);
177    coerce_number!(deserialize_f64, f64, visit_f64);
178
179    fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
180        match self.value {
181            Value::String(s) if s.chars().count() == 1 => {
182                visitor.visit_char(s.chars().next().expect("just counted one"))
183            }
184            Value::String(_) => Err(CoerceError::new("expected a single character")),
185            _ => Err(self.invalid("a character")),
186        }
187    }
188
189    fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
190        self.deserialize_string(visitor)
191    }
192
193    fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
194        match self.value {
195            Value::String(s) => visitor.visit_str(s),
196            // A JSON body may legitimately hold a number where the struct
197            // wants a string (an id, a postcode). Stringify rather than fail.
198            Value::Number(n) => visitor.visit_string(n.to_string()),
199            Value::Bool(b) => visitor.visit_string(b.to_string()),
200            _ => Err(self.invalid("a string")),
201        }
202    }
203
204    fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
205        match self.value {
206            Value::String(s) => visitor.visit_bytes(s.as_bytes()),
207            _ => Err(self.invalid("bytes")),
208        }
209    }
210
211    fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
212        self.deserialize_bytes(visitor)
213    }
214
215    fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
216        match self.value {
217            Value::Null => visitor.visit_none(),
218            _ => visitor.visit_some(self),
219        }
220    }
221
222    fn deserialize_unit<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
223        match self.value {
224            Value::Null => visitor.visit_unit(),
225            _ => Err(self.invalid("null")),
226        }
227    }
228
229    fn deserialize_unit_struct<V: Visitor<'de>>(
230        self,
231        _name: &'static str,
232        visitor: V,
233    ) -> Result<V::Value, CoerceError> {
234        self.deserialize_unit(visitor)
235    }
236
237    fn deserialize_newtype_struct<V: Visitor<'de>>(
238        self,
239        _name: &'static str,
240        visitor: V,
241    ) -> Result<V::Value, CoerceError> {
242        visitor.visit_newtype_struct(self)
243    }
244
245    fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
246        match self.value {
247            Value::Array(items) => visitor.visit_seq(SeqReader { items, index: 0 }),
248            // `?tag=x` where the field is `Vec<String>`: a single value is a
249            // one-element list. Without this, whether `tags` parses would
250            // depend on how many were submitted.
251            Value::Null => visitor.visit_seq(SeqReader { items: &[], index: 0 }),
252            single => visitor.visit_seq(SingleReader { value: Some(single) }),
253        }
254    }
255
256    fn deserialize_tuple<V: Visitor<'de>>(
257        self,
258        _len: usize,
259        visitor: V,
260    ) -> Result<V::Value, CoerceError> {
261        self.deserialize_seq(visitor)
262    }
263
264    fn deserialize_tuple_struct<V: Visitor<'de>>(
265        self,
266        _name: &'static str,
267        _len: usize,
268        visitor: V,
269    ) -> Result<V::Value, CoerceError> {
270        self.deserialize_seq(visitor)
271    }
272
273    fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
274        match self.value {
275            Value::Object(map) => visitor.visit_map(MapReader {
276                entries: map.iter().collect(),
277                index: 0,
278                value: None,
279            }),
280            _ => Err(self.invalid("an object")),
281        }
282    }
283
284    fn deserialize_struct<V: Visitor<'de>>(
285        self,
286        _name: &'static str,
287        _fields: &'static [&'static str],
288        visitor: V,
289    ) -> Result<V::Value, CoerceError> {
290        self.deserialize_map(visitor)
291    }
292
293    fn deserialize_enum<V: Visitor<'de>>(
294        self,
295        _name: &'static str,
296        _variants: &'static [&'static str],
297        visitor: V,
298    ) -> Result<V::Value, CoerceError> {
299        match self.value {
300            // `?status=active` — a unit variant by name.
301            Value::String(name) => visitor.visit_enum(name.as_str().into_deserializer()),
302            // `{"kind": {...}}` — an externally tagged variant.
303            Value::Object(map) if map.len() == 1 => {
304                let (name, value) = map.iter().next().expect("just checked len == 1");
305                visitor.visit_enum(EnumReader { name, value })
306            }
307            _ => Err(self.invalid("an enum variant")),
308        }
309    }
310
311    fn deserialize_identifier<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, CoerceError> {
312        self.deserialize_string(visitor)
313    }
314
315    forward_to_deserialize_any! { ignored_any }
316}
317
318struct SeqReader<'de> {
319    items: &'de [Value],
320    index: usize,
321}
322
323impl<'de> SeqAccess<'de> for SeqReader<'de> {
324    type Error = CoerceError;
325
326    fn next_element_seed<T: DeserializeSeed<'de>>(
327        &mut self,
328        seed: T,
329    ) -> Result<Option<T::Value>, CoerceError> {
330        let Some(value) = self.items.get(self.index) else {
331            return Ok(None);
332        };
333        self.index += 1;
334        seed.deserialize(Coercing::new(value)).map(Some)
335    }
336
337    fn size_hint(&self) -> Option<usize> {
338        Some(self.items.len() - self.index)
339    }
340}
341
342/// Presents one scalar as a one-element sequence.
343struct SingleReader<'de> {
344    value: Option<&'de Value>,
345}
346
347impl<'de> SeqAccess<'de> for SingleReader<'de> {
348    type Error = CoerceError;
349
350    fn next_element_seed<T: DeserializeSeed<'de>>(
351        &mut self,
352        seed: T,
353    ) -> Result<Option<T::Value>, CoerceError> {
354        match self.value.take() {
355            Some(value) => seed.deserialize(Coercing::new(value)).map(Some),
356            None => Ok(None),
357        }
358    }
359
360    fn size_hint(&self) -> Option<usize> {
361        Some(self.value.is_some() as usize)
362    }
363}
364
365struct MapReader<'de> {
366    entries: Vec<(&'de String, &'de Value)>,
367    index: usize,
368    value: Option<&'de Value>,
369}
370
371impl<'de> MapAccess<'de> for MapReader<'de> {
372    type Error = CoerceError;
373
374    fn next_key_seed<K: DeserializeSeed<'de>>(
375        &mut self,
376        seed: K,
377    ) -> Result<Option<K::Value>, CoerceError> {
378        let Some((key, value)) = self.entries.get(self.index) else {
379            return Ok(None);
380        };
381        self.index += 1;
382        self.value = Some(value);
383        seed.deserialize(key.as_str().into_deserializer()).map(Some)
384    }
385
386    fn next_value_seed<V: DeserializeSeed<'de>>(
387        &mut self,
388        seed: V,
389    ) -> Result<V::Value, CoerceError> {
390        let value =
391            self.value.take().ok_or_else(|| CoerceError::new("value requested before its key"))?;
392        seed.deserialize(Coercing::new(value))
393    }
394
395    fn size_hint(&self) -> Option<usize> {
396        Some(self.entries.len() - self.index)
397    }
398}
399
400struct EnumReader<'de> {
401    name: &'de String,
402    value: &'de Value,
403}
404
405impl<'de> EnumAccess<'de> for EnumReader<'de> {
406    type Error = CoerceError;
407    type Variant = Self;
408
409    fn variant_seed<V: DeserializeSeed<'de>>(
410        self,
411        seed: V,
412    ) -> Result<(V::Value, Self), CoerceError> {
413        let name = seed.deserialize(self.name.as_str().into_deserializer())?;
414        Ok((name, self))
415    }
416}
417
418impl<'de> VariantAccess<'de> for EnumReader<'de> {
419    type Error = CoerceError;
420
421    fn unit_variant(self) -> Result<(), CoerceError> {
422        Ok(())
423    }
424
425    fn newtype_variant_seed<T: DeserializeSeed<'de>>(
426        self,
427        seed: T,
428    ) -> Result<T::Value, CoerceError> {
429        seed.deserialize(Coercing::new(self.value))
430    }
431
432    fn tuple_variant<V: Visitor<'de>>(
433        self,
434        _len: usize,
435        visitor: V,
436    ) -> Result<V::Value, CoerceError> {
437        de::Deserializer::deserialize_seq(Coercing::new(self.value), visitor)
438    }
439
440    fn struct_variant<V: Visitor<'de>>(
441        self,
442        _fields: &'static [&'static str],
443        visitor: V,
444    ) -> Result<V::Value, CoerceError> {
445        de::Deserializer::deserialize_map(Coercing::new(self.value), visitor)
446    }
447}
448
449/// Build a JSON object from string pairs — how route parameters reach the
450/// deserialiser.
451pub fn object_from_strings<'a>(pairs: impl IntoIterator<Item = (&'a String, &'a String)>) -> Value {
452    let mut map = Map::new();
453    for (key, value) in pairs {
454        map.insert(key.clone(), Value::String(value.clone()));
455    }
456    Value::Object(map)
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use serde::Deserialize;
463    use serde_json::json;
464
465    #[derive(Debug, PartialEq, Deserialize)]
466    struct Pagination {
467        page: u32,
468        per_page: Option<u16>,
469        sort: String,
470    }
471
472    #[test]
473    fn coerces_strings_into_numbers() {
474        let parsed: Pagination =
475            from_value(&json!({ "page": "2", "per_page": "50", "sort": "name" })).unwrap();
476        assert_eq!(parsed, Pagination { page: 2, per_page: Some(50), sort: "name".into() });
477    }
478
479    #[test]
480    fn real_numbers_still_work() {
481        let parsed: Pagination =
482            from_value(&json!({ "page": 2, "per_page": 50, "sort": "name" })).unwrap();
483        assert_eq!(parsed.page, 2);
484    }
485
486    #[test]
487    fn numbers_coerce_into_strings() {
488        #[derive(Deserialize)]
489        struct Row {
490            postcode: String,
491        }
492        let parsed: Row = from_value(&json!({ "postcode": 1234 })).unwrap();
493        assert_eq!(parsed.postcode, "1234");
494    }
495
496    #[test]
497    fn coerces_the_booleans_a_form_actually_sends() {
498        #[derive(Deserialize)]
499        struct Flags {
500            a: bool,
501            b: bool,
502            c: bool,
503            d: bool,
504        }
505        let parsed: Flags =
506            from_value(&json!({ "a": "1", "b": "true", "c": "on", "d": "no" })).unwrap();
507        assert!(parsed.a && parsed.b && parsed.c && !parsed.d);
508    }
509
510    #[test]
511    fn a_missing_optional_is_none() {
512        let parsed: Pagination = from_value(&json!({ "page": "1", "sort": "id" })).unwrap();
513        assert_eq!(parsed.per_page, None);
514    }
515
516    #[test]
517    fn an_explicit_null_optional_is_none() {
518        let parsed: Pagination =
519            from_value(&json!({ "page": "1", "per_page": null, "sort": "id" })).unwrap();
520        assert_eq!(parsed.per_page, None);
521    }
522
523    #[test]
524    fn an_empty_string_is_not_silently_treated_as_absent() {
525        // Policy belongs to `ConvertEmptyStringsToNull`, not the encoding.
526        let err = from_value::<Pagination>(&json!({ "page": "1", "per_page": "", "sort": "id" }))
527            .unwrap_err();
528        assert!(err.message().contains("not a valid"), "{}", err.message());
529    }
530
531    #[test]
532    fn an_unparsable_number_names_the_value() {
533        let err = from_value::<Pagination>(&json!({ "page": "abc", "sort": "id" })).unwrap_err();
534        assert!(err.message().contains("`abc`"), "{}", err.message());
535    }
536
537    #[test]
538    fn a_single_value_deserialises_as_a_one_element_list() {
539        #[derive(Deserialize)]
540        struct Tags {
541            tag: Vec<String>,
542        }
543        let one: Tags = from_value(&json!({ "tag": "rust" })).unwrap();
544        assert_eq!(one.tag, vec!["rust"]);
545
546        let many: Tags = from_value(&json!({ "tag": ["rust", "web"] })).unwrap();
547        assert_eq!(many.tag, vec!["rust", "web"]);
548    }
549
550    #[test]
551    fn nested_structures_coerce_all_the_way_down() {
552        #[derive(Debug, PartialEq, Deserialize)]
553        struct Inner {
554            n: i64,
555        }
556        #[derive(Debug, PartialEq, Deserialize)]
557        struct Outer {
558            inner: Inner,
559            list: Vec<Inner>,
560        }
561
562        let parsed: Outer =
563            from_value(&json!({ "inner": { "n": "1" }, "list": [{ "n": "2" }, { "n": "3" }] }))
564                .unwrap();
565        assert_eq!(parsed.inner.n, 1);
566        assert_eq!(parsed.list, vec![Inner { n: 2 }, Inner { n: 3 }]);
567    }
568
569    #[test]
570    fn unit_enum_variants_come_from_strings() {
571        #[derive(Debug, PartialEq, Deserialize)]
572        #[serde(rename_all = "lowercase")]
573        enum Status {
574            Active,
575            Archived,
576        }
577        #[derive(Debug, PartialEq, Deserialize)]
578        struct Filter {
579            status: Status,
580        }
581
582        let parsed: Filter = from_value(&json!({ "status": "archived" })).unwrap();
583        assert_eq!(parsed.status, Status::Archived);
584    }
585
586    #[test]
587    fn deserialises_a_bare_scalar() {
588        assert_eq!(from_value::<u64>(&json!("42")).unwrap(), 42);
589        assert_eq!(from_value::<String>(&json!("x")).unwrap(), "x");
590        assert!(from_value::<bool>(&json!("yes")).unwrap());
591    }
592
593    #[test]
594    fn a_wrong_shape_reports_what_it_found() {
595        let err = from_value::<Pagination>(&json!("nope")).unwrap_err();
596        assert!(err.message().contains("found a string"), "{}", err.message());
597    }
598
599    #[test]
600    fn out_of_range_numbers_are_rejected() {
601        assert!(from_value::<u8>(&json!("300")).is_err());
602        assert!(from_value::<i32>(&json!("-1")).is_ok());
603        assert!(from_value::<u32>(&json!("-1")).is_err());
604    }
605
606    #[test]
607    fn builds_an_object_from_string_pairs() {
608        let id = ("id".to_string(), "7".to_string());
609        let slug = ("slug".to_string(), "hello".to_string());
610        let value = object_from_strings([(&id.0, &id.1), (&slug.0, &slug.1)]);
611        assert_eq!(value, json!({ "id": "7", "slug": "hello" }));
612
613        #[derive(Deserialize)]
614        struct Params {
615            id: u64,
616            slug: String,
617        }
618        let parsed: Params = from_value(&value).unwrap();
619        assert_eq!(parsed.id, 7);
620        assert_eq!(parsed.slug, "hello");
621    }
622}