Skip to main content

roas_http_validator/
decoder.rs

1//! Reading a media type this crate does not know how to read.
2//!
3//! The built-in decoders cover JSON, `application/x-www-form-urlencoded`
4//! and `text/*`. Everything else — `multipart/form-data`, XML, a
5//! protobuf-over-HTTP body — reports [`ErrorKind::Unsupported`] rather
6//! than guessing, which is honest but not much use to someone who has
7//! such a body and a schema for it.
8//!
9//! This is the way in. A caller registers a function that turns bytes
10//! into a [`Value`], and the schema takes it from there.
11//!
12//! It is deliberately a hook rather than more built-ins, for two
13//! reasons that differ by format. **Multipart** would mean owning a
14//! boundary parser and buffering file uploads, which is the one place
15//! this crate's "the caller decides what to buffer" posture matters
16//! most. **XML** has no specified mapping onto a JSON Schema instance
17//! at all — OpenAPI's XML Object is serialization metadata for code
18//! generators, so any translation is a choice, and implementations make
19//! different ones. Better to take the caller's choice than to invent
20//! one and report violations against it.
21//!
22//! ```
23//! use roas_http_validator::Options;
24//!
25//! let options = Options::new().decoder("application/xml", |bytes, _media_type| {
26//!     let text = std::str::from_utf8(bytes).map_err(|error| error.to_string())?;
27//!     // Whatever mapping your clients actually use.
28//!     Ok(serde_json::json!({ "raw": text }))
29//! });
30//! ```
31//!
32//! [`ErrorKind::Unsupported`]: crate::ErrorKind::Unsupported
33
34use std::sync::Arc;
35
36use serde_json::Value;
37
38/// Turns a body of one media type into the value its schema judges.
39///
40/// Called with the bytes and the media type they arrived as — the
41/// latter matters when one decoder is registered for a range like
42/// `text/*`. An `Err` is reported as a malformed body, carrying the
43/// reason given.
44pub type Decoder = Arc<dyn Fn(&[u8], &str) -> Result<Value, String> + Send + Sync>;
45
46/// The decoders one validator was given, looked up the way a Media Type
47/// Object is: exact match first, then a `type/*` range, then `*/*`.
48#[derive(Clone, Default)]
49pub(crate) struct Decoders {
50    entries: Vec<(String, Decoder)>,
51}
52
53impl Decoders {
54    /// Register `decoder` for `media_type`, replacing any already there.
55    pub(crate) fn insert(&mut self, media_type: &str, decoder: Decoder) {
56        let key = normalize(media_type);
57        match self.entries.iter_mut().find(|(known, _)| *known == key) {
58            Some(entry) => entry.1 = decoder,
59            None => self.entries.push((key, decoder)),
60        }
61    }
62
63    /// The media types registered, for `Debug`.
64    pub(crate) fn media_types(&self) -> impl Iterator<Item = &str> {
65        self.entries
66            .iter()
67            .map(|(media_type, _)| media_type.as_str())
68    }
69
70    /// The decoder for `media_type`, most specific first.
71    pub(crate) fn find(&self, media_type: &str) -> Option<&Decoder> {
72        let media_type = normalize(media_type);
73        let range = media_type
74            .split_once('/')
75            .map(|(kind, _)| format!("{kind}/*"));
76
77        let mut best: Option<(usize, &Decoder)> = None;
78        for (key, decoder) in &self.entries {
79            // Lower is more specific, so the exact key always wins.
80            let rank = if *key == media_type {
81                0
82            } else if range.as_ref() == Some(key) {
83                1
84            } else if key == "*/*" {
85                2
86            } else {
87                continue;
88            };
89            if best.is_none_or(|(known, _)| rank < known) {
90                best = Some((rank, decoder));
91            }
92        }
93        best.map(|(_, decoder)| decoder)
94    }
95}
96
97/// A media type without its parameters, lowercased — the same shape
98/// [`crate::RequestView::content_type`] produces.
99fn normalize(media_type: &str) -> String {
100    media_type
101        .split(';')
102        .next()
103        .unwrap_or(media_type)
104        .trim()
105        .to_ascii_lowercase()
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    fn decoder(tag: &'static str) -> Decoder {
113        Arc::new(move |_bytes: &[u8], _media_type: &str| Ok(Value::String(tag.to_owned())))
114    }
115
116    fn found(decoders: &Decoders, media_type: &str) -> Option<String> {
117        let decoder = decoders.find(media_type)?;
118        match decoder(b"", media_type) {
119            Ok(Value::String(tag)) => Some(tag),
120            other => panic!("unexpected {other:?}"),
121        }
122    }
123
124    #[test]
125    fn an_exact_media_type_is_found() {
126        let mut decoders = Decoders::default();
127        decoders.insert("application/xml", decoder("xml"));
128        assert_eq!(found(&decoders, "application/xml").as_deref(), Some("xml"));
129        assert_eq!(found(&decoders, "application/json"), None);
130    }
131
132    #[test]
133    fn parameters_and_case_do_not_hide_a_decoder() {
134        let mut decoders = Decoders::default();
135        decoders.insert("Application/XML; charset=utf-8", decoder("xml"));
136        assert_eq!(found(&decoders, "application/xml").as_deref(), Some("xml"));
137    }
138
139    #[test]
140    fn a_range_catches_what_no_exact_key_does() {
141        let mut decoders = Decoders::default();
142        decoders.insert("text/*", decoder("range"));
143        decoders.insert("*/*", decoder("any"));
144        assert_eq!(found(&decoders, "text/csv").as_deref(), Some("range"));
145        assert_eq!(found(&decoders, "image/png").as_deref(), Some("any"));
146    }
147
148    #[test]
149    fn the_most_specific_registration_wins() {
150        let mut decoders = Decoders::default();
151        decoders.insert("*/*", decoder("any"));
152        decoders.insert("text/*", decoder("range"));
153        decoders.insert("text/csv", decoder("exact"));
154        assert_eq!(found(&decoders, "text/csv").as_deref(), Some("exact"));
155        assert_eq!(found(&decoders, "text/plain").as_deref(), Some("range"));
156    }
157
158    #[test]
159    fn registering_the_same_media_type_twice_replaces_it() {
160        let mut decoders = Decoders::default();
161        decoders.insert("text/csv", decoder("first"));
162        decoders.insert("text/csv", decoder("second"));
163        assert_eq!(found(&decoders, "text/csv").as_deref(), Some("second"));
164        assert_eq!(decoders.media_types().count(), 1);
165    }
166
167    #[test]
168    fn an_empty_registry_finds_nothing() {
169        assert!(Decoders::default().find("text/csv").is_none());
170    }
171}