gotham/router/route/matcher/
accept.rs

1//! Defines the `AcceptHeaderRouterMatcher`.
2
3use hyper::header::{HeaderMap, ACCEPT};
4use hyper::StatusCode;
5use log::trace;
6use mime::Mime;
7
8use super::{LookupTable, LookupTableFromTypes};
9use crate::router::route::RouteMatcher;
10use crate::router::RouteNonMatch;
11use crate::state::{request_id, FromState, State};
12
13/// A mime type that is optionally weighted with a quality.
14struct QMime {
15    mime: Mime,
16    _weight: Option<f32>,
17}
18
19impl QMime {
20    fn new(mime: Mime, weight: Option<f32>) -> Self {
21        Self {
22            mime,
23            _weight: weight,
24        }
25    }
26}
27
28impl core::str::FromStr for QMime {
29    type Err = anyhow::Error;
30
31    fn from_str(str: &str) -> anyhow::Result<Self> {
32        match str.find(";q=") {
33            None => Ok(Self::new(str.parse()?, None)),
34            Some(index) => {
35                let mime = str[..index].parse()?;
36                let weight = str[index + 3..].parse()?;
37                Ok(Self::new(mime, Some(weight)))
38            }
39        }
40    }
41}
42
43/// A `RouteMatcher` that succeeds when the `Request` has been made with an `Accept` header that
44/// includes one or more supported media types. A missing `Accept` header, or the value of `*/*`
45/// will also positvely match. It supports the quality weighted syntax, but does not take the quality
46/// into consideration when matching.
47///
48/// # Examples
49///
50/// ```rust
51/// # fn main() {
52/// #   use hyper::header::{HeaderMap, ACCEPT};
53/// #   use gotham::state::State;
54/// #   use gotham::router::route::matcher::{AcceptHeaderRouteMatcher, RouteMatcher};
55/// #
56/// #   State::with_new(|state| {
57/// #
58/// let supported_media_types = vec![mime::APPLICATION_JSON, mime::IMAGE_STAR];
59/// let matcher = AcceptHeaderRouteMatcher::new(supported_media_types);
60///
61/// // No accept header
62/// state.put(HeaderMap::new());
63/// assert!(matcher.is_match(&state).is_ok());
64///
65/// // Accept header of `*/*`
66/// let mut headers = HeaderMap::new();
67/// headers.insert(ACCEPT, "*/*".parse().unwrap());
68/// state.put(headers);
69/// assert!(matcher.is_match(&state).is_ok());
70///
71/// // Accept header of `application/json`
72/// let mut headers = HeaderMap::new();
73/// headers.insert(ACCEPT, "application/json".parse().unwrap());
74/// state.put(headers);
75/// assert!(matcher.is_match(&state).is_ok());
76///
77/// // Not a valid Accept header
78/// let mut headers = HeaderMap::new();
79/// headers.insert(ACCEPT, "text/plain".parse().unwrap());
80/// state.put(headers);
81/// assert!(matcher.is_match(&state).is_err());
82///
83/// // At least one supported accept header
84/// let mut headers = HeaderMap::new();
85/// headers.insert(ACCEPT, "text/plain".parse().unwrap());
86/// headers.insert(ACCEPT, "application/json".parse().unwrap());
87/// state.put(headers);
88/// assert!(matcher.is_match(&state).is_ok());
89
90/// // Accept header of `image/*`
91/// let mut headers = HeaderMap::new();
92/// headers.insert(ACCEPT, "image/*".parse().unwrap());
93/// state.put(headers);
94/// assert!(matcher.is_match(&state).is_ok());
95/// #
96/// #   });
97/// # }
98/// ```
99#[derive(Clone)]
100pub struct AcceptHeaderRouteMatcher {
101    supported_media_types: Vec<mime::Mime>,
102    lookup_table: LookupTable,
103}
104
105impl AcceptHeaderRouteMatcher {
106    /// Creates a new `AcceptHeaderRouteMatcher`
107    pub fn new(supported_media_types: Vec<mime::Mime>) -> Self {
108        let lookup_table = LookupTable::from_types(supported_media_types.iter(), true);
109        Self {
110            supported_media_types,
111            lookup_table,
112        }
113    }
114}
115
116#[inline]
117fn err(state: &State) -> RouteNonMatch {
118    trace!(
119        "[{}] did not provide an Accept with media types supported by this Route",
120        request_id(state)
121    );
122
123    RouteNonMatch::new(StatusCode::NOT_ACCEPTABLE)
124}
125
126impl RouteMatcher for AcceptHeaderRouteMatcher {
127    /// Determines if the `Request` was made using an `Accept` header that includes one or more
128    /// supported media types. A missing `Accept` header, or the value of `*/*` will also positvely
129    /// match.
130    ///
131    /// Quality values within `Accept` header values are not considered by the matcher, as the
132    /// matcher is only able to indicate whether a successful match has been found.
133    fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
134        HeaderMap::borrow_from(state)
135            .get(ACCEPT)
136            .map(|header| {
137                // parse mime types from the accept header
138                let acceptable = header
139                    .to_str()
140                    .map_err(|_| err(state))?
141                    .split(',')
142                    .map(|str| str.trim().parse())
143                    .collect::<Result<Vec<QMime>, _>>()
144                    .map_err(|_| err(state))?;
145
146                for qmime in acceptable {
147                    // get mime type candidates from the lookup table
148                    let essence = qmime.mime.essence_str();
149                    let candidates = match self.lookup_table.get(essence) {
150                        Some(candidates) => candidates,
151                        None => continue,
152                    };
153                    for i in candidates {
154                        let candidate = &self.supported_media_types[*i];
155
156                        // check that the candidates have the same suffix - this is not included in the
157                        // essence string
158                        if candidate.suffix() != qmime.mime.suffix() && qmime.mime.subtype() != "*"
159                        {
160                            continue;
161                        }
162
163                        // this candidate matches - params don't play a role in accept header matching
164                        return Ok(());
165                    }
166                }
167
168                // no candidates found
169                Err(err(state))
170            })
171            .unwrap_or_else(|| {
172                // no accept header - assume all types are acceptable
173                Ok(())
174            })
175    }
176}
177
178#[cfg(test)]
179mod test {
180    use super::*;
181
182    fn with_state<F>(accept: Option<&str>, block: F)
183    where
184        F: FnOnce(&mut State),
185    {
186        State::with_new(|state| {
187            let mut headers = HeaderMap::new();
188            if let Some(acc) = accept {
189                headers.insert(ACCEPT, acc.parse().unwrap());
190            }
191            state.put(headers);
192            block(state);
193        });
194    }
195
196    #[test]
197    fn no_accept_header() {
198        let matcher = AcceptHeaderRouteMatcher::new(vec![mime::TEXT_PLAIN]);
199        with_state(None, |state| assert!(matcher.is_match(state).is_ok()));
200    }
201
202    #[test]
203    fn single_mime_type() {
204        let matcher = AcceptHeaderRouteMatcher::new(vec![mime::TEXT_PLAIN, mime::IMAGE_PNG]);
205        with_state(Some("text/plain"), |state| {
206            assert!(matcher.is_match(state).is_ok())
207        });
208        with_state(Some("text/html"), |state| {
209            assert!(matcher.is_match(state).is_err())
210        });
211        with_state(Some("image/png"), |state| {
212            assert!(matcher.is_match(state).is_ok())
213        });
214        with_state(Some("image/webp"), |state| {
215            assert!(matcher.is_match(state).is_err())
216        });
217    }
218
219    #[test]
220    fn star_star() {
221        let matcher = AcceptHeaderRouteMatcher::new(vec![mime::IMAGE_PNG]);
222        with_state(Some("*/*"), |state| {
223            assert!(matcher.is_match(state).is_ok())
224        });
225    }
226
227    #[test]
228    fn image_star() {
229        let matcher = AcceptHeaderRouteMatcher::new(vec![mime::IMAGE_PNG]);
230        with_state(Some("image/*"), |state| {
231            assert!(matcher.is_match(state).is_ok())
232        });
233    }
234
235    #[test]
236    fn suffix_matched_by_wildcard() {
237        let matcher = AcceptHeaderRouteMatcher::new(vec!["application/rss+xml".parse().unwrap()]);
238        with_state(Some("*/*"), |state| {
239            assert!(matcher.is_match(state).is_ok())
240        });
241        with_state(Some("application/*"), |state| {
242            assert!(matcher.is_match(state).is_ok())
243        });
244    }
245
246    #[test]
247    fn complex_header() {
248        let matcher = AcceptHeaderRouteMatcher::new(vec![mime::IMAGE_PNG]);
249        with_state(Some("text/html,image/webp;q=0.8"), |state| {
250            assert!(matcher.is_match(state).is_err())
251        });
252        with_state(Some("text/html,image/webp;q=0.8,*/*;q=0.1"), |state| {
253            assert!(matcher.is_match(state).is_ok())
254        });
255    }
256}