mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-20 06:54:46 +00:00
merge workspace and main crate
This commit is contained in:
parent
52679ad29d
commit
5587ded60d
45 changed files with 58 additions and 67 deletions
503
src/auth.rs
Normal file
503
src/auth.rs
Normal file
|
@ -0,0 +1,503 @@
|
|||
use crate::{AuthError, Forbidden, HeaderName};
|
||||
use cookie::CookieJar;
|
||||
use futures_util::{future, future::{FutureExt, TryFutureExt}};
|
||||
use gotham::{
|
||||
handler::HandlerFuture,
|
||||
hyper::header::{AUTHORIZATION, HeaderMap},
|
||||
middleware::{Middleware, NewMiddleware},
|
||||
state::{FromState, State}
|
||||
};
|
||||
use jsonwebtoken::{
|
||||
errors::ErrorKind,
|
||||
DecodingKey
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
panic::RefUnwindSafe,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
pub use jsonwebtoken::Validation as AuthValidation;
|
||||
|
||||
/// The authentication status returned by the auth middleware for each request.
|
||||
#[derive(Debug, StateData)]
|
||||
pub enum AuthStatus<T : Send + 'static>
|
||||
{
|
||||
/// The auth status is unknown.
|
||||
Unknown,
|
||||
/// The request has been performed without any kind of authentication.
|
||||
Unauthenticated,
|
||||
/// The request has been performed with an invalid authentication.
|
||||
Invalid,
|
||||
/// The request has been performed with an expired authentication.
|
||||
Expired,
|
||||
/// The request has been performed with a valid authentication.
|
||||
Authenticated(T)
|
||||
}
|
||||
|
||||
impl<T> Clone for AuthStatus<T>
|
||||
where
|
||||
T : Clone + Send + 'static
|
||||
{
|
||||
fn clone(&self) -> Self
|
||||
{
|
||||
match self {
|
||||
Self::Unknown => Self::Unknown,
|
||||
Self::Unauthenticated => Self::Unauthenticated,
|
||||
Self::Invalid => Self::Invalid,
|
||||
Self::Expired => Self::Expired,
|
||||
Self::Authenticated(data) => Self::Authenticated(data.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Copy for AuthStatus<T>
|
||||
where
|
||||
T : Copy + Send + 'static
|
||||
{
|
||||
}
|
||||
|
||||
impl<T : Send + 'static> AuthStatus<T>
|
||||
{
|
||||
pub fn ok(self) -> Result<T, AuthError>
|
||||
{
|
||||
match self {
|
||||
Self::Authenticated(data) => Ok(data),
|
||||
_ => Err(Forbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The source of the authentication token in the request.
|
||||
#[derive(Clone, Debug, StateData)]
|
||||
pub enum AuthSource
|
||||
{
|
||||
/// Take the token from a cookie with the given name.
|
||||
Cookie(String),
|
||||
/// Take the token from a header with the given name.
|
||||
Header(HeaderName),
|
||||
/// Take the token from the HTTP Authorization header. This is different from `Header("Authorization")`
|
||||
/// as it will follow the `scheme param` format from the HTTP specification. The `scheme` will
|
||||
/// be discarded, so its value doesn't matter.
|
||||
AuthorizationHeader
|
||||
}
|
||||
|
||||
/**
|
||||
This trait will help the auth middleware to determine the validity of an authentication token.
|
||||
|
||||
A very basic implementation could look like this:
|
||||
```
|
||||
# use gotham_restful::{AuthHandler, State};
|
||||
#
|
||||
const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc";
|
||||
|
||||
struct CustomAuthHandler;
|
||||
impl<T> AuthHandler<T> for CustomAuthHandler {
|
||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>> {
|
||||
Some(SECRET.to_vec())
|
||||
}
|
||||
}
|
||||
```
|
||||
*/
|
||||
pub trait AuthHandler<Data>
|
||||
{
|
||||
/// Return the SHA256-HMAC secret used to verify the JWT token.
|
||||
fn jwt_secret<F : FnOnce() -> Option<Data>>(&self, state : &mut State, decode_data : F) -> Option<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// An `AuthHandler` returning always the same secret. See `AuthMiddleware` for a usage example.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StaticAuthHandler
|
||||
{
|
||||
secret : Vec<u8>
|
||||
}
|
||||
|
||||
impl StaticAuthHandler
|
||||
{
|
||||
pub fn from_vec(secret : Vec<u8>) -> Self
|
||||
{
|
||||
Self { secret }
|
||||
}
|
||||
|
||||
pub fn from_array(secret : &[u8]) -> Self
|
||||
{
|
||||
Self::from_vec(secret.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AuthHandler<T> for StaticAuthHandler
|
||||
{
|
||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
|
||||
{
|
||||
Some(self.secret.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This is the auth middleware. To use it, first make sure you have the `auth` feature enabled. Then
|
||||
simply add it to your pipeline and request it inside your handler:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct AuthResource;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct AuthData {
|
||||
sub: String,
|
||||
exp: u64
|
||||
}
|
||||
|
||||
#[read_all(AuthResource)]
|
||||
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
||||
format!("{:?}", auth).into()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let auth : AuthMiddleware<AuthData, _> = AuthMiddleware::new(
|
||||
AuthSource::AuthorizationHeader,
|
||||
AuthValidation::default(),
|
||||
StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc")
|
||||
);
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build());
|
||||
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||
route.resource::<AuthResource>("auth");
|
||||
}));
|
||||
}
|
||||
```
|
||||
*/
|
||||
#[derive(Debug)]
|
||||
pub struct AuthMiddleware<Data, Handler>
|
||||
{
|
||||
source : AuthSource,
|
||||
validation : AuthValidation,
|
||||
handler : Handler,
|
||||
_data : PhantomData<Data>
|
||||
}
|
||||
|
||||
impl<Data, Handler> Clone for AuthMiddleware<Data, Handler>
|
||||
where Handler : Clone
|
||||
{
|
||||
fn clone(&self) -> Self
|
||||
{
|
||||
Self {
|
||||
source: self.source.clone(),
|
||||
validation: self.validation.clone(),
|
||||
handler: self.handler.clone(),
|
||||
_data: self._data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Data, Handler> AuthMiddleware<Data, Handler>
|
||||
where
|
||||
Data : DeserializeOwned + Send,
|
||||
Handler : AuthHandler<Data> + Default
|
||||
{
|
||||
pub fn from_source(source : AuthSource) -> Self
|
||||
{
|
||||
Self {
|
||||
source,
|
||||
validation: Default::default(),
|
||||
handler: Default::default(),
|
||||
_data: Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Data, Handler> AuthMiddleware<Data, Handler>
|
||||
where
|
||||
Data : DeserializeOwned + Send,
|
||||
Handler : AuthHandler<Data>
|
||||
{
|
||||
pub fn new(source : AuthSource, validation : AuthValidation, handler : Handler) -> Self
|
||||
{
|
||||
Self {
|
||||
source,
|
||||
validation,
|
||||
handler,
|
||||
_data: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_status(&self, state : &mut State) -> AuthStatus<Data>
|
||||
{
|
||||
// extract the provided token, if any
|
||||
let token = match &self.source {
|
||||
AuthSource::Cookie(name) => {
|
||||
CookieJar::try_borrow_from(&state)
|
||||
.and_then(|jar| jar.get(&name))
|
||||
.map(|cookie| cookie.value().to_owned())
|
||||
},
|
||||
AuthSource::Header(name) => {
|
||||
HeaderMap::try_borrow_from(&state)
|
||||
.and_then(|map| map.get(name))
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.map(|value| value.to_owned())
|
||||
},
|
||||
AuthSource::AuthorizationHeader => {
|
||||
HeaderMap::try_borrow_from(&state)
|
||||
.and_then(|map| map.get(AUTHORIZATION))
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.and_then(|value| value.split_whitespace().nth(1))
|
||||
.map(|value| value.to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
// unauthed if no token
|
||||
let token = match token {
|
||||
Some(token) => token,
|
||||
None => return AuthStatus::Unauthenticated
|
||||
};
|
||||
|
||||
// get the secret from the handler, possibly decoding claims ourselves
|
||||
let secret = self.handler.jwt_secret(state, || {
|
||||
let b64 = token.split('.').nth(1)?;
|
||||
let raw = base64::decode_config(b64, base64::URL_SAFE_NO_PAD).ok()?;
|
||||
serde_json::from_slice(&raw).ok()?
|
||||
});
|
||||
|
||||
// unknown if no secret
|
||||
let secret = match secret {
|
||||
Some(secret) => secret,
|
||||
None => return AuthStatus::Unknown
|
||||
};
|
||||
|
||||
// validate the token
|
||||
let data : Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) {
|
||||
Ok(data) => data.claims,
|
||||
Err(e) => match dbg!(e.into_kind()) {
|
||||
ErrorKind::ExpiredSignature => return AuthStatus::Expired,
|
||||
_ => return AuthStatus::Invalid
|
||||
}
|
||||
};
|
||||
|
||||
// we found a valid token
|
||||
AuthStatus::Authenticated(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Data, Handler> Middleware for AuthMiddleware<Data, Handler>
|
||||
where
|
||||
Data : DeserializeOwned + Send + 'static,
|
||||
Handler : AuthHandler<Data>
|
||||
{
|
||||
fn call<Chain>(self, mut state : State, chain : Chain) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Chain : FnOnce(State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
// put the source in our state, required for e.g. openapi
|
||||
state.put(self.source.clone());
|
||||
|
||||
// put the status in our state
|
||||
let status = self.auth_status(&mut state);
|
||||
state.put(status);
|
||||
|
||||
// call the rest of the chain
|
||||
chain(state).and_then(|(state, res)| future::ok((state, res))).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Data, Handler> NewMiddleware for AuthMiddleware<Data, Handler>
|
||||
where
|
||||
Self : Clone + Middleware + Sync + RefUnwindSafe
|
||||
{
|
||||
type Instance = Self;
|
||||
|
||||
fn new_middleware(&self) -> Result<Self::Instance, std::io::Error>
|
||||
{
|
||||
let c : Self = self.clone();
|
||||
Ok(c)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use cookie::Cookie;
|
||||
use std::fmt::Debug;
|
||||
|
||||
// 256-bit random string
|
||||
const JWT_SECRET : &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW";
|
||||
|
||||
// some known tokens
|
||||
const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk";
|
||||
const EXPIRED_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjE1Nzc4MzcxMDB9.eV1snaGLYrJ7qUoMk74OvBY3WUU9M0Je5HTU2xtX1v0";
|
||||
const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9";
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct TestData
|
||||
{
|
||||
iss : String,
|
||||
sub : String,
|
||||
iat : u64,
|
||||
exp : u64
|
||||
}
|
||||
|
||||
impl Default for TestData
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self {
|
||||
iss: "msrd0".to_owned(),
|
||||
sub: "gotham-restful".to_owned(),
|
||||
iat: 1577836800,
|
||||
exp: 4102444800
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NoneAuthHandler;
|
||||
impl<T> AuthHandler<T> for NoneAuthHandler
|
||||
{
|
||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_none_secret()
|
||||
{
|
||||
let middleware = <AuthMiddleware<TestData, NoneAuthHandler>>::from_source(AuthSource::AuthorizationHeader);
|
||||
State::with_new(|mut state| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Bearer {}", VALID_TOKEN).parse().unwrap());
|
||||
state.put(headers);
|
||||
middleware.auth_status(&mut state);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestAssertingHandler;
|
||||
impl<T> AuthHandler<T> for TestAssertingHandler
|
||||
where T : Debug + Default + PartialEq
|
||||
{
|
||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, decode_data : F) -> Option<Vec<u8>>
|
||||
{
|
||||
assert_eq!(decode_data(), Some(T::default()));
|
||||
Some(JWT_SECRET.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_decode_data()
|
||||
{
|
||||
let middleware = <AuthMiddleware<TestData, TestAssertingHandler>>::from_source(AuthSource::AuthorizationHeader);
|
||||
State::with_new(|mut state| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Bearer {}", VALID_TOKEN).parse().unwrap());
|
||||
state.put(headers);
|
||||
middleware.auth_status(&mut state);
|
||||
});
|
||||
}
|
||||
|
||||
fn new_middleware<T>(source : AuthSource) -> AuthMiddleware<T, StaticAuthHandler>
|
||||
where T : DeserializeOwned + Send
|
||||
{
|
||||
AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_no_token()
|
||||
{
|
||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||
State::with_new(|mut state| {
|
||||
let status = middleware.auth_status(&mut state);
|
||||
match status {
|
||||
AuthStatus::Unauthenticated => {},
|
||||
_ => panic!("Expected AuthStatus::Unauthenticated, got {:?}", status)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_expired_token()
|
||||
{
|
||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||
State::with_new(|mut state| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Bearer {}", EXPIRED_TOKEN).parse().unwrap());
|
||||
state.put(headers);
|
||||
let status = middleware.auth_status(&mut state);
|
||||
match status {
|
||||
AuthStatus::Expired => {},
|
||||
_ => panic!("Expected AuthStatus::Expired, got {:?}", status)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_invalid_token()
|
||||
{
|
||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||
State::with_new(|mut state| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Bearer {}", INVALID_TOKEN).parse().unwrap());
|
||||
state.put(headers);
|
||||
let status = middleware.auth_status(&mut state);
|
||||
match status {
|
||||
AuthStatus::Invalid => {},
|
||||
_ => panic!("Expected AuthStatus::Invalid, got {:?}", status)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_auth_header_token()
|
||||
{
|
||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||
State::with_new(|mut state| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, format!("Bearer {}", VALID_TOKEN).parse().unwrap());
|
||||
state.put(headers);
|
||||
let status = middleware.auth_status(&mut state);
|
||||
match status {
|
||||
AuthStatus::Authenticated(data) => assert_eq!(data, TestData::default()),
|
||||
_ => panic!("Expected AuthStatus::Authenticated, got {:?}", status)
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_header_token()
|
||||
{
|
||||
let header_name = "x-znoiprwmvfexju";
|
||||
let middleware = new_middleware::<TestData>(AuthSource::Header(HeaderName::from_static(header_name)));
|
||||
State::with_new(|mut state| {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header_name, VALID_TOKEN.parse().unwrap());
|
||||
state.put(headers);
|
||||
let status = middleware.auth_status(&mut state);
|
||||
match status {
|
||||
AuthStatus::Authenticated(data) => assert_eq!(data, TestData::default()),
|
||||
_ => panic!("Expected AuthStatus::Authenticated, got {:?}", status)
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_middleware_cookie_token()
|
||||
{
|
||||
let cookie_name = "znoiprwmvfexju";
|
||||
let middleware = new_middleware::<TestData>(AuthSource::Cookie(cookie_name.to_owned()));
|
||||
State::with_new(|mut state| {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add_original(Cookie::new(cookie_name, VALID_TOKEN));
|
||||
state.put(jar);
|
||||
let status = middleware.auth_status(&mut state);
|
||||
match status {
|
||||
AuthStatus::Authenticated(data) => assert_eq!(data, TestData::default()),
|
||||
_ => panic!("Expected AuthStatus::Authenticated, got {:?}", status)
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
330
src/lib.rs
Normal file
330
src/lib.rs
Normal file
|
@ -0,0 +1,330 @@
|
|||
#![allow(clippy::tabs_in_doc_comments)]
|
||||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||
#![deny(intra_doc_link_resolution_failure)]
|
||||
/*!
|
||||
**Note:** The `stable` branch contains some bugfixes against the last release. The `master`
|
||||
branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be
|
||||
compatible with the new future / async stuff.
|
||||
|
||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
||||
create resources with assigned methods that aim to be a more convenient way of creating handlers
|
||||
for requests. Assuming you assign `/foobar` to your resource, you can implement the following
|
||||
methods:
|
||||
|
||||
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ----------- | ------------------ | --------- | ----------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
| create | body | POST | /foobar |
|
||||
| change_all | body | PUT | /foobar |
|
||||
| change | id, body | PUT | /foobar/:id |
|
||||
| remove_all | | DELETE | /foobar |
|
||||
| remove | id | DELETE | /foobar/:id |
|
||||
|
||||
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
|
||||
simple example could look like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::router::builder::*;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
/// Our RESTful resource.
|
||||
#[derive(Resource)]
|
||||
#[resource(read)]
|
||||
struct FooResource;
|
||||
|
||||
/// The return type of the foo read method.
|
||||
#[derive(Serialize)]
|
||||
# #[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
id: u64
|
||||
}
|
||||
|
||||
/// The foo read method handler.
|
||||
#[read(FooResource)]
|
||||
fn read(id: u64) -> Success<Foo> {
|
||||
Foo { id }.into()
|
||||
}
|
||||
# fn main() {
|
||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
# route.resource::<FooResource>("foo");
|
||||
# }));
|
||||
# }
|
||||
```
|
||||
|
||||
# Arguments
|
||||
|
||||
Some methods require arguments. Those should be
|
||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
||||
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
||||
type needs to implement [`QueryStringExtractor`].
|
||||
|
||||
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
|
||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
||||
`Sync` there is unfortunately no more convenient way.
|
||||
|
||||
# Uploads and Downloads
|
||||
|
||||
By default, every request body is parsed from json, and every respone is converted to json using
|
||||
[serde_json]. However, you may also use raw bodies. This is an example where the request body
|
||||
is simply returned as the response again, no json parsing involved:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::router::builder::*;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Resource)]
|
||||
#[resource(create)]
|
||||
struct ImageResource;
|
||||
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
||||
struct RawImage {
|
||||
content: Vec<u8>,
|
||||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(ImageResource)]
|
||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
# fn main() {
|
||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
# route.resource::<ImageResource>("image");
|
||||
# }));
|
||||
# }
|
||||
```
|
||||
|
||||
# Features
|
||||
|
||||
To make life easier for common use-cases, this create offers a few features that might be helpful
|
||||
when you implement your web server.
|
||||
|
||||
## Authentication Feature
|
||||
|
||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
||||
register a middleware that can automatically check for the existence of an JWT authentication
|
||||
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
|
||||
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
||||
None of this is currently supported by gotham's own JWT middleware.
|
||||
|
||||
A simple example that uses only a single secret could look like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Resource)]
|
||||
#[resource(read)]
|
||||
struct SecretResource;
|
||||
|
||||
#[derive(Serialize)]
|
||||
# #[derive(OpenapiType)]
|
||||
struct Secret {
|
||||
id: u64,
|
||||
intended_for: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct AuthData {
|
||||
sub: String,
|
||||
exp: u64
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||
let intended_for = auth.ok()?.sub;
|
||||
Ok(Secret { id, intended_for })
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let auth: AuthMiddleware<AuthData, _> = AuthMiddleware::new(
|
||||
AuthSource::AuthorizationHeader,
|
||||
AuthValidation::default(),
|
||||
StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc")
|
||||
);
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build());
|
||||
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||
route.resource::<SecretResource>("secret");
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## Database Feature
|
||||
|
||||
The database feature allows an easy integration of [diesel] into your handler functions. Please
|
||||
note however that due to the way gotham's diesel middleware implementation, it is not possible
|
||||
to run async code while holding a database connection. If you need to combine async and database,
|
||||
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
|
||||
|
||||
A simple non-async example could look like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate diesel;
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use diesel::{table, PgConnection, QueryResult, RunQueryDsl};
|
||||
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||
# use gotham_middleware_diesel::DieselMiddleware;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
# use std::env;
|
||||
# table! {
|
||||
# foo (id) {
|
||||
# id -> Int8,
|
||||
# value -> Text,
|
||||
# }
|
||||
# }
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[derive(Queryable, Serialize)]
|
||||
# #[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
id: i64,
|
||||
value: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||
foo::table.load(conn)
|
||||
}
|
||||
|
||||
type Repo = gotham_middleware_diesel::Repo<PgConnection>;
|
||||
|
||||
fn main() {
|
||||
let repo = Repo::new(&env::var("DATABASE_URL").unwrap());
|
||||
let diesel = DieselMiddleware::new(repo);
|
||||
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build());
|
||||
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||
route.resource::<FooResource>("foo");
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
# Examples
|
||||
|
||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
||||
|
||||
# License
|
||||
|
||||
Licensed under your option of:
|
||||
- [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache)
|
||||
- [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL)
|
||||
|
||||
|
||||
[diesel]: https://diesel.rs/
|
||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||
[gotham]: https://gotham.rs/
|
||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
||||
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
|
||||
[`RequestBody`]: trait.RequestBody.html
|
||||
[`State`]: ../gotham/state/struct.State.html
|
||||
*/
|
||||
|
||||
// weird proc macro issue
|
||||
extern crate self as gotham_restful;
|
||||
|
||||
#[macro_use] extern crate gotham_derive;
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate serde;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use gotham;
|
||||
#[doc(no_inline)]
|
||||
pub use gotham::{
|
||||
hyper::{header::HeaderName, StatusCode},
|
||||
state::{FromState, State}
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use mime::Mime;
|
||||
|
||||
pub use gotham_restful_derive::*;
|
||||
|
||||
/// Not public API
|
||||
#[doc(hidden)]
|
||||
pub mod export
|
||||
{
|
||||
pub use futures_util::future::FutureExt;
|
||||
|
||||
pub use serde_json;
|
||||
|
||||
#[cfg(feature = "database")]
|
||||
pub use gotham_middleware_diesel::Repo;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use indexmap::IndexMap;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapiv3 as openapi;
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
mod auth;
|
||||
#[cfg(feature = "auth")]
|
||||
pub use auth::{
|
||||
AuthHandler,
|
||||
AuthMiddleware,
|
||||
AuthSource,
|
||||
AuthStatus,
|
||||
AuthValidation,
|
||||
StaticAuthHandler
|
||||
};
|
||||
|
||||
pub mod matcher;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
mod openapi;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapi::{
|
||||
builder::OpenapiInfo,
|
||||
router::GetOpenapi,
|
||||
types::{OpenapiSchema, OpenapiType}
|
||||
};
|
||||
|
||||
mod resource;
|
||||
pub use resource::{
|
||||
Resource,
|
||||
ResourceMethod,
|
||||
ResourceReadAll,
|
||||
ResourceRead,
|
||||
ResourceSearch,
|
||||
ResourceCreate,
|
||||
ResourceChangeAll,
|
||||
ResourceChange,
|
||||
ResourceRemoveAll,
|
||||
ResourceRemove
|
||||
};
|
||||
|
||||
mod response;
|
||||
pub use response::Response;
|
||||
|
||||
mod result;
|
||||
pub use result::{
|
||||
AuthError,
|
||||
AuthError::Forbidden,
|
||||
AuthErrorOrOther,
|
||||
AuthResult,
|
||||
AuthSuccess,
|
||||
IntoResponseError,
|
||||
NoContent,
|
||||
Raw,
|
||||
ResourceResult,
|
||||
Success
|
||||
};
|
||||
|
||||
mod routing;
|
||||
pub use routing::{DrawResources, DrawResourceRoutes};
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use routing::WithOpenapi;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
217
src/matcher/accept.rs
Normal file
217
src/matcher/accept.rs
Normal file
|
@ -0,0 +1,217 @@
|
|||
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()));
|
||||
}
|
||||
}
|
173
src/matcher/content_type.rs
Normal file
173
src/matcher/content_type.rs
Normal file
|
@ -0,0 +1,173 @@
|
|||
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()));
|
||||
}
|
||||
}
|
35
src/matcher/mod.rs
Normal file
35
src/matcher/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod accept;
|
||||
pub use accept::AcceptHeaderMatcher;
|
||||
|
||||
mod content_type;
|
||||
pub use content_type::ContentTypeMatcher;
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
162
src/openapi/builder.rs
Normal file
162
src/openapi/builder.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
use crate::{OpenapiType, OpenapiSchema};
|
||||
use indexmap::IndexMap;
|
||||
use openapiv3::{
|
||||
Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema,
|
||||
Server
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenapiInfo
|
||||
{
|
||||
pub title : String,
|
||||
pub version : String,
|
||||
pub urls : Vec<String>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OpenapiBuilder
|
||||
{
|
||||
pub openapi : Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl OpenapiBuilder
|
||||
{
|
||||
pub fn new(info : OpenapiInfo) -> Self
|
||||
{
|
||||
Self {
|
||||
openapi: Arc::new(RwLock::new(OpenAPI {
|
||||
openapi: "3.0.2".to_string(),
|
||||
info: openapiv3::Info {
|
||||
title: info.title,
|
||||
version: info.version,
|
||||
..Default::default()
|
||||
},
|
||||
servers: info.urls.into_iter()
|
||||
.map(|url| Server { url, ..Default::default() })
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to
|
||||
/// modify the path and add it back after the modification
|
||||
pub fn remove_path(&mut self, path : &str) -> PathItem
|
||||
{
|
||||
let mut openapi = self.openapi.write().unwrap();
|
||||
match openapi.paths.swap_remove(path) {
|
||||
Some(Item(item)) => item,
|
||||
_ => PathItem::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem)
|
||||
{
|
||||
let mut openapi = self.openapi.write().unwrap();
|
||||
openapi.paths.insert(path.to_string(), Item(item));
|
||||
}
|
||||
|
||||
fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema)
|
||||
{
|
||||
self.add_schema_dependencies(&mut schema.dependencies);
|
||||
|
||||
let mut openapi = self.openapi.write().unwrap();
|
||||
match &mut openapi.components {
|
||||
Some(comp) => {
|
||||
comp.schemas.insert(name, Item(schema.into_schema()));
|
||||
},
|
||||
None => {
|
||||
let mut comp = Components::default();
|
||||
comp.schemas.insert(name, Item(schema.into_schema()));
|
||||
openapi.components = Some(comp);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>)
|
||||
{
|
||||
let keys : Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
|
||||
for dep in keys
|
||||
{
|
||||
let dep_schema = dependencies.swap_remove(&dep);
|
||||
if let Some(dep_schema) = dep_schema
|
||||
{
|
||||
self.add_schema_impl(dep, dep_schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_schema<T : OpenapiType>(&mut self) -> ReferenceOr<Schema>
|
||||
{
|
||||
let mut schema = T::schema();
|
||||
match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
|
||||
self.add_schema_impl(name, schema);
|
||||
reference
|
||||
},
|
||||
None => {
|
||||
self.add_schema_dependencies(&mut schema.dependencies);
|
||||
Item(schema.into_schema())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Message
|
||||
{
|
||||
msg : String
|
||||
}
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Messages
|
||||
{
|
||||
msgs : Vec<Message>
|
||||
}
|
||||
|
||||
fn info() -> OpenapiInfo
|
||||
{
|
||||
OpenapiInfo {
|
||||
title: "TEST CASE".to_owned(),
|
||||
version: "1.2.3".to_owned(),
|
||||
urls: vec!["http://localhost:1234".to_owned(), "https://example.org".to_owned()]
|
||||
}
|
||||
}
|
||||
|
||||
fn openapi(builder : OpenapiBuilder) -> OpenAPI
|
||||
{
|
||||
Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_builder()
|
||||
{
|
||||
let info = info();
|
||||
let builder = OpenapiBuilder::new(info.clone());
|
||||
let openapi = openapi(builder);
|
||||
|
||||
assert_eq!(info.title, openapi.info.title);
|
||||
assert_eq!(info.version, openapi.info.version);
|
||||
assert_eq!(info.urls.len(), openapi.servers.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_schema()
|
||||
{
|
||||
let mut builder = OpenapiBuilder::new(info());
|
||||
builder.add_schema::<Option<Messages>>();
|
||||
let openapi = openapi(builder);
|
||||
|
||||
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Message"] , ReferenceOr::Item(Message ::schema().into_schema()));
|
||||
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Messages"], ReferenceOr::Item(Messages::schema().into_schema()));
|
||||
}
|
||||
}
|
110
src/openapi/handler.rs
Normal file
110
src/openapi/handler.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use super::SECURITY_NAME;
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::{
|
||||
error::Result,
|
||||
handler::{Handler, HandlerFuture, NewHandler},
|
||||
helpers::http::response::create_response,
|
||||
state::State
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||
use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock}
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenapiHandler
|
||||
{
|
||||
openapi : Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl OpenapiHandler
|
||||
{
|
||||
pub fn new(openapi : Arc<RwLock<OpenAPI>>) -> Self
|
||||
{
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for OpenapiHandler
|
||||
{
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> Result<Self>
|
||||
{
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>>
|
||||
{
|
||||
use crate::AuthSource;
|
||||
use gotham::state::FromState;
|
||||
|
||||
let source = match AuthSource::try_borrow_from(state) {
|
||||
Some(source) => source,
|
||||
None => return Default::default()
|
||||
};
|
||||
|
||||
let security_scheme = match source {
|
||||
AuthSource::Cookie(name) => SecurityScheme::APIKey {
|
||||
location: APIKeyLocation::Cookie,
|
||||
name: name.to_string()
|
||||
},
|
||||
AuthSource::Header(name) => SecurityScheme::APIKey {
|
||||
location: APIKeyLocation::Header,
|
||||
name: name.to_string()
|
||||
},
|
||||
AuthSource::AuthorizationHeader => SecurityScheme::HTTP {
|
||||
scheme: "bearer".to_owned(),
|
||||
bearer_format: Some("JWT".to_owned())
|
||||
}
|
||||
};
|
||||
|
||||
let mut security_schemes : IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default();
|
||||
security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme));
|
||||
|
||||
security_schemes
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
fn get_security(state : &mut State) -> (Vec<SecurityRequirement>, IndexMap<String, ReferenceOr<SecurityScheme>>)
|
||||
{
|
||||
Default::default()
|
||||
}
|
||||
|
||||
impl Handler for OpenapiHandler
|
||||
{
|
||||
fn handle(self, mut state : State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
let openapi = match self.openapi.read() {
|
||||
Ok(openapi) => openapi,
|
||||
Err(e) => {
|
||||
error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
|
||||
let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
return future::ok((state, res)).boxed()
|
||||
}
|
||||
};
|
||||
|
||||
let mut openapi = openapi.clone();
|
||||
let security_schemes = get_security(&mut state);
|
||||
let mut components = openapi.components.unwrap_or_default();
|
||||
components.security_schemes = security_schemes;
|
||||
openapi.components = Some(components);
|
||||
|
||||
match serde_json::to_string(&openapi) {
|
||||
Ok(body) => {
|
||||
let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body);
|
||||
future::ok((state, res)).boxed()
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Unable to handle OpenAPI request due to error: {}", e);
|
||||
let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
future::ok((state, res)).boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
src/openapi/mod.rs
Normal file
8
src/openapi/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
const SECURITY_NAME : &str = "authToken";
|
||||
|
||||
pub mod builder;
|
||||
pub mod handler;
|
||||
pub mod operation;
|
||||
pub mod router;
|
||||
pub mod types;
|
216
src/openapi/operation.rs
Normal file
216
src/openapi/operation.rs
Normal file
|
@ -0,0 +1,216 @@
|
|||
use crate::{
|
||||
resource::*,
|
||||
result::*,
|
||||
OpenapiSchema,
|
||||
RequestBody
|
||||
};
|
||||
use super::SECURITY_NAME;
|
||||
use indexmap::IndexMap;
|
||||
use mime::Mime;
|
||||
use openapiv3::{
|
||||
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr,
|
||||
ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind,
|
||||
StatusCode, Type
|
||||
};
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
struct OperationParams<'a>
|
||||
{
|
||||
path_params : Vec<(&'a str, ReferenceOr<Schema>)>,
|
||||
query_params : Option<OpenapiSchema>
|
||||
}
|
||||
|
||||
impl<'a> OperationParams<'a>
|
||||
{
|
||||
fn add_path_params(&self, params : &mut Vec<ReferenceOr<Parameter>>)
|
||||
{
|
||||
for param in &self.path_params
|
||||
{
|
||||
params.push(Item(Parameter::Path {
|
||||
parameter_data: ParameterData {
|
||||
name: (*param).0.to_string(),
|
||||
description: None,
|
||||
required: true,
|
||||
deprecated: None,
|
||||
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
|
||||
example: None,
|
||||
examples: IndexMap::new()
|
||||
},
|
||||
style: Default::default(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn add_query_params(self, params : &mut Vec<ReferenceOr<Parameter>>)
|
||||
{
|
||||
let query_params = match self.query_params {
|
||||
Some(qp) => qp.schema,
|
||||
None => return
|
||||
};
|
||||
let query_params = match query_params {
|
||||
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||
_ => panic!("Query Parameters needs to be a plain struct")
|
||||
};
|
||||
for (name, schema) in query_params.properties
|
||||
{
|
||||
let required = query_params.required.contains(&name);
|
||||
params.push(Item(Parameter::Query {
|
||||
parameter_data: ParameterData {
|
||||
name,
|
||||
description: None,
|
||||
required,
|
||||
deprecated: None,
|
||||
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||
example: None,
|
||||
examples: IndexMap::new()
|
||||
},
|
||||
allow_reserved: false,
|
||||
style: Default::default(),
|
||||
allow_empty_value: None
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn into_params(self) -> Vec<ReferenceOr<Parameter>>
|
||||
{
|
||||
let mut params : Vec<ReferenceOr<Parameter>> = Vec::new();
|
||||
self.add_path_params(&mut params);
|
||||
self.add_query_params(&mut params);
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OperationDescription<'a>
|
||||
{
|
||||
operation_id : Option<String>,
|
||||
default_status : crate::StatusCode,
|
||||
accepted_types : Option<Vec<Mime>>,
|
||||
schema : ReferenceOr<Schema>,
|
||||
params : OperationParams<'a>,
|
||||
body_schema : Option<ReferenceOr<Schema>>,
|
||||
supported_types : Option<Vec<Mime>>,
|
||||
requires_auth : bool
|
||||
}
|
||||
|
||||
impl<'a> OperationDescription<'a>
|
||||
{
|
||||
pub fn new<Handler : ResourceMethod>(schema : ReferenceOr<Schema>) -> Self
|
||||
{
|
||||
Self {
|
||||
operation_id: Handler::operation_id(),
|
||||
default_status: Handler::Res::default_status(),
|
||||
accepted_types: Handler::Res::accepted_types(),
|
||||
schema,
|
||||
params: Default::default(),
|
||||
body_schema: None,
|
||||
supported_types: None,
|
||||
requires_auth: Handler::wants_auth()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr<Schema>) -> Self
|
||||
{
|
||||
self.params.path_params.push((name, schema));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_query_params(mut self, params : OpenapiSchema) -> Self
|
||||
{
|
||||
self.params.query_params = Some(params);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_body<Body : RequestBody>(mut self, schema : ReferenceOr<Schema>) -> Self
|
||||
{
|
||||
self.body_schema = Some(schema);
|
||||
self.supported_types = Body::supported_types();
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
fn schema_to_content(types : Vec<Mime>, schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType>
|
||||
{
|
||||
let mut content : IndexMap<String, MediaType> = IndexMap::new();
|
||||
for ty in types
|
||||
{
|
||||
content.insert(ty.to_string(), MediaType {
|
||||
schema: Some(schema.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
content
|
||||
}
|
||||
|
||||
pub fn into_operation(self) -> Operation
|
||||
{
|
||||
// this is unfortunately neccessary to prevent rust from complaining about partially moving self
|
||||
let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = (
|
||||
self.operation_id, self.default_status, self.accepted_types, self.schema, self.params, self.body_schema, self.supported_types, self.requires_auth);
|
||||
|
||||
let content = Self::schema_to_content(accepted_types.or_all_types(), schema);
|
||||
|
||||
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
|
||||
responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response {
|
||||
description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(),
|
||||
content,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let request_body = body_schema.map(|schema| Item(OARequestBody {
|
||||
description: None,
|
||||
content: Self::schema_to_content(supported_types.or_all_types(), schema),
|
||||
required: true
|
||||
}));
|
||||
|
||||
let mut security = Vec::new();
|
||||
if requires_auth
|
||||
{
|
||||
let mut sec = IndexMap::new();
|
||||
sec.insert(SECURITY_NAME.to_owned(), Vec::new());
|
||||
security.push(sec);
|
||||
}
|
||||
|
||||
Operation {
|
||||
tags: Vec::new(),
|
||||
operation_id,
|
||||
parameters: params.into_params(),
|
||||
request_body,
|
||||
responses: Responses {
|
||||
default: None,
|
||||
responses
|
||||
},
|
||||
deprecated: false,
|
||||
security,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use crate::{OpenapiType, ResourceResult};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_content_schema_to_content()
|
||||
{
|
||||
let types = NoContent::accepted_types();
|
||||
let schema = <NoContent as OpenapiType>::schema();
|
||||
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||
assert!(content.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_schema_to_content()
|
||||
{
|
||||
let types = Raw::<&str>::accepted_types();
|
||||
let schema = <Raw<&str> as OpenapiType>::schema();
|
||||
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||
assert_eq!(content.len(), 1);
|
||||
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
||||
assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#);
|
||||
}
|
||||
}
|
195
src/openapi/router.rs
Normal file
195
src/openapi/router.rs
Normal file
|
@ -0,0 +1,195 @@
|
|||
use crate::{
|
||||
resource::*,
|
||||
routing::*,
|
||||
OpenapiType,
|
||||
};
|
||||
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
|
||||
use gotham::{
|
||||
pipeline::chain::PipelineHandleChain,
|
||||
router::builder::*
|
||||
};
|
||||
use std::panic::RefUnwindSafe;
|
||||
|
||||
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
|
||||
pub trait GetOpenapi
|
||||
{
|
||||
fn get_openapi(&mut self, path : &str);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OpenapiRouter<'a, D>
|
||||
{
|
||||
pub(crate) router : &'a mut D,
|
||||
pub(crate) scope : Option<&'a str>,
|
||||
pub(crate) openapi_builder : &'a mut OpenapiBuilder
|
||||
}
|
||||
|
||||
macro_rules! implOpenapiRouter {
|
||||
($implType:ident) => {
|
||||
|
||||
impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
pub fn scope<F>(&mut self, path : &str, callback : F)
|
||||
where
|
||||
F : FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
|
||||
{
|
||||
let mut openapi_builder = self.openapi_builder.clone();
|
||||
let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/"));
|
||||
self.router.scope(path, |router| {
|
||||
let mut router = OpenapiRouter {
|
||||
router,
|
||||
scope: Some(new_scope.as_ref().map(String::as_ref).unwrap_or(path)),
|
||||
openapi_builder: &mut openapi_builder
|
||||
};
|
||||
callback(&mut router);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn get_openapi(&mut self, path : &str)
|
||||
{
|
||||
self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn resource<R : Resource>(&mut self, path : &str)
|
||||
{
|
||||
R::setup((self, path));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn read_all<Handler : ResourceReadAll>(&mut self)
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read_all::<Handler>()
|
||||
}
|
||||
|
||||
fn read<Handler : ResourceRead>(&mut self)
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read::<Handler>()
|
||||
}
|
||||
|
||||
fn search<Handler : ResourceSearch>(&mut self)
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(OperationDescription::new::<Handler>(schema).with_query_params(Handler::Query::schema()).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).search::<Handler>()
|
||||
}
|
||||
|
||||
fn create<Handler : ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.post = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).create::<Handler>()
|
||||
}
|
||||
|
||||
fn change_all<Handler : ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.put = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).change_all::<Handler>()
|
||||
}
|
||||
|
||||
fn change<Handler : ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.put = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).with_body::<Handler::Body>(body_schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).change::<Handler>()
|
||||
}
|
||||
|
||||
fn remove_all<Handler : ResourceRemoveAll>(&mut self)
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).remove_all::<Handler>()
|
||||
}
|
||||
|
||||
fn remove<Handler : ResourceRemove>(&mut self)
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.delete = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).remove::<Handler>()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
implOpenapiRouter!(RouterBuilder);
|
||||
implOpenapiRouter!(ScopeBuilder);
|
387
src/openapi/types.rs
Normal file
387
src/openapi/types.rs
Normal file
|
@ -0,0 +1,387 @@
|
|||
#[cfg(feature = "chrono")]
|
||||
use chrono::{
|
||||
Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use openapiv3::{
|
||||
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr::Item,
|
||||
ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
|
||||
};
|
||||
#[cfg(feature = "uuid")]
|
||||
use uuid::Uuid;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
hash::BuildHasher
|
||||
};
|
||||
|
||||
/**
|
||||
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
|
||||
already implemented for primitive types, String, Vec, Option and the like. To have it available
|
||||
for your type, simply derive from [`OpenapiType`].
|
||||
|
||||
[`OpenapiType`]: trait.OpenapiType.html
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct OpenapiSchema
|
||||
{
|
||||
/// The name of this schema. If it is None, the schema will be inlined.
|
||||
pub name : Option<String>,
|
||||
/// Whether this particular schema is nullable. Note that there is no guarantee that this will
|
||||
/// make it into the final specification, it might just be interpreted as a hint to make it
|
||||
/// an optional parameter.
|
||||
pub nullable : bool,
|
||||
/// The actual OpenAPI schema.
|
||||
pub schema : SchemaKind,
|
||||
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
|
||||
/// along with this schema.
|
||||
pub dependencies : IndexMap<String, OpenapiSchema>
|
||||
}
|
||||
|
||||
impl OpenapiSchema
|
||||
{
|
||||
/// Create a new schema that has no name.
|
||||
pub fn new(schema : SchemaKind) -> Self
|
||||
{
|
||||
Self {
|
||||
name: None,
|
||||
nullable: false,
|
||||
schema,
|
||||
dependencies: IndexMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this schema to an `openapiv3::Schema` that can be serialized to the OpenAPI Spec.
|
||||
pub fn into_schema(self) -> Schema
|
||||
{
|
||||
Schema {
|
||||
schema_data: SchemaData {
|
||||
nullable: self.nullable,
|
||||
title: self.name,
|
||||
..Default::default()
|
||||
},
|
||||
schema_kind: self.schema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives
|
||||
access to the [`OpenapiSchema`] of this type. It is provided for primitive types, String and the
|
||||
like. For use on your own types, there is a derive macro:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
#
|
||||
#[derive(OpenapiType)]
|
||||
struct MyResponse {
|
||||
message: String
|
||||
}
|
||||
```
|
||||
|
||||
[`OpenapiSchema`]: struct.OpenapiSchema.html
|
||||
*/
|
||||
pub trait OpenapiType
|
||||
{
|
||||
fn schema() -> OpenapiSchema;
|
||||
}
|
||||
|
||||
impl OpenapiType for ()
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
|
||||
additional_properties: Some(AdditionalProperties::Any(false)),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for bool
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Boolean{}))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! int_types {
|
||||
($($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default())))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(unsigned $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
minimum: Some(0),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(bits = $bits:expr, $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(unsigned bits = $bits:expr, $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
|
||||
minimum: Some(0),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
int_types!(isize);
|
||||
int_types!(unsigned usize);
|
||||
int_types!(bits = 8, i8);
|
||||
int_types!(unsigned bits = 8, u8);
|
||||
int_types!(bits = 16, i16);
|
||||
int_types!(unsigned bits = 16, u16);
|
||||
int_types!(bits = 32, i32);
|
||||
int_types!(unsigned bits = 32, u32);
|
||||
int_types!(bits = 64, i64);
|
||||
int_types!(unsigned bits = 64, u64);
|
||||
int_types!(bits = 128, i128);
|
||||
int_types!(unsigned bits = 128, u128);
|
||||
|
||||
macro_rules! num_types {
|
||||
($($num_ty:ty = $num_fmt:ident),*) => {$(
|
||||
impl OpenapiType for $num_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType {
|
||||
format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*}
|
||||
}
|
||||
|
||||
num_types!(f32 = Float, f64 = Double);
|
||||
|
||||
macro_rules! str_types {
|
||||
($($str_ty:ty),*) => {$(
|
||||
impl OpenapiType for $str_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default())))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(format = $format:ident, $($str_ty:ty),*) => {$(
|
||||
impl OpenapiType for $str_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
use openapiv3::StringFormat;
|
||||
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::$format),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(format_str = $format:expr, $($str_ty:ty),*) => {$(
|
||||
impl OpenapiType for $str_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown($format.to_string()),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
str_types!(String, &str);
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate);
|
||||
#[cfg(feature = "chrono")]
|
||||
str_types!(format = DateTime, DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime);
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
str_types!(format_str = "uuid", Uuid);
|
||||
|
||||
impl<T : OpenapiType> OpenapiType for Option<T>
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
let schema = match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
|
||||
dependencies.insert(name, schema);
|
||||
SchemaKind::AllOf { all_of: vec![reference] }
|
||||
},
|
||||
None => schema.schema
|
||||
};
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: true,
|
||||
name: None,
|
||||
schema,
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : OpenapiType> OpenapiType for Vec<T>
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
|
||||
let items = match schema.name.clone()
|
||||
{
|
||||
Some(name) => {
|
||||
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
|
||||
dependencies.insert(name, schema);
|
||||
reference
|
||||
},
|
||||
None => Item(Box::new(schema.into_schema()))
|
||||
};
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: false,
|
||||
name: None,
|
||||
schema: SchemaKind::Type(Type::Array(ArrayType {
|
||||
items,
|
||||
min_items: None,
|
||||
max_items: None,
|
||||
unique_items: false
|
||||
})),
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : OpenapiType> OpenapiType for BTreeSet<T>
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
<Vec<T> as OpenapiType>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : OpenapiType, S : BuildHasher> OpenapiType for HashSet<T, S>
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
<Vec<T> as OpenapiType>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for serde_json::Value
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema {
|
||||
nullable: true,
|
||||
name: None,
|
||||
schema: SchemaKind::Any(Default::default()),
|
||||
dependencies: Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
|
||||
type Unit = ();
|
||||
|
||||
macro_rules! assert_schema {
|
||||
($ty:ident $(<$generic:ident>)* => $json:expr) => {
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<test_schema_ $ty:snake $(_ $generic:snake)*>]()
|
||||
{
|
||||
let schema = <$ty $(<$generic>)* as OpenapiType>::schema().into_schema();
|
||||
let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty)));
|
||||
assert_eq!(schema_json, $json);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#);
|
||||
assert_schema!(bool => r#"{"type":"boolean"}"#);
|
||||
assert_schema!(isize => r#"{"type":"integer"}"#);
|
||||
assert_schema!(usize => r#"{"type":"integer","minimum":0}"#);
|
||||
assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#);
|
||||
assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#);
|
||||
assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#);
|
||||
assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#);
|
||||
assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#);
|
||||
assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#);
|
||||
assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#);
|
||||
assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#);
|
||||
assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#);
|
||||
assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#);
|
||||
assert_schema!(f32 => r#"{"type":"number","format":"float"}"#);
|
||||
assert_schema!(f64 => r#"{"type":"number","format":"double"}"#);
|
||||
|
||||
assert_schema!(String => r#"{"type":"string"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(Date<FixedOffset> => r#"{"type":"string","format":"date"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(Date<Local> => r#"{"type":"string","format":"date"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(Date<Utc> => r#"{"type":"string","format":"date"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(DateTime<FixedOffset> => r#"{"type":"string","format":"date-time"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(DateTime<Local> => r#"{"type":"string","format":"date-time"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(DateTime<Utc> => r#"{"type":"string","format":"date-time"}"#);
|
||||
#[cfg(feature = "chrono")]
|
||||
assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#);
|
||||
#[cfg(feature = "uuid")]
|
||||
assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#);
|
||||
|
||||
assert_schema!(Option<String> => r#"{"nullable":true,"type":"string"}"#);
|
||||
assert_schema!(Vec<String> => r#"{"type":"array","items":{"type":"string"}}"#);
|
||||
|
||||
assert_schema!(Value => r#"{"nullable":true}"#);
|
||||
}
|
99
src/resource.rs
Normal file
99
src/resource.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType};
|
||||
use gotham::{
|
||||
extractor::QueryStringExtractor,
|
||||
hyper::Body,
|
||||
state::State
|
||||
};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
/// This trait must be implemented by every RESTful Resource. It will
|
||||
/// allow you to register the different methods for this Resource.
|
||||
pub trait Resource
|
||||
{
|
||||
/// The name of this resource. Must be unique.
|
||||
fn name() -> String;
|
||||
|
||||
/// Setup all routes of this resource. Take a look at the rest_resource!
|
||||
/// macro if you don't feel like caring yourself.
|
||||
fn setup<D : DrawResourceRoutes>(route : D);
|
||||
}
|
||||
|
||||
pub trait ResourceMethod
|
||||
{
|
||||
type Res : ResourceResult + Send + 'static;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn operation_id() -> Option<String>
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
fn wants_auth() -> bool
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a GET request on the Resource root.
|
||||
pub trait ResourceReadAll : ResourceMethod
|
||||
{
|
||||
fn read_all(state : State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a GET request on the Resource with an id.
|
||||
pub trait ResourceRead : ResourceMethod
|
||||
{
|
||||
type ID : ResourceID + 'static;
|
||||
|
||||
fn read(state : State, id : Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a GET request on the Resource with additional search parameters.
|
||||
pub trait ResourceSearch : ResourceMethod
|
||||
{
|
||||
type Query : ResourceType + QueryStringExtractor<Body> + Sync;
|
||||
|
||||
fn search(state : State, query : Self::Query) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a POST request on the Resource root.
|
||||
pub trait ResourceCreate : ResourceMethod
|
||||
{
|
||||
type Body : RequestBody;
|
||||
|
||||
fn create(state : State, body : Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a PUT request on the Resource root.
|
||||
pub trait ResourceChangeAll : ResourceMethod
|
||||
{
|
||||
type Body : RequestBody;
|
||||
|
||||
fn change_all(state : State, body : Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a PUT request on the Resource with an id.
|
||||
pub trait ResourceChange : ResourceMethod
|
||||
{
|
||||
type Body : RequestBody;
|
||||
type ID : ResourceID + 'static;
|
||||
|
||||
fn change(state : State, id : Self::ID, body : Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a DELETE request on the Resource root.
|
||||
pub trait ResourceRemoveAll : ResourceMethod
|
||||
{
|
||||
fn remove_all(state : State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// Handle a DELETE request on the Resource with an id.
|
||||
pub trait ResourceRemove : ResourceMethod
|
||||
{
|
||||
type ID : ResourceID + 'static;
|
||||
|
||||
fn remove(state : State, id : Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
64
src/response.rs
Normal file
64
src/response.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use gotham::hyper::{Body, StatusCode};
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
|
||||
/// A response, used to create the final gotham response from.
|
||||
#[derive(Debug)]
|
||||
pub struct Response
|
||||
{
|
||||
pub status : StatusCode,
|
||||
pub body : Body,
|
||||
pub mime : Option<Mime>
|
||||
}
|
||||
|
||||
impl Response
|
||||
{
|
||||
/// Create a new `Response` from raw data.
|
||||
pub fn new<B : Into<Body>>(status : StatusCode, body : B, mime : Option<Mime>) -> Self
|
||||
{
|
||||
Self {
|
||||
status,
|
||||
body: body.into(),
|
||||
mime
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `Response` with mime type json from already serialized data.
|
||||
pub fn json<B : Into<Body>>(status : StatusCode, body : B) -> Self
|
||||
{
|
||||
Self {
|
||||
status,
|
||||
body: body.into(),
|
||||
mime: Some(APPLICATION_JSON)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a _204 No Content_ `Response`.
|
||||
pub fn no_content() -> Self
|
||||
{
|
||||
Self {
|
||||
status: StatusCode::NO_CONTENT,
|
||||
body: Body::empty(),
|
||||
mime: None
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty _403 Forbidden_ `Response`.
|
||||
pub fn forbidden() -> Self
|
||||
{
|
||||
Self {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: Body::empty(),
|
||||
mime: None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn full_body(mut self) -> Result<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::Error>
|
||||
{
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::body::to_bytes;
|
||||
|
||||
let bytes : &[u8] = &block_on(to_bytes(&mut self.body))?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
}
|
109
src/result/auth_result.rs
Normal file
109
src/result/auth_result.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use gotham_restful_derive::ResourceError;
|
||||
|
||||
|
||||
/**
|
||||
This is an error type that always yields a _403 Forbidden_ response. This type is best used in
|
||||
combination with [`AuthSuccess`] or [`AuthResult`].
|
||||
|
||||
[`AuthSuccess`]: type.AuthSuccess.html
|
||||
[`AuthResult`]: type.AuthResult.html
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, ResourceError)]
|
||||
pub enum AuthError
|
||||
{
|
||||
#[status(FORBIDDEN)]
|
||||
#[display("Forbidden")]
|
||||
Forbidden
|
||||
}
|
||||
|
||||
/**
|
||||
This return type can be used to map another `ResourceResult` that can only be returned if the
|
||||
client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can
|
||||
look something like this (assuming the `auth` feature is enabled):
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
# use serde::Deserialize;
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
# #[derive(Clone, Deserialize)]
|
||||
# struct MyAuthData { exp : u64 }
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
||||
let auth_data = match auth {
|
||||
AuthStatus::Authenticated(data) => data,
|
||||
_ => return Err(Forbidden)
|
||||
};
|
||||
// do something
|
||||
Ok(NoContent::default())
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
pub type AuthSuccess<T> = Result<T, AuthError>;
|
||||
|
||||
/**
|
||||
This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication
|
||||
error, or delegates to another error type. This type is best used with [`AuthResult`].
|
||||
|
||||
[`AuthResult`]: type.AuthResult.html
|
||||
*/
|
||||
#[derive(Debug, ResourceError)]
|
||||
pub enum AuthErrorOrOther<E>
|
||||
{
|
||||
#[status(UNAUTHORIZED)]
|
||||
#[display("Forbidden")]
|
||||
Forbidden,
|
||||
#[display("{0}")]
|
||||
Other(#[from] E)
|
||||
}
|
||||
|
||||
impl<E> From<AuthError> for AuthErrorOrOther<E>
|
||||
{
|
||||
fn from(err : AuthError) -> Self
|
||||
{
|
||||
match err {
|
||||
AuthError::Forbidden => Self::Forbidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This return type can be used to map another `ResourceResult` that can only be returned if the
|
||||
client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can
|
||||
look something like this (assuming the `auth` feature is enabled):
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
# use serde::Deserialize;
|
||||
# use std::io;
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
# #[derive(Clone, Deserialize)]
|
||||
# struct MyAuthData { exp : u64 }
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
||||
let auth_data = match auth {
|
||||
AuthStatus::Authenticated(data) => data,
|
||||
_ => Err(Forbidden)?
|
||||
};
|
||||
// do something
|
||||
Ok(NoContent::default().into())
|
||||
}
|
||||
# }
|
||||
*/
|
||||
pub type AuthResult<T, E> = Result<T, AuthErrorOrOther<E>>;
|
191
src/result/mod.rs
Normal file
191
src/result/mod.rs
Normal file
|
@ -0,0 +1,191 @@
|
|||
use crate::Response;
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use futures_util::future::FutureExt;
|
||||
use mime::{Mime, STAR_STAR};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
error::Error,
|
||||
future::Future,
|
||||
fmt::{Debug, Display},
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
mod auth_result;
|
||||
pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess};
|
||||
|
||||
mod no_content;
|
||||
pub use no_content::NoContent;
|
||||
|
||||
mod raw;
|
||||
pub use raw::Raw;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod result;
|
||||
pub use result::IntoResponseError;
|
||||
|
||||
mod success;
|
||||
pub use success::Success;
|
||||
|
||||
|
||||
pub(crate) trait OrAllTypes
|
||||
{
|
||||
fn or_all_types(self) -> Vec<Mime>;
|
||||
}
|
||||
|
||||
impl OrAllTypes for Option<Vec<Mime>>
|
||||
{
|
||||
fn or_all_types(self) -> Vec<Mime>
|
||||
{
|
||||
self.unwrap_or_else(|| vec![STAR_STAR])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A trait provided to convert a resource's result to json.
|
||||
pub trait ResourceResult
|
||||
{
|
||||
type Err : Error + Send + 'static;
|
||||
|
||||
/// Turn this into a response that can be returned to the browser. This api will likely
|
||||
/// change in the future.
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>;
|
||||
|
||||
/// Return a list of supported mime types.
|
||||
fn accepted_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode
|
||||
{
|
||||
crate::StatusCode::OK
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<Res : ResourceResult> crate::OpenapiType for Res
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
Self::schema()
|
||||
}
|
||||
}
|
||||
|
||||
/// The default json returned on an 500 Internal Server Error.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ResourceError
|
||||
{
|
||||
error : bool,
|
||||
message : String
|
||||
}
|
||||
|
||||
impl<T : ToString> From<T> for ResourceError
|
||||
{
|
||||
fn from(message : T) -> Self
|
||||
{
|
||||
Self {
|
||||
error: true,
|
||||
message: message.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_response_helper<Err, F>(create_response : F) -> Pin<Box<dyn Future<Output = Result<Response, Err>> + Send>>
|
||||
where
|
||||
Err : Send + 'static,
|
||||
F : FnOnce() -> Result<Response, Err>
|
||||
{
|
||||
let res = create_response();
|
||||
async move { res }.boxed()
|
||||
}
|
||||
|
||||
#[cfg(feature = "errorlog")]
|
||||
fn errorlog<E : Display>(e : E)
|
||||
{
|
||||
error!("The handler encountered an error: {}", e);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "errorlog"))]
|
||||
fn errorlog<E>(_e : E) {}
|
||||
|
||||
fn handle_error<E>(e : E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
|
||||
where
|
||||
E : Display + IntoResponseError
|
||||
{
|
||||
into_response_helper(|| {
|
||||
errorlog(&e);
|
||||
e.into_response_error()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
impl<Res> ResourceResult for Pin<Box<dyn Future<Output = Res> + Send>>
|
||||
where
|
||||
Res : ResourceResult + 'static
|
||||
{
|
||||
type Err = Res::Err;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>
|
||||
{
|
||||
self.then(|result| {
|
||||
result.into_response()
|
||||
}).boxed()
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
Res::accepted_types()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
Res::schema()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode
|
||||
{
|
||||
Res::default_status()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||
struct Msg
|
||||
{
|
||||
msg : String
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
#[error("An Error")]
|
||||
struct MsgError;
|
||||
|
||||
#[test]
|
||||
fn result_from_future()
|
||||
{
|
||||
let nc = NoContent::default();
|
||||
let res = block_on(nc.into_response()).unwrap();
|
||||
|
||||
let fut_nc = async move { NoContent::default() }.boxed();
|
||||
let fut_res = block_on(fut_nc.into_response()).unwrap();
|
||||
|
||||
assert_eq!(res.status, fut_res.status);
|
||||
assert_eq!(res.mime, fut_res.mime);
|
||||
assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap());
|
||||
}
|
||||
}
|
141
src/result/no_content.rs
Normal file
141
src/result/no_content.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use super::{ResourceResult, handle_error};
|
||||
use crate::{IntoResponseError, Response};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::{OpenapiSchema, OpenapiType};
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use mime::Mime;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
future::Future,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
/**
|
||||
This is the return type of a resource that doesn't actually return something. It will result
|
||||
in a _204 No Content_ answer by default. You don't need to use this type directly if using
|
||||
the function attributes:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(_state: &mut State) {
|
||||
// do something
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct NoContent;
|
||||
|
||||
impl From<()> for NoContent
|
||||
{
|
||||
fn from(_ : ()) -> Self
|
||||
{
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceResult for NoContent
|
||||
{
|
||||
// TODO this shouldn't be a serde_json::Error
|
||||
type Err = serde_json::Error; // just for easier handling of `Result<NoContent, E>`
|
||||
|
||||
/// This will always be a _204 No Content_ together with an empty string.
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>
|
||||
{
|
||||
future::ok(Response::no_content()).boxed()
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
Some(Vec::new())
|
||||
}
|
||||
|
||||
/// Returns the schema of the `()` type.
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
<()>::schema()
|
||||
}
|
||||
|
||||
/// This will always be a _204 No Content_
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode
|
||||
{
|
||||
crate::StatusCode::NO_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ResourceResult for Result<NoContent, E>
|
||||
where
|
||||
E : Display + IntoResponseError<Err = serde_json::Error>
|
||||
{
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, serde_json::Error>> + Send>>
|
||||
{
|
||||
match self {
|
||||
Ok(nc) => nc.into_response(),
|
||||
Err(e) => handle_error(e)
|
||||
}
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
NoContent::accepted_types()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
<NoContent as ResourceResult>::schema()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode
|
||||
{
|
||||
NoContent::default_status()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
#[error("An Error")]
|
||||
struct MsgError;
|
||||
|
||||
#[test]
|
||||
fn no_content_has_empty_response()
|
||||
{
|
||||
let no_content = NoContent::default();
|
||||
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
||||
assert_eq!(res.mime, None);
|
||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_content_result()
|
||||
{
|
||||
let no_content : Result<NoContent, MsgError> = Ok(NoContent::default());
|
||||
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
||||
assert_eq!(res.mime, None);
|
||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||
}
|
||||
}
|
168
src/result/raw.rs
Normal file
168
src/result/raw.rs
Normal file
|
@ -0,0 +1,168 @@
|
|||
use super::{IntoResponseError, ResourceResult, handle_error};
|
||||
use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use futures_core::future::Future;
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::hyper::body::{Body, Bytes};
|
||||
use mime::Mime;
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
||||
use serde_json::error::Error as SerdeJsonError;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fmt::Display,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
/**
|
||||
This type can be used both as a raw request body, as well as as a raw response. However, all types
|
||||
of request bodies are accepted by this type. It is therefore recommended to derive your own type
|
||||
from [`RequestBody`] and only use this when you need to return a raw response. This is a usage
|
||||
example that simply returns its body:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::router::builder::*;
|
||||
# use gotham_restful::*;
|
||||
#[derive(Resource)]
|
||||
#[resource(create)]
|
||||
struct ImageResource;
|
||||
|
||||
#[create(ImageResource)]
|
||||
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||
body
|
||||
}
|
||||
# fn main() {
|
||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
# route.resource::<ImageResource>("img");
|
||||
# }));
|
||||
# }
|
||||
```
|
||||
|
||||
[`OpenapiType`]: trait.OpenapiType.html
|
||||
*/
|
||||
#[derive(Debug)]
|
||||
pub struct Raw<T>
|
||||
{
|
||||
pub raw : T,
|
||||
pub mime : Mime
|
||||
}
|
||||
|
||||
impl<T> Raw<T>
|
||||
{
|
||||
pub fn new(raw : T, mime : Mime) -> Self
|
||||
{
|
||||
Self { raw, mime }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> AsMut<U> for Raw<T>
|
||||
where
|
||||
T : AsMut<U>
|
||||
{
|
||||
fn as_mut(&mut self) -> &mut U
|
||||
{
|
||||
self.raw.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> AsRef<U> for Raw<T>
|
||||
where
|
||||
T : AsRef<U>
|
||||
{
|
||||
fn as_ref(&self) -> &U
|
||||
{
|
||||
self.raw.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : Clone> Clone for Raw<T>
|
||||
{
|
||||
fn clone(&self) -> Self
|
||||
{
|
||||
Self {
|
||||
raw: self.raw.clone(),
|
||||
mime: self.mime.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : for<'a> From<&'a [u8]>> FromBody for Raw<T>
|
||||
{
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_body(body : Bytes, mime : Mime) -> Result<Self, Self::Err>
|
||||
{
|
||||
Ok(Self::new(body.as_ref().into(), mime))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RequestBody for Raw<T>
|
||||
where
|
||||
Raw<T> : FromBody + ResourceType
|
||||
{
|
||||
}
|
||||
|
||||
impl<T : Into<Body>> ResourceResult for Raw<T>
|
||||
where
|
||||
Self : Send
|
||||
{
|
||||
type Err = SerdeJsonError; // just for easier handling of `Result<Raw<T>, E>`
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>>
|
||||
{
|
||||
future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ResourceResult for Result<Raw<T>, E>
|
||||
where
|
||||
Raw<T> : ResourceResult,
|
||||
E : Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
|
||||
{
|
||||
type Err = E::Err;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
|
||||
{
|
||||
match self {
|
||||
Ok(raw) => raw.into_response(),
|
||||
Err(e) => handle_error(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
<Raw<T> as ResourceResult>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use mime::TEXT_PLAIN;
|
||||
|
||||
#[test]
|
||||
fn raw_response()
|
||||
{
|
||||
let msg = "Test";
|
||||
let raw = Raw::new(msg, TEXT_PLAIN);
|
||||
let res = block_on(raw.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::OK);
|
||||
assert_eq!(res.mime, Some(TEXT_PLAIN));
|
||||
assert_eq!(res.full_body().unwrap(), msg.as_bytes());
|
||||
}
|
||||
}
|
106
src/result/result.rs
Normal file
106
src/result/result.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use super::{ResourceResult, handle_error, into_response_helper};
|
||||
use crate::{
|
||||
result::ResourceError,
|
||||
Response, ResponseBody, StatusCode
|
||||
};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use futures_core::future::Future;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::Display,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
pub trait IntoResponseError
|
||||
{
|
||||
type Err : Error + Send + 'static;
|
||||
|
||||
fn into_response_error(self) -> Result<Response, Self::Err>;
|
||||
}
|
||||
|
||||
impl<E : Error> IntoResponseError for E
|
||||
{
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn into_response_error(self) -> Result<Response, Self::Err>
|
||||
{
|
||||
let err : ResourceError = self.into();
|
||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, E> ResourceResult for Result<R, E>
|
||||
where
|
||||
R : ResponseBody,
|
||||
E : Display + IntoResponseError<Err = serde_json::Error>
|
||||
{
|
||||
type Err = E::Err;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
|
||||
{
|
||||
match self {
|
||||
Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))),
|
||||
Err(e) => handle_error(e)
|
||||
}
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
R::schema()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use crate::result::OrAllTypes;
|
||||
use futures_executor::block_on;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||
struct Msg
|
||||
{
|
||||
msg : String
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
#[error("An Error")]
|
||||
struct MsgError;
|
||||
|
||||
#[test]
|
||||
fn result_ok()
|
||||
{
|
||||
let ok : Result<Msg, MsgError> = Ok(Msg::default());
|
||||
let res = block_on(ok.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::OK);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_err()
|
||||
{
|
||||
let err : Result<Msg, MsgError> = Err(MsgError::default());
|
||||
let res = block_on(err.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_accepts_json()
|
||||
{
|
||||
assert!(<Result<Msg, MsgError>>::accepted_types().or_all_types().contains(&APPLICATION_JSON))
|
||||
}
|
||||
}
|
163
src/result/success.rs
Normal file
163
src/result/success.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use super::{ResourceResult, into_response_helper};
|
||||
use crate::{Response, ResponseBody};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use gotham::hyper::StatusCode;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
ops::{Deref, DerefMut}
|
||||
};
|
||||
|
||||
/**
|
||||
This can be returned from a resource when there is no cause of an error. It behaves similar to a
|
||||
smart pointer like box, it that it implements `AsRef`, `Deref` and the likes.
|
||||
|
||||
Usage example:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[derive(Deserialize, Serialize)]
|
||||
# #[derive(OpenapiType)]
|
||||
struct MyResponse {
|
||||
message: &'static str
|
||||
}
|
||||
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
||||
let res = MyResponse { message: "I'm always happy" };
|
||||
res.into()
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
#[derive(Debug)]
|
||||
pub struct Success<T>(T);
|
||||
|
||||
impl<T> AsMut<T> for Success<T>
|
||||
{
|
||||
fn as_mut(&mut self) -> &mut T
|
||||
{
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Success<T>
|
||||
{
|
||||
fn as_ref(&self) -> &T
|
||||
{
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Success<T>
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T
|
||||
{
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Success<T>
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut T
|
||||
{
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Success<T>
|
||||
{
|
||||
fn from(t : T) -> Self
|
||||
{
|
||||
Self(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : Clone> Clone for Success<T>
|
||||
{
|
||||
fn clone(&self) -> Self
|
||||
{
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : Copy> Copy for Success<T>
|
||||
{
|
||||
}
|
||||
|
||||
impl<T : Default> Default for Success<T>
|
||||
{
|
||||
fn default() -> Self
|
||||
{
|
||||
Self(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : ResponseBody> ResourceResult for Success<T>
|
||||
where
|
||||
Self : Send
|
||||
{
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>
|
||||
{
|
||||
into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?)))
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
T::schema()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test
|
||||
{
|
||||
use super::*;
|
||||
use crate::result::OrAllTypes;
|
||||
use futures_executor::block_on;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||
struct Msg
|
||||
{
|
||||
msg : String
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_always_successfull()
|
||||
{
|
||||
let success : Success<Msg> = Msg::default().into();
|
||||
let res = block_on(success.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::OK);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_accepts_json()
|
||||
{
|
||||
assert!(<Success<Msg>>::accepted_types().or_all_types().contains(&APPLICATION_JSON))
|
||||
}
|
||||
}
|
438
src/routing.rs
Normal file
438
src/routing.rs
Normal file
|
@ -0,0 +1,438 @@
|
|||
use crate::{
|
||||
matcher::{AcceptHeaderMatcher, ContentTypeMatcher},
|
||||
resource::*,
|
||||
result::{ResourceError, ResourceResult},
|
||||
RequestBody,
|
||||
Response,
|
||||
StatusCode
|
||||
};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::openapi::{
|
||||
builder::{OpenapiBuilder, OpenapiInfo},
|
||||
router::OpenapiRouter
|
||||
};
|
||||
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::{
|
||||
handler::{HandlerError, HandlerFuture, IntoHandlerError},
|
||||
helpers::http::response::{create_empty_response, create_response},
|
||||
pipeline::chain::PipelineHandleChain,
|
||||
router::{
|
||||
builder::*,
|
||||
non_match::RouteNonMatch,
|
||||
route::matcher::RouteMatcher
|
||||
},
|
||||
state::{FromState, State}
|
||||
};
|
||||
use gotham::hyper::{
|
||||
body::to_bytes,
|
||||
header::CONTENT_TYPE,
|
||||
Body,
|
||||
HeaderMap,
|
||||
Method
|
||||
};
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{
|
||||
future::Future,
|
||||
panic::RefUnwindSafe,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
/// Allow us to extract an id from a path.
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
struct PathExtractor<ID : RefUnwindSafe + Send + 'static>
|
||||
{
|
||||
id : ID
|
||||
}
|
||||
|
||||
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
|
||||
/// router into one that will only allow RESTful resources, but record them and generate
|
||||
/// an OpenAPI specification on request.
|
||||
#[cfg(feature = "openapi")]
|
||||
pub trait WithOpenapi<D>
|
||||
{
|
||||
fn with_openapi<F>(&mut self, info : OpenapiInfo, block : F)
|
||||
where
|
||||
F : FnOnce(OpenapiRouter<'_, D>);
|
||||
}
|
||||
|
||||
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
||||
/// any RESTful `Resource` with a path.
|
||||
pub trait DrawResources
|
||||
{
|
||||
fn resource<R : Resource>(&mut self, path : &str);
|
||||
}
|
||||
|
||||
/// This trait allows to draw routes within an resource. Use this only inside the
|
||||
/// `Resource::setup` method.
|
||||
pub trait DrawResourceRoutes
|
||||
{
|
||||
fn read_all<Handler : ResourceReadAll>(&mut self);
|
||||
|
||||
fn read<Handler : ResourceRead>(&mut self);
|
||||
|
||||
fn search<Handler : ResourceSearch>(&mut self);
|
||||
|
||||
fn create<Handler : ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static;
|
||||
|
||||
fn change_all<Handler : ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static;
|
||||
|
||||
fn change<Handler : ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static;
|
||||
|
||||
fn remove_all<Handler : ResourceRemoveAll>(&mut self);
|
||||
|
||||
fn remove<Handler : ResourceRemove>(&mut self);
|
||||
}
|
||||
|
||||
fn response_from(res : Response, state : &State) -> gotham::hyper::Response<Body>
|
||||
{
|
||||
let mut r = create_empty_response(state, res.status);
|
||||
if let Some(mime) = res.mime
|
||||
{
|
||||
r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap());
|
||||
}
|
||||
if Method::borrow_from(state) != Method::HEAD
|
||||
{
|
||||
*r.body_mut() = res.body;
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
async fn to_handler_future<F, R>(state : State, get_result : F) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)>
|
||||
where
|
||||
F : FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
||||
R : ResourceResult
|
||||
{
|
||||
let (state, res) = get_result(state).await;
|
||||
let res = res.into_response().await;
|
||||
match res {
|
||||
Ok(res) => {
|
||||
let r = response_from(res, &state);
|
||||
Ok((state, r))
|
||||
},
|
||||
Err(e) => Err((state, e.into_handler_error()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn body_to_res<B, F, R>(mut state : State, get_result : F) -> (State, Result<gotham::hyper::Response<Body>, HandlerError>)
|
||||
where
|
||||
B : RequestBody,
|
||||
F : FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
||||
R : ResourceResult
|
||||
{
|
||||
let body = to_bytes(Body::take_from(&mut state)).await;
|
||||
|
||||
let body = match body {
|
||||
Ok(body) => body,
|
||||
Err(e) => return (state, Err(e.into_handler_error()))
|
||||
};
|
||||
|
||||
let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
|
||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||
None => {
|
||||
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
return (state, Ok(res))
|
||||
}
|
||||
};
|
||||
|
||||
let res = {
|
||||
let body = match B::from_body(body, content_type) {
|
||||
Ok(body) => body,
|
||||
Err(e) => {
|
||||
let error : ResourceError = e.into();
|
||||
let res = match serde_json::to_string(&error) {
|
||||
Ok(json) => {
|
||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
Ok(res)
|
||||
},
|
||||
Err(e) => Err(e.into_handler_error())
|
||||
};
|
||||
return (state, res)
|
||||
}
|
||||
};
|
||||
get_result(state, body)
|
||||
};
|
||||
|
||||
let (state, res) = res.await;
|
||||
let res = res.into_response().await;
|
||||
|
||||
let res = match res {
|
||||
Ok(res) => {
|
||||
let r = response_from(res, &state);
|
||||
Ok(r)
|
||||
},
|
||||
Err(e) => Err(e.into_handler_error())
|
||||
};
|
||||
(state, res)
|
||||
}
|
||||
|
||||
fn handle_with_body<B, F, R>(state : State, get_result : F) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
B : RequestBody + 'static,
|
||||
F : FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
|
||||
R : ResourceResult + Send + 'static
|
||||
{
|
||||
body_to_res(state, get_result)
|
||||
.then(|(state, res)| match res {
|
||||
Ok(ok) => future::ok((state, ok)),
|
||||
Err(err) => future::err((state, err))
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn read_all_handler<Handler : ResourceReadAll>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
to_handler_future(state, |state| Handler::read_all(state)).boxed()
|
||||
}
|
||||
|
||||
fn read_handler<Handler : ResourceRead>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
let id = {
|
||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
to_handler_future(state, |state| Handler::read(state, id)).boxed()
|
||||
}
|
||||
|
||||
fn search_handler<Handler : ResourceSearch>(mut state : State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
let query = Handler::Query::take_from(&mut state);
|
||||
to_handler_future(state, |state| Handler::search(state, query)).boxed()
|
||||
}
|
||||
|
||||
fn create_handler<Handler : ResourceCreate>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
|
||||
}
|
||||
|
||||
fn change_all_handler<Handler : ResourceChangeAll>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body))
|
||||
}
|
||||
|
||||
fn change_handler<Handler : ResourceChange>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res : 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let id = {
|
||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body))
|
||||
}
|
||||
|
||||
fn remove_all_handler<Handler : ResourceRemoveAll>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
to_handler_future(state, |state| Handler::remove_all(state)).boxed()
|
||||
}
|
||||
|
||||
fn remove_handler<Handler : ResourceRemove>(state : State) -> Pin<Box<HandlerFuture>>
|
||||
{
|
||||
let id = {
|
||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
to_handler_future(state, |state| Handler::remove(state, id)).boxed()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MaybeMatchAcceptHeader
|
||||
{
|
||||
matcher : Option<AcceptHeaderMatcher>
|
||||
}
|
||||
|
||||
impl RouteMatcher for MaybeMatchAcceptHeader
|
||||
{
|
||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
||||
{
|
||||
match &self.matcher {
|
||||
Some(matcher) => matcher.is_match(state),
|
||||
None => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
|
||||
{
|
||||
fn from(types : Option<Vec<Mime>>) -> Self
|
||||
{
|
||||
let types = match types {
|
||||
Some(types) if types.is_empty() => None,
|
||||
types => types
|
||||
};
|
||||
Self {
|
||||
matcher: types.map(AcceptHeaderMatcher::new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MaybeMatchContentTypeHeader
|
||||
{
|
||||
matcher : Option<ContentTypeMatcher>
|
||||
}
|
||||
|
||||
impl RouteMatcher for MaybeMatchContentTypeHeader
|
||||
{
|
||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
||||
{
|
||||
match &self.matcher {
|
||||
Some(matcher) => matcher.is_match(state),
|
||||
None => Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader
|
||||
{
|
||||
fn from(types : Option<Vec<Mime>>) -> Self
|
||||
{
|
||||
Self {
|
||||
matcher: types.map(ContentTypeMatcher::new).map(ContentTypeMatcher::allow_no_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! implDrawResourceRoutes {
|
||||
($implType:ident) => {
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn with_openapi<F>(&mut self, info : OpenapiInfo, block : F)
|
||||
where
|
||||
F : FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>)
|
||||
{
|
||||
let router = OpenapiRouter {
|
||||
router: self,
|
||||
scope: None,
|
||||
openapi_builder: &mut OpenapiBuilder::new(info)
|
||||
};
|
||||
block(router);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C, P> DrawResources for $implType<'a, C, P>
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn resource<R : Resource>(&mut self, path : &str)
|
||||
{
|
||||
R::setup((self, path));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters
|
||||
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
||||
where
|
||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P : RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn read_all<Handler : ResourceReadAll>(&mut self)
|
||||
{
|
||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0.get(&self.1)
|
||||
.extend_route_matcher(matcher)
|
||||
.to(|state| read_all_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn read<Handler : ResourceRead>(&mut self)
|
||||
{
|
||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0.get(&format!("{}/:id", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| read_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn search<Handler : ResourceSearch>(&mut self)
|
||||
{
|
||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0.get(&format!("{}/search", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_query_string_extractor::<Handler::Query>()
|
||||
.to(|state| search_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn create<Handler : ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res : Send + 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0.post(&self.1)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.to(|state| create_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn change_all<Handler : ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res : Send + 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0.put(&self.1)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.to(|state| change_all_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn change<Handler : ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res : Send + 'static,
|
||||
Handler::Body : 'static
|
||||
{
|
||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0.put(&format!("{}/:id", self.1))
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| change_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn remove_all<Handler : ResourceRemoveAll>(&mut self)
|
||||
{
|
||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0.delete(&self.1)
|
||||
.extend_route_matcher(matcher)
|
||||
.to(|state| remove_all_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn remove<Handler : ResourceRemove>(&mut self)
|
||||
{
|
||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0.delete(&format!("{}/:id", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| remove_handler::<Handler>(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implDrawResourceRoutes!(RouterBuilder);
|
||||
implDrawResourceRoutes!(ScopeBuilder);
|
137
src/types.rs
Normal file
137
src/types.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiType;
|
||||
|
||||
use gotham::hyper::body::Bytes;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{
|
||||
error::Error,
|
||||
panic::RefUnwindSafe
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
pub trait ResourceType
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
impl<T> ResourceType for T
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
pub trait ResourceType : OpenapiType
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<T : OpenapiType> ResourceType for T
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// A type that can be used inside a response body. Implemented for every type that is
|
||||
/// serializable with serde. If the `openapi` feature is used, it must also be of type
|
||||
/// `OpenapiType`.
|
||||
pub trait ResponseBody : ResourceType + Serialize
|
||||
{
|
||||
}
|
||||
|
||||
impl<T : ResourceType + Serialize> ResponseBody for T
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
This trait should be implemented for every type that can be built from an HTTP request body
|
||||
plus its media type. For most use cases it is sufficient to derive this trait, you usually
|
||||
don't need to manually implement this. Therefore, make sure that the first variable of
|
||||
your struct can be built from [`Bytes`], and the second one can be build from [`Mime`].
|
||||
If you have any additional variables, they need to be `Default`. This is an example of
|
||||
such a struct:
|
||||
|
||||
```rust
|
||||
# #[macro_use] extern crate gotham_restful;
|
||||
# use gotham_restful::*;
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
||||
struct RawImage {
|
||||
content: Vec<u8>,
|
||||
content_type: Mime
|
||||
}
|
||||
```
|
||||
|
||||
[`Bytes`]: ../bytes/struct.Bytes.html
|
||||
[`Mime`]: ../mime/struct.Mime.html
|
||||
*/
|
||||
pub trait FromBody : Sized
|
||||
{
|
||||
/// The error type returned by the conversion if it was unsuccessfull. When using the derive
|
||||
/// macro, there is no way to trigger an error, so `Infallible` is used here. However, this
|
||||
/// might change in the future.
|
||||
type Err : Error;
|
||||
|
||||
/// Perform the conversion.
|
||||
fn from_body(body : Bytes, content_type : Mime) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
impl<T : DeserializeOwned> FromBody for T
|
||||
{
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn from_body(body : Bytes, _content_type : Mime) -> Result<Self, Self::Err>
|
||||
{
|
||||
serde_json::from_slice(&body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
A type that can be used inside a request body. Implemented for every type that is deserializable
|
||||
with serde. If the `openapi` feature is used, it must also be of type [`OpenapiType`].
|
||||
|
||||
If you want a non-deserializable type to be used as a request body, e.g. because you'd like to
|
||||
get the raw data, you can derive it for your own type. All you need is to have a type implementing
|
||||
[`FromBody`] and optionally a list of supported media types:
|
||||
|
||||
```rust
|
||||
# #[macro_use] extern crate gotham_restful;
|
||||
# use gotham_restful::*;
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
||||
struct RawImage {
|
||||
content: Vec<u8>,
|
||||
content_type: Mime
|
||||
}
|
||||
```
|
||||
|
||||
[`FromBody`]: trait.FromBody.html
|
||||
[`OpenapiType`]: trait.OpenapiType.html
|
||||
*/
|
||||
pub trait RequestBody : ResourceType + FromBody
|
||||
{
|
||||
/// Return all types that are supported as content types. Use `None` if all types are supported.
|
||||
fn supported_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T : ResourceType + DeserializeOwned> RequestBody for T
|
||||
{
|
||||
fn supported_types() -> Option<Vec<Mime>>
|
||||
{
|
||||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
}
|
||||
|
||||
/// A type than can be used as a parameter to a resource method. Implemented for every type
|
||||
/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of
|
||||
/// type `OpenapiType`.
|
||||
pub trait ResourceID : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync
|
||||
{
|
||||
}
|
||||
|
||||
impl<T : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T
|
||||
{
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue