gotham/router/route/matcher/
accept.rs1use 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
13struct 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#[derive(Clone)]
100pub struct AcceptHeaderRouteMatcher {
101 supported_media_types: Vec<mime::Mime>,
102 lookup_table: LookupTable,
103}
104
105impl AcceptHeaderRouteMatcher {
106 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 fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
134 HeaderMap::borrow_from(state)
135 .get(ACCEPT)
136 .map(|header| {
137 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 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 if candidate.suffix() != qmime.mime.suffix() && qmime.mime.subtype() != "*"
159 {
160 continue;
161 }
162
163 return Ok(());
165 }
166 }
167
168 Err(err(state))
170 })
171 .unwrap_or_else(|| {
172 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}