From c1cb0e692a3370d12151c35ac216efc325cfdc3d Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 20 May 2020 09:33:12 +0200 Subject: [PATCH] gotham finally has a release candidate --- Cargo.toml | 6 +- example/Cargo.toml | 4 +- src/matcher/accept.rs | 217 ------------------------------------ src/matcher/content_type.rs | 173 ---------------------------- src/matcher/mod.rs | 35 ------ src/routing.rs | 11 +- 6 files changed, 10 insertions(+), 436 deletions(-) delete mode 100644 src/matcher/accept.rs delete mode 100644 src/matcher/content_type.rs diff --git a/Cargo.toml b/Cargo.toml index 78e61d2..ef44aa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,9 @@ chrono = { version = "0.4.11", features = ["serde"], optional = true } cookie = { version = "0.13.3", optional = true } futures-core = "0.3.4" futures-util = "0.3.4" -gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } -gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } +gotham = { version = "0.5.0-rc.1", default-features = false } +gotham_derive = "0.5.0-rc.1" +gotham_middleware_diesel = { version = "0.1.2", optional = true } gotham_restful_derive = { version = "0.1.0-dev" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" diff --git a/example/Cargo.toml b/example/Cargo.toml index d594b89..763ef0c 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -15,8 +15,8 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] fake = "2.2" -gotham = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } -gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } +gotham = { version = "0.5.0-rc.1", default-features = false } +gotham_derive = "0.5.0-rc.1" gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] } log = "0.4.8" log4rs = { version = "0.12", features = ["console_appender"], default-features = false } diff --git a/src/matcher/accept.rs b/src/matcher/accept.rs deleted file mode 100644 index 9aa1d91..0000000 --- a/src/matcher/accept.rs +++ /dev/null @@ -1,217 +0,0 @@ -use super::{LookupTable, LookupTableFromTypes}; -use gotham::{ - hyper::{ - header::{HeaderMap, ACCEPT}, - StatusCode - }, - router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, - state::{FromState, State} -}; -use mime::Mime; -use std::{ - num::ParseFloatError, - str::FromStr -}; -use thiserror::Error; - - -/// A mime type that is optionally weighted with a quality. -#[derive(Debug)] -struct QMime -{ - mime : Mime, - weight : Option -} - -impl QMime -{ - fn new(mime : Mime, weight : Option) -> Self - { - Self { mime, weight } - } -} - -#[derive(Debug, Error)] -enum QMimeError -{ - #[error("Unable to parse mime type: {0}")] - MimeError(#[from] mime::FromStrError), - #[error("Unable to parse mime quality: {0}")] - NumError(#[from] ParseFloatError) -} - -impl FromStr for QMime -{ - type Err = QMimeError; - - fn from_str(str : &str) -> Result - { - 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))) - } - } - } -} - - -/** -A route matcher that checks whether the supported types match the accept header of the request. - -Usage: - -``` -# use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; -# use gotham_restful::matcher::AcceptHeaderMatcher; -# -# const img_content : &[u8] = b"This is the content of a webp image"; -# -# let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); -let types = vec![IMAGE_WEBP]; -let matcher = AcceptHeaderMatcher::new(types); - -# build_simple_router(|route| { -// use the matcher for your request -route.post("/foo") - .extend_route_matcher(matcher) - .to(|state| { - // we know that the client is a modern browser and can handle webp images -# let IMAGE_WEBP : mime::Mime = "image/webp".parse().unwrap(); - let res = create_response(&state, StatusCode::OK, IMAGE_WEBP, img_content); - (state, res) - }); -# }); -``` -*/ -#[derive(Clone, Debug)] -pub struct AcceptHeaderMatcher -{ - types : Vec, - lookup_table : LookupTable -} - -impl AcceptHeaderMatcher -{ - /// Create a new `AcceptHeaderMatcher` with the given types that can be produced by the route. - pub fn new(types : Vec) -> Self - { - let lookup_table = LookupTable::from_types(types.iter(), true); - Self { types, lookup_table } - } -} - -#[inline] -fn err() -> RouteNonMatch -{ - RouteNonMatch::new(StatusCode::NOT_ACCEPTABLE) -} - -impl RouteMatcher for AcceptHeaderMatcher -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { - HeaderMap::borrow_from(state).get(ACCEPT) - .map(|header| { - // parse mime types from the accept header - let acceptable = header.to_str() - .map_err(|_| err())? - .split(',') - .map(|str| str.trim().parse()) - .collect::, _>>() - .map_err(|_| err())?; - - for qmime in acceptable - { - // get mime type candidates from the lookup table - 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.types[*i]; - - // check that the candidates have the same suffix - this is not included in the - // essence string - if candidate.suffix() != qmime.mime.suffix() - { - continue - } - - // this candidate matches - params don't play a role in accept header matching - return Ok(()) - } - } - - // no candidates found - Err(err()) - }).unwrap_or_else(|| { - // no accept header - assume all types are acceptable - Ok(()) - }) - } -} - - -#[cfg(test)] -mod test -{ - use super::*; - - fn with_state(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 = AcceptHeaderMatcher::new(vec!(mime::TEXT_PLAIN)); - with_state(None, |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn single_mime_type() - { - let matcher = AcceptHeaderMatcher::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 = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); - with_state(Some("*/*"), |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn image_star() - { - let matcher = AcceptHeaderMatcher::new(vec!(mime::IMAGE_PNG)); - with_state(Some("image/*"), |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn complex_header() - { - let matcher = AcceptHeaderMatcher::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())); - } -} diff --git a/src/matcher/content_type.rs b/src/matcher/content_type.rs deleted file mode 100644 index aca8b9a..0000000 --- a/src/matcher/content_type.rs +++ /dev/null @@ -1,173 +0,0 @@ -use super::{LookupTable, LookupTableFromTypes}; -use gotham::{ - hyper::{ - header::{HeaderMap, CONTENT_TYPE}, - StatusCode - }, - router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, - state::{FromState, State} -}; -use mime::Mime; - -/** -A route matcher that checks for the presence of a supported content type. - -Usage: - -``` -# use gotham::{helpers::http::response::create_response, hyper::StatusCode, router::builder::*}; -# use gotham_restful::matcher::ContentTypeMatcher; -# -let types = vec![mime::TEXT_HTML, mime::TEXT_PLAIN]; -let matcher = ContentTypeMatcher::new(types) - // optionally accept requests with no content type - .allow_no_type(); - -# build_simple_router(|route| { -// use the matcher for your request -route.post("/foo") - .extend_route_matcher(matcher) - .to(|state| { - let res = create_response(&state, StatusCode::OK, mime::TEXT_PLAIN, "Correct Content Type!"); - (state, res) - }); -# }); -``` -*/ -#[derive(Clone, Debug)] -pub struct ContentTypeMatcher -{ - types : Vec, - lookup_table : LookupTable, - allow_no_type : bool -} - -impl ContentTypeMatcher -{ - /// Create a new `ContentTypeMatcher` with the given supported types that does not allow requests - /// that don't include a content-type header. - pub fn new(types : Vec) -> Self - { - let lookup_table = LookupTable::from_types(types.iter(), false); - Self { types, lookup_table, allow_no_type: false } - } - - /// Modify this matcher to allow requests that don't include a content-type header. - pub fn allow_no_type(mut self) -> Self - { - self.allow_no_type = true; - self - } -} - -#[inline] -fn err() -> RouteNonMatch -{ - RouteNonMatch::new(StatusCode::UNSUPPORTED_MEDIA_TYPE) -} - -impl RouteMatcher for ContentTypeMatcher -{ - fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> - { - HeaderMap::borrow_from(state).get(CONTENT_TYPE) - .map(|ty| { - // parse mime type from the content type header - let mime : Mime = ty.to_str() - .map_err(|_| err())? - .parse() - .map_err(|_| err())?; - - // get mime type candidates from the lookup table - let essence = mime.essence_str(); - let candidates = self.lookup_table.get(essence).ok_or_else(err)?; - for i in candidates - { - let candidate = &self.types[*i]; - - // check that the candidates have the same suffix - this is not included in the - // essence string - if candidate.suffix() != mime.suffix() - { - continue - } - - // check that this candidate has at least the parameters that the content type - // has and that their values are equal - if candidate.params().any(|(key, value)| mime.get_param(key) != Some(value)) - { - continue - } - - // this candidate matches - return Ok(()) - } - - // no candidates found - Err(err()) - }).unwrap_or_else(|| { - // no type present - if self.allow_no_type { Ok(()) } else { Err(err()) } - }) - } -} - - -#[cfg(test)] -mod test -{ - use super::*; - - fn with_state(content_type : Option<&str>, block : F) - where F : FnOnce(&mut State) -> () - { - State::with_new(|state| { - let mut headers = HeaderMap::new(); - if let Some(ty) = content_type - { - headers.insert(CONTENT_TYPE, ty.parse().unwrap()); - } - state.put(headers); - block(state); - }); - } - - #[test] - fn empty_type_list() - { - let matcher = ContentTypeMatcher::new(Vec::new()); - with_state(None, |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_err())); - - let matcher = matcher.allow_no_type(); - with_state(None, |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn simple_type() - { - let matcher = ContentTypeMatcher::new(vec![mime::TEXT_PLAIN]); - with_state(None, |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("text/plain; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_ok())); - } - - #[test] - fn complex_type() - { - let matcher = ContentTypeMatcher::new(vec!["image/svg+xml; charset=utf-8".parse().unwrap()]); - with_state(Some("image/svg"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/svg+xml"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/svg+xml; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("image/svg+xml; charset=utf-8; eol=lf"), |state| assert!(matcher.is_match(&state).is_ok())); - with_state(Some("image/svg+xml; charset=us-ascii"), |state| assert!(matcher.is_match(&state).is_err())); - with_state(Some("image/svg+json; charset=utf-8"), |state| assert!(matcher.is_match(&state).is_err())); - } - - #[test] - fn type_mismatch() - { - let matcher = ContentTypeMatcher::new(vec![mime::TEXT_HTML]); - with_state(Some("text/plain"), |state| assert!(matcher.is_match(&state).is_err())); - } -} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 9cbfcbb..cc7e734 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -1,40 +1,5 @@ -use itertools::Itertools; -use mime::Mime; -use std::collections::HashMap; - -mod accept; -pub use accept::AcceptHeaderMatcher; - -mod content_type; -pub use content_type::ContentTypeMatcher; - #[cfg(feature = "cors")] mod access_control_request_method; #[cfg(feature = "cors")] pub use access_control_request_method::AccessControlRequestMethodMatcher; -type LookupTable = HashMap>; - -trait LookupTableFromTypes -{ - fn from_types<'a, I : Iterator>(types : I, include_stars : bool) -> Self; -} - -impl LookupTableFromTypes for LookupTable -{ - fn from_types<'a, I : Iterator>(types : I, include_stars : bool) -> Self - { - if include_stars - { - return types - .enumerate() - .flat_map(|(i, mime)| vec![("*/*".to_owned(), i), (format!("{}/*", mime.type_()), i), (mime.essence_str().to_owned(), i)].into_iter()) - .into_group_map(); - } - - types - .enumerate() - .map(|(i, mime)| (mime.essence_str().to_owned(), i)) - .into_group_map() - } -} diff --git a/src/routing.rs b/src/routing.rs index 916b244..5610379 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,5 +1,4 @@ use crate::{ - matcher::{AcceptHeaderMatcher, ContentTypeMatcher}, resource::*, result::{ResourceError, ResourceResult}, RequestBody, @@ -22,7 +21,7 @@ use gotham::{ router::{ builder::*, non_match::RouteNonMatch, - route::matcher::RouteMatcher + route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} }, state::{FromState, State} }; @@ -262,7 +261,7 @@ fn remove_handler(state : State) -> Pin + matcher : Option } impl RouteMatcher for MaybeMatchAcceptHeader @@ -285,7 +284,7 @@ impl From>> for MaybeMatchAcceptHeader types => types }; Self { - matcher: types.map(AcceptHeaderMatcher::new) + matcher: types.map(AcceptHeaderRouteMatcher::new) } } } @@ -293,7 +292,7 @@ impl From>> for MaybeMatchAcceptHeader #[derive(Clone)] struct MaybeMatchContentTypeHeader { - matcher : Option + matcher : Option } impl RouteMatcher for MaybeMatchContentTypeHeader @@ -312,7 +311,7 @@ impl From>> for MaybeMatchContentTypeHeader fn from(types : Option>) -> Self { Self { - matcher: types.map(ContentTypeMatcher::new).map(ContentTypeMatcher::allow_no_type) + matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type()) } } }