1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-23 04:52:28 +00:00

gotham finally has a release candidate

This commit is contained in:
Dominic 2020-05-20 09:33:12 +02:00
parent 8321b63982
commit c1cb0e692a
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
6 changed files with 10 additions and 436 deletions

View file

@ -23,9 +23,9 @@ chrono = { version = "0.4.11", features = ["serde"], optional = true }
cookie = { version = "0.13.3", optional = true } cookie = { version = "0.13.3", optional = true }
futures-core = "0.3.4" futures-core = "0.3.4"
futures-util = "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 = { version = "0.5.0-rc.1", default-features = false }
gotham_derive = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev" } gotham_derive = "0.5.0-rc.1"
gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", version = "0.1.0", optional = true } gotham_middleware_diesel = { version = "0.1.2", optional = true }
gotham_restful_derive = { version = "0.1.0-dev" } gotham_restful_derive = { version = "0.1.0-dev" }
indexmap = { version = "1.3.2", optional = true } indexmap = { version = "1.3.2", optional = true }
itertools = "0.9.0" itertools = "0.9.0"

View file

@ -15,8 +15,8 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies] [dependencies]
fake = "2.2" fake = "2.2"
gotham = { 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 = { git = "https://github.com/gotham-rs/gotham", version = "0.5.0-dev", default-features = false } gotham_derive = "0.5.0-rc.1"
gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] } gotham_restful = { version = "0.1.0-dev", features = ["auth", "openapi"] }
log = "0.4.8" log = "0.4.8"
log4rs = { version = "0.12", features = ["console_appender"], default-features = false } log4rs = { version = "0.12", features = ["console_appender"], default-features = false }

View file

@ -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<f32>
}
impl QMime
{
fn new(mime : Mime, weight : Option<f32>) -> 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<Self, Self::Err>
{
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<Mime>,
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<Mime>) -> 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::<Result<Vec<QMime>, _>>()
.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<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 = 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()));
}
}

View file

@ -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<Mime>,
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<Mime>) -> 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<F>(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()));
}
}

View file

@ -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")] #[cfg(feature = "cors")]
mod access_control_request_method; mod access_control_request_method;
#[cfg(feature = "cors")] #[cfg(feature = "cors")]
pub use access_control_request_method::AccessControlRequestMethodMatcher; pub use access_control_request_method::AccessControlRequestMethodMatcher;
type LookupTable = HashMap<String, Vec<usize>>;
trait LookupTableFromTypes
{
fn from_types<'a, I : Iterator<Item = &'a Mime>>(types : I, include_stars : bool) -> Self;
}
impl LookupTableFromTypes for LookupTable
{
fn from_types<'a, I : Iterator<Item = &'a Mime>>(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()
}
}

View file

@ -1,5 +1,4 @@
use crate::{ use crate::{
matcher::{AcceptHeaderMatcher, ContentTypeMatcher},
resource::*, resource::*,
result::{ResourceError, ResourceResult}, result::{ResourceError, ResourceResult},
RequestBody, RequestBody,
@ -22,7 +21,7 @@ use gotham::{
router::{ router::{
builder::*, builder::*,
non_match::RouteNonMatch, non_match::RouteNonMatch,
route::matcher::RouteMatcher route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
}, },
state::{FromState, State} state::{FromState, State}
}; };
@ -262,7 +261,7 @@ fn remove_handler<Handler : ResourceRemove>(state : State) -> Pin<Box<HandlerFut
#[derive(Clone)] #[derive(Clone)]
struct MaybeMatchAcceptHeader struct MaybeMatchAcceptHeader
{ {
matcher : Option<AcceptHeaderMatcher> matcher : Option<AcceptHeaderRouteMatcher>
} }
impl RouteMatcher for MaybeMatchAcceptHeader impl RouteMatcher for MaybeMatchAcceptHeader
@ -285,7 +284,7 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
types => types types => types
}; };
Self { Self {
matcher: types.map(AcceptHeaderMatcher::new) matcher: types.map(AcceptHeaderRouteMatcher::new)
} }
} }
} }
@ -293,7 +292,7 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
#[derive(Clone)] #[derive(Clone)]
struct MaybeMatchContentTypeHeader struct MaybeMatchContentTypeHeader
{ {
matcher : Option<ContentTypeMatcher> matcher : Option<ContentTypeHeaderRouteMatcher>
} }
impl RouteMatcher for MaybeMatchContentTypeHeader impl RouteMatcher for MaybeMatchContentTypeHeader
@ -312,7 +311,7 @@ impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader
fn from(types : Option<Vec<Mime>>) -> Self fn from(types : Option<Vec<Mime>>) -> Self
{ {
Self { Self {
matcher: types.map(ContentTypeMatcher::new).map(ContentTypeMatcher::allow_no_type) matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
} }
} }
} }