use http::header::{HeaderMap, ACCEPT};
use http::StatusCode;
use log::trace;
use mime::Mime;
use super::{LookupTable, LookupTableFromTypes};
use crate::router::route::RouteMatcher;
use crate::router::RouteNonMatch;
use crate::state::{request_id, FromState, State};
struct QMime {
mime: Mime,
_weight: Option<f32>,
}
impl QMime {
fn new(mime: Mime, weight: Option<f32>) -> Self {
Self {
mime,
_weight: weight,
}
}
}
impl core::str::FromStr for QMime {
type Err = anyhow::Error;
fn from_str(str: &str) -> anyhow::Result<Self> {
match str.find(";q=") {
None => Ok(Self::new(str.parse()?, None)),
Some(index) => {
let mime = str[..index].parse()?;
let weight = str[index + 3..].parse()?;
Ok(Self::new(mime, Some(weight)))
}
}
}
}
#[derive(Clone)]
pub struct AcceptHeaderRouteMatcher {
supported_media_types: Vec<mime::Mime>,
lookup_table: LookupTable,
}
impl AcceptHeaderRouteMatcher {
pub fn new(supported_media_types: Vec<mime::Mime>) -> Self {
let lookup_table = LookupTable::from_types(supported_media_types.iter(), true);
Self {
supported_media_types,
lookup_table,
}
}
}
#[inline]
fn err(state: &State) -> RouteNonMatch {
trace!(
"[{}] did not provide an Accept with media types supported by this Route",
request_id(state)
);
RouteNonMatch::new(StatusCode::NOT_ACCEPTABLE)
}
impl RouteMatcher for AcceptHeaderRouteMatcher {
fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
HeaderMap::borrow_from(state)
.get(ACCEPT)
.map(|header| {
let acceptable = header
.to_str()
.map_err(|_| err(state))?
.split(',')
.map(|str| str.trim().parse())
.collect::<Result<Vec<QMime>, _>>()
.map_err(|_| err(state))?;
for qmime in acceptable {
let essence = qmime.mime.essence_str();
let candidates = match self.lookup_table.get(essence) {
Some(candidates) => candidates,
None => continue,
};
for i in candidates {
let candidate = &self.supported_media_types[*i];
if candidate.suffix() != qmime.mime.suffix() && qmime.mime.subtype() != "*"
{
continue;
}
return Ok(());
}
}
Err(err(state))
})
.unwrap_or_else(|| {
Ok(())
})
}
}
#[cfg(test)]
mod test {
use super::*;
fn with_state<F>(accept: Option<&str>, block: F)
where
F: FnOnce(&mut State),
{
State::with_new(|state| {
let mut headers = HeaderMap::new();
if let Some(acc) = accept {
headers.insert(ACCEPT, acc.parse().unwrap());
}
state.put(headers);
block(state);
});
}
#[test]
fn no_accept_header() {
let matcher = AcceptHeaderRouteMatcher::new(vec![mime::TEXT_PLAIN]);
with_state(None, |state| assert!(matcher.is_match(state).is_ok()));
}
#[test]
fn single_mime_type() {
let matcher = AcceptHeaderRouteMatcher::new(vec![mime::TEXT_PLAIN, mime::IMAGE_PNG]);
with_state(Some("text/plain"), |state| {
assert!(matcher.is_match(state).is_ok())
});
with_state(Some("text/html"), |state| {
assert!(matcher.is_match(state).is_err())
});
with_state(Some("image/png"), |state| {
assert!(matcher.is_match(state).is_ok())
});
with_state(Some("image/webp"), |state| {
assert!(matcher.is_match(state).is_err())
});
}
#[test]
fn star_star() {
let matcher = AcceptHeaderRouteMatcher::new(vec![mime::IMAGE_PNG]);
with_state(Some("*/*"), |state| {
assert!(matcher.is_match(state).is_ok())
});
}
#[test]
fn image_star() {
let matcher = AcceptHeaderRouteMatcher::new(vec![mime::IMAGE_PNG]);
with_state(Some("image/*"), |state| {
assert!(matcher.is_match(state).is_ok())
});
}
#[test]
fn suffix_matched_by_wildcard() {
let matcher = AcceptHeaderRouteMatcher::new(vec!["application/rss+xml".parse().unwrap()]);
with_state(Some("*/*"), |state| {
assert!(matcher.is_match(state).is_ok())
});
with_state(Some("application/*"), |state| {
assert!(matcher.is_match(state).is_ok())
});
}
#[test]
fn complex_header() {
let matcher = AcceptHeaderRouteMatcher::new(vec![mime::IMAGE_PNG]);
with_state(Some("text/html,image/webp;q=0.8"), |state| {
assert!(matcher.is_match(state).is_err())
});
with_state(Some("text/html,image/webp;q=0.8,*/*;q=0.1"), |state| {
assert!(matcher.is_match(state).is_ok())
});
}
}