mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-23 04:52:28 +00:00
Merge branch 'error-derive' into 'master'
Allow custom error types through a macro and allow them to be used with Result Closes #13 and #9 See merge request msrd0/gotham-restful!12
This commit is contained in:
commit
0d95ca4abb
19 changed files with 1165 additions and 751 deletions
|
@ -70,7 +70,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Uploads and Downloads can also be handled, but you need to specify the mime type manually:
|
Uploads and Downloads can also be handled:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
|
|
|
@ -36,7 +36,7 @@ struct User
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_read_all(Users)]
|
#[rest_read_all(Users)]
|
||||||
fn read_all(_state : &mut State) -> Success<Vec<Option<User>>>
|
fn read_all() -> Success<Vec<Option<User>>>
|
||||||
{
|
{
|
||||||
vec![Username().fake(), Username().fake()]
|
vec![Username().fake(), Username().fake()]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -46,56 +46,55 @@ fn read_all(_state : &mut State) -> Success<Vec<Option<User>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_read(Users)]
|
#[rest_read(Users)]
|
||||||
fn read(_state : &mut State, id : u64) -> Success<User>
|
fn read(id : u64) -> Success<User>
|
||||||
{
|
{
|
||||||
let username : String = Username().fake();
|
let username : String = Username().fake();
|
||||||
User { username: format!("{}{}", username, id) }.into()
|
User { username: format!("{}{}", username, id) }.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_search(Users)]
|
#[rest_search(Users)]
|
||||||
fn search(_state : &mut State, query : User) -> Success<User>
|
fn search(query : User) -> Success<User>
|
||||||
{
|
{
|
||||||
query.into()
|
query.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_create(Users)]
|
#[rest_create(Users)]
|
||||||
fn create(_state : &mut State, body : User)
|
fn create(body : User)
|
||||||
{
|
{
|
||||||
info!("Created User: {}", body.username);
|
info!("Created User: {}", body.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_update_all(Users)]
|
#[rest_update_all(Users)]
|
||||||
fn update_all(_state : &mut State, body : Vec<User>)
|
fn update_all(body : Vec<User>)
|
||||||
{
|
{
|
||||||
info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::<Vec<String>>());
|
info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::<Vec<String>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_update(Users)]
|
#[rest_update(Users)]
|
||||||
fn update(_state : &mut State, id : u64, body : User)
|
fn update(id : u64, body : User)
|
||||||
{
|
{
|
||||||
info!("Change User {} to {}", id, body.username);
|
info!("Change User {} to {}", id, body.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_delete_all(Users)]
|
#[rest_delete_all(Users)]
|
||||||
fn delete_all(_state : &mut State)
|
fn delete_all()
|
||||||
{
|
{
|
||||||
info!("Delete all Users");
|
info!("Delete all Users");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_delete(Users)]
|
#[rest_delete(Users)]
|
||||||
fn delete(_state : &mut State, id : u64)
|
fn delete(id : u64)
|
||||||
{
|
{
|
||||||
info!("Delete User {}", id);
|
info!("Delete User {}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_read_all(Auth)]
|
#[rest_read_all(Auth)]
|
||||||
fn auth_read_all(auth : AuthStatus<()>) -> AuthResult<Success<String>>
|
fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess<String>
|
||||||
{
|
{
|
||||||
let str : Success<String> = match auth {
|
match auth {
|
||||||
AuthStatus::Authenticated(data) => format!("{:?}", data).into(),
|
AuthStatus::Authenticated(data) => Ok(format!("{:?}", data).into()),
|
||||||
_ => return AuthErr
|
_ => Err(Forbidden)
|
||||||
};
|
}
|
||||||
str.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDR : &str = "127.0.0.1:18080";
|
const ADDR : &str = "127.0.0.1:18080";
|
||||||
|
|
|
@ -52,7 +52,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Uploads and Downloads can also be handled, but you need to specify the mime type manually:
|
Uploads and Downloads can also be handled:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -131,6 +131,8 @@ pub mod export
|
||||||
{
|
{
|
||||||
pub use futures_util::future::FutureExt;
|
pub use futures_util::future::FutureExt;
|
||||||
|
|
||||||
|
pub use serde_json;
|
||||||
|
|
||||||
#[cfg(feature = "database")]
|
#[cfg(feature = "database")]
|
||||||
pub use gotham_middleware_diesel::Repo;
|
pub use gotham_middleware_diesel::Repo;
|
||||||
|
|
||||||
|
@ -176,14 +178,20 @@ pub use resource::{
|
||||||
ResourceDelete
|
ResourceDelete
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod response;
|
||||||
|
pub use response::Response;
|
||||||
|
|
||||||
mod result;
|
mod result;
|
||||||
pub use result::{
|
pub use result::{
|
||||||
|
AuthError,
|
||||||
|
AuthError::Forbidden,
|
||||||
|
AuthErrorOrOther,
|
||||||
AuthResult,
|
AuthResult,
|
||||||
AuthResult::AuthErr,
|
AuthSuccess,
|
||||||
|
IntoResponseError,
|
||||||
NoContent,
|
NoContent,
|
||||||
Raw,
|
Raw,
|
||||||
ResourceResult,
|
ResourceResult,
|
||||||
Response,
|
|
||||||
Success
|
Success
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
63
gotham_restful/src/response.rs
Normal file
63
gotham_restful/src/response.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use gotham::hyper::{Body, StatusCode};
|
||||||
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
|
|
||||||
|
/// A response, used to create the final gotham response from.
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,704 +0,0 @@
|
||||||
use crate::{ResponseBody, StatusCode};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::{OpenapiSchema, OpenapiType};
|
|
||||||
use futures_core::future::Future;
|
|
||||||
use futures_util::{future, future::FutureExt};
|
|
||||||
use gotham::hyper::Body;
|
|
||||||
#[cfg(feature = "errorlog")]
|
|
||||||
use log::error;
|
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::error::Error as SerdeJsonError;
|
|
||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
fmt::Debug,
|
|
||||||
pin::Pin
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A response, used to create the final gotham response from.
|
|
||||||
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)]
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 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() -> StatusCode
|
|
||||||
{
|
|
||||||
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 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 : std::fmt::Display>(e : E)
|
|
||||||
{
|
|
||||||
error!("The handler encountered an error: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "errorlog"))]
|
|
||||||
fn errorlog<E>(_e : E) {}
|
|
||||||
|
|
||||||
impl<R : ResponseBody, E : Error> ResourceResult for Result<R, E>
|
|
||||||
where
|
|
||||||
Self : Send
|
|
||||||
{
|
|
||||||
type Err = SerdeJsonError;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>>
|
|
||||||
{
|
|
||||||
into_response_helper(|| {
|
|
||||||
Ok(match self {
|
|
||||||
Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?),
|
|
||||||
Err(e) => {
|
|
||||||
errorlog(&e);
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
R::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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() -> StatusCode
|
|
||||||
{
|
|
||||||
Res::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
This can be returned from a resource when there is no cause of an error. For 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)]
|
|
||||||
# struct MyResource;
|
|
||||||
#
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
# #[derive(OpenapiType)]
|
|
||||||
struct MyResponse {
|
|
||||||
message: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rest_read_all(MyResource)]
|
|
||||||
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
|
||||||
let res = MyResponse { message: "I'm always happy".to_string() };
|
|
||||||
res.into()
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
pub struct Success<T>(T);
|
|
||||||
|
|
||||||
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 : Debug> Debug for Success<T>
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Success({:?})", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResponseBody> ResourceResult for Success<T>
|
|
||||||
where
|
|
||||||
Self : Send
|
|
||||||
{
|
|
||||||
type Err = SerdeJsonError;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>>
|
|
||||||
{
|
|
||||||
into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
T::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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)]
|
|
||||||
# struct MyResource;
|
|
||||||
#
|
|
||||||
# #[derive(Clone, Deserialize)]
|
|
||||||
# struct MyAuthData { exp : u64 }
|
|
||||||
#
|
|
||||||
#[rest_read_all(MyResource)]
|
|
||||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent> {
|
|
||||||
let auth_data = match auth {
|
|
||||||
AuthStatus::Authenticated(data) => data,
|
|
||||||
_ => return AuthErr
|
|
||||||
};
|
|
||||||
// do something
|
|
||||||
NoContent::default().into()
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
pub enum AuthResult<T>
|
|
||||||
{
|
|
||||||
Ok(T),
|
|
||||||
AuthErr
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> AuthResult<T>
|
|
||||||
{
|
|
||||||
pub fn is_ok(&self) -> bool
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::Ok(_) => true,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unwrap(self) -> T
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::Ok(data) => data,
|
|
||||||
_ => panic!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<T> for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn from(t : T) -> Self
|
|
||||||
{
|
|
||||||
Self::Ok(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Clone> Clone for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::Ok(t) => Self::Ok(t.clone()),
|
|
||||||
Self::AuthErr => Self::AuthErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Debug> Debug for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Ok(t) => write!(f, "Ok({:?})", t),
|
|
||||||
Self::AuthErr => write!(f, "AuthErr")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResourceResult> ResourceResult for AuthResult<T>
|
|
||||||
{
|
|
||||||
type Err = T::Err;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>
|
|
||||||
{
|
|
||||||
match self
|
|
||||||
{
|
|
||||||
Self::Ok(res) => res.into_response(),
|
|
||||||
Self::AuthErr => future::ok(Response::forbidden()).boxed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
T::accepted_types()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
T::schema()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn default_status() -> StatusCode
|
|
||||||
{
|
|
||||||
T::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResourceResult<Err = SerdeJsonError>, E : Error> ResourceResult for Result<AuthResult<T>, E>
|
|
||||||
{
|
|
||||||
type Err = T::Err;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, T::Err>> + Send>>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(r) => r.into_response(),
|
|
||||||
Err(e) => {
|
|
||||||
into_response_helper(|| {
|
|
||||||
errorlog(&e);
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
T::accepted_types()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
T::schema()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn default_status() -> StatusCode
|
|
||||||
{
|
|
||||||
T::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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)]
|
|
||||||
# struct MyResource;
|
|
||||||
#
|
|
||||||
#[rest_read_all(MyResource)]
|
|
||||||
fn read_all(_state: &mut State) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct NoContent;
|
|
||||||
|
|
||||||
impl From<()> for NoContent
|
|
||||||
{
|
|
||||||
fn from(_ : ()) -> Self
|
|
||||||
{
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResourceResult for NoContent
|
|
||||||
{
|
|
||||||
type Err = SerdeJsonError; // 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() -> StatusCode
|
|
||||||
{
|
|
||||||
StatusCode::NO_CONTENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E : Error> ResourceResult for Result<NoContent, E>
|
|
||||||
where
|
|
||||||
Self : Send
|
|
||||||
{
|
|
||||||
type Err = SerdeJsonError;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(nc) => nc.into_response(),
|
|
||||||
Err(e) => into_response_helper(|| {
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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() -> StatusCode
|
|
||||||
{
|
|
||||||
NoContent::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 : Clone> Clone for Raw<T>
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
raw: self.raw.clone(),
|
|
||||||
mime: self.mime.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Debug> Debug for Raw<T>
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Raw({:?}, {:?})", self.raw, self.mime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 : Error> ResourceResult for Result<Raw<T>, E>
|
|
||||||
where
|
|
||||||
Self : Send,
|
|
||||||
Raw<T> : ResourceResult<Err = SerdeJsonError>
|
|
||||||
{
|
|
||||||
type Err = SerdeJsonError;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(raw) => raw.into_response(),
|
|
||||||
Err(e) => into_response_helper(|| {
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
<Raw<T> as ResourceResult>::accepted_types()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
|
||||||
struct Msg
|
|
||||||
{
|
|
||||||
msg : String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Error)]
|
|
||||||
#[error("An Error")]
|
|
||||||
struct MsgError;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resource_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 resource_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_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 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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());
|
|
||||||
}
|
|
||||||
}
|
|
107
gotham_restful/src/result/auth_result.rs
Normal file
107
gotham_restful/src/result/auth_result.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
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(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)]
|
||||||
|
# struct MyResource;
|
||||||
|
#
|
||||||
|
# #[derive(Clone, Deserialize)]
|
||||||
|
# struct MyAuthData { exp : u64 }
|
||||||
|
#
|
||||||
|
#[rest_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(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)]
|
||||||
|
# struct MyResource;
|
||||||
|
#
|
||||||
|
# #[derive(Clone, Deserialize)]
|
||||||
|
# struct MyAuthData { exp : u64 }
|
||||||
|
#
|
||||||
|
#[rest_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>>;
|
224
gotham_restful/src/result/mod.rs
Normal file
224
gotham_restful/src/result/mod.rs
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
use crate::Response;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::OpenapiSchema;
|
||||||
|
use futures_util::future::FutureExt;
|
||||||
|
use mime::Mime;
|
||||||
|
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;
|
||||||
|
|
||||||
|
mod result;
|
||||||
|
pub use result::IntoResponseError;
|
||||||
|
|
||||||
|
mod success;
|
||||||
|
pub use success::Success;
|
||||||
|
|
||||||
|
/// 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 crate::{OpenapiType, StatusCode};
|
||||||
|
use futures_executor::block_on;
|
||||||
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
|
struct Msg
|
||||||
|
{
|
||||||
|
msg : String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Error)]
|
||||||
|
#[error("An Error")]
|
||||||
|
struct MsgError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resource_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 resource_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_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 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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
gotham_restful/src/result/no_content.rs
Normal file
106
gotham_restful/src/result/no_content.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
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)]
|
||||||
|
# struct MyResource;
|
||||||
|
#
|
||||||
|
#[rest_read_all(MyResource)]
|
||||||
|
fn read_all(_state: &mut State) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
#[derive(Clone, Copy, 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()
|
||||||
|
}
|
||||||
|
}
|
90
gotham_restful/src/result/raw.rs
Normal file
90
gotham_restful/src/result/raw.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use super::{IntoResponseError, ResourceResult, handle_error};
|
||||||
|
use crate::{Response, StatusCode};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::OpenapiSchema;
|
||||||
|
use futures_core::future::Future;
|
||||||
|
use futures_util::{future, future::FutureExt};
|
||||||
|
use gotham::hyper::Body;
|
||||||
|
use mime::Mime;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
||||||
|
use serde_json::error::Error as SerdeJsonError;
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
pin::Pin
|
||||||
|
};
|
||||||
|
|
||||||
|
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 : Clone> Clone for Raw<T>
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
raw: self.raw.clone(),
|
||||||
|
mime: self.mime.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T : Debug> Debug for Raw<T>
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Raw({:?}, {:?})", self.raw, self.mime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
59
gotham_restful/src/result/result.rs
Normal file
59
gotham_restful/src/result/result.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
136
gotham_restful/src/result/success.rs
Normal file
136
gotham_restful/src/result/success.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
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)]
|
||||||
|
# struct MyResource;
|
||||||
|
#
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
# #[derive(OpenapiType)]
|
||||||
|
struct MyResponse {
|
||||||
|
message: &'static str
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rest_read_all(MyResource)]
|
||||||
|
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
||||||
|
let res = MyResponse { message: "I'm always happy" };
|
||||||
|
res.into()
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
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 : Debug> Debug for Success<T>
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Success({:?})", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,16 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
matcher::{AcceptHeaderMatcher, ContentTypeMatcher},
|
matcher::{AcceptHeaderMatcher, ContentTypeMatcher},
|
||||||
openapi::router::OpenapiRouter,
|
|
||||||
resource::*,
|
resource::*,
|
||||||
result::{ResourceError, ResourceResult, Response},
|
result::{ResourceError, ResourceResult},
|
||||||
RequestBody,
|
RequestBody,
|
||||||
|
Response,
|
||||||
StatusCode
|
StatusCode
|
||||||
};
|
};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::openapi::builder::OpenapiBuilder;
|
use crate::openapi::{
|
||||||
|
builder::OpenapiBuilder,
|
||||||
|
router::OpenapiRouter
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::{future, future::FutureExt};
|
use futures_util::{future, future::FutureExt};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::OpenapiType;
|
use crate::OpenapiType;
|
||||||
use crate::result::ResourceError;
|
|
||||||
|
|
||||||
use gotham::hyper::body::Bytes;
|
use gotham::hyper::body::Bytes;
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::panic::RefUnwindSafe;
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
panic::RefUnwindSafe
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
#[cfg(not(feature = "openapi"))]
|
||||||
pub trait ResourceType
|
pub trait ResourceType
|
||||||
|
@ -44,7 +47,7 @@ impl<T : ResourceType + Serialize> ResponseBody for T
|
||||||
/// to create the type from a hyper body chunk and it's content type.
|
/// to create the type from a hyper body chunk and it's content type.
|
||||||
pub trait FromBody : Sized
|
pub trait FromBody : Sized
|
||||||
{
|
{
|
||||||
type Err : Into<ResourceError>;
|
type Err : Error;
|
||||||
|
|
||||||
/// Create the request body from a raw body and the content type.
|
/// Create the request body from a raw body and the content type.
|
||||||
fn from_body(body : Bytes, content_type : Mime) -> Result<Self, Self::Err>;
|
fn from_body(body : Bytes, content_type : Mime) -> Result<Self, Self::Err>;
|
||||||
|
@ -60,6 +63,14 @@ impl<T : DeserializeOwned> FromBody for T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This error type can be used by `FromBody` implementations when there is no need to return any
|
||||||
|
/// errors.
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Error)]
|
||||||
|
#[error("No Error")]
|
||||||
|
pub struct FromBodyNoError;
|
||||||
|
|
||||||
|
|
||||||
/// A type that can be used inside a request body. Implemented for every type that is
|
/// 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
|
/// deserializable with serde. If the `openapi` feature is used, it must also be of type
|
||||||
/// `OpenapiType`.
|
/// `OpenapiType`.
|
||||||
|
|
|
@ -18,8 +18,10 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
heck = "0.3.1"
|
heck = "0.3.1"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
proc-macro2 = "1.0.10"
|
proc-macro2 = "1.0.10"
|
||||||
quote = "1.0.3"
|
quote = "1.0.3"
|
||||||
|
regex = "1.3.7"
|
||||||
syn = { version = "1.0.17", features = ["extra-traits", "full"] }
|
syn = { version = "1.0.17", features = ["extra-traits", "full"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -120,7 +120,7 @@ fn expand(tokens : TokenStream) -> Result<TokenStream2, Error>
|
||||||
impl #generics #krate::FromBody for #ident #generics
|
impl #generics #krate::FromBody for #ident #generics
|
||||||
where #where_clause
|
where #where_clause
|
||||||
{
|
{
|
||||||
type Err = String;
|
type Err = #krate::FromBodyNoError;
|
||||||
|
|
||||||
fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result<Self, Self::Err>
|
fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result<Self, Self::Err>
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,6 +12,8 @@ mod request_body;
|
||||||
use request_body::expand_request_body;
|
use request_body::expand_request_body;
|
||||||
mod resource;
|
mod resource;
|
||||||
use resource::expand_resource;
|
use resource::expand_resource;
|
||||||
|
mod resource_error;
|
||||||
|
use resource_error::expand_resource_error;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
mod openapi_type;
|
mod openapi_type;
|
||||||
|
|
||||||
|
@ -52,6 +54,13 @@ pub fn derive_resource(tokens : TokenStream) -> TokenStream
|
||||||
print_tokens(expand_resource(tokens))
|
print_tokens(expand_resource(tokens))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(ResourceError, attributes(display, from, status))]
|
||||||
|
pub fn derive_resource_error(tokens : TokenStream) -> TokenStream
|
||||||
|
{
|
||||||
|
print_tokens(expand_resource_error(tokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
use crate::util::CollectToResult;
|
use crate::util::{CollectToResult, remove_parens};
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::{
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
Delimiter,
|
|
||||||
TokenStream as TokenStream2,
|
|
||||||
TokenTree
|
|
||||||
};
|
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use std::{iter, iter::FromIterator};
|
|
||||||
use syn::{
|
use syn::{
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
Attribute,
|
Attribute,
|
||||||
|
@ -84,21 +79,6 @@ fn to_bool(lit : &Lit) -> Result<bool, Error>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_parens(input : TokenStream2) -> TokenStream2
|
|
||||||
{
|
|
||||||
let iter = input.into_iter().flat_map(|tt| {
|
|
||||||
if let TokenTree::Group(group) = &tt
|
|
||||||
{
|
|
||||||
if group.delimiter() == Delimiter::Parenthesis
|
|
||||||
{
|
|
||||||
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box::new(iter::once(tt))
|
|
||||||
});
|
|
||||||
TokenStream2::from_iter(iter)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_attributes(input : &[Attribute]) -> Result<Attrs, Error>
|
fn parse_attributes(input : &[Attribute]) -> Result<Attrs, Error>
|
||||||
{
|
{
|
||||||
let mut parsed = Attrs::default();
|
let mut parsed = Attrs::default();
|
||||||
|
|
299
gotham_restful_derive/src/resource_error.rs
Normal file
299
gotham_restful_derive/src/resource_error.rs
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
use crate::util::{CollectToResult, remove_parens};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use regex::Regex;
|
||||||
|
use std::iter;
|
||||||
|
use syn::{
|
||||||
|
parse_macro_input,
|
||||||
|
spanned::Spanned,
|
||||||
|
Attribute,
|
||||||
|
Data,
|
||||||
|
DeriveInput,
|
||||||
|
Error,
|
||||||
|
Fields,
|
||||||
|
GenericParam,
|
||||||
|
Ident,
|
||||||
|
LitStr,
|
||||||
|
Path,
|
||||||
|
PathSegment,
|
||||||
|
Type,
|
||||||
|
Variant
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct ErrorVariantField
|
||||||
|
{
|
||||||
|
attrs : Vec<Attribute>,
|
||||||
|
ident : Ident,
|
||||||
|
ty : Type
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ErrorVariant
|
||||||
|
{
|
||||||
|
ident : Ident,
|
||||||
|
status : Option<Path>,
|
||||||
|
is_named : bool,
|
||||||
|
fields : Vec<ErrorVariantField>,
|
||||||
|
from_ty : Option<(usize, Type)>,
|
||||||
|
display : Option<LitStr>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_variant(variant : Variant) -> Result<ErrorVariant, Error>
|
||||||
|
{
|
||||||
|
let status = match variant.attrs.iter()
|
||||||
|
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string()))
|
||||||
|
{
|
||||||
|
Some(attr) => Some(parse_macro_input::parse::<Path>(remove_parens(attr.tokens.clone()).into())?),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut is_named = false;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
match variant.fields {
|
||||||
|
Fields::Named(named) => {
|
||||||
|
is_named = true;
|
||||||
|
for field in named.named
|
||||||
|
{
|
||||||
|
let span = field.span();
|
||||||
|
fields.push(ErrorVariantField {
|
||||||
|
attrs: field.attrs,
|
||||||
|
ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?,
|
||||||
|
ty: field.ty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Fields::Unnamed(unnamed) => {
|
||||||
|
for (i, field) in unnamed.unnamed.into_iter().enumerate()
|
||||||
|
{
|
||||||
|
fields.push(ErrorVariantField {
|
||||||
|
attrs: field.attrs,
|
||||||
|
ident: format_ident!("arg{}", i),
|
||||||
|
ty: field.ty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Fields::Unit => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let from_ty = fields.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string())))
|
||||||
|
.map(|(i, field)| (i, field.ty.clone()));
|
||||||
|
|
||||||
|
let display = match variant.attrs.iter()
|
||||||
|
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string()))
|
||||||
|
{
|
||||||
|
Some(attr) => Some(parse_macro_input::parse::<LitStr>(remove_parens(attr.tokens.clone()).into())?),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ErrorVariant {
|
||||||
|
ident: variant.ident,
|
||||||
|
status,
|
||||||
|
is_named,
|
||||||
|
fields,
|
||||||
|
from_ty,
|
||||||
|
display
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_segment(name : &str) -> PathSegment
|
||||||
|
{
|
||||||
|
PathSegment {
|
||||||
|
ident: format_ident!("{}", name),
|
||||||
|
arguments: Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
// TODO this is a really ugly regex that requires at least two characters between captures
|
||||||
|
static ref DISPLAY_REGEX : Regex = Regex::new(r"(^|[^\{])\{(?P<param>[^\}]+)\}([^\}]|$)").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorVariant
|
||||||
|
{
|
||||||
|
fn fields_pat(&self) -> TokenStream2
|
||||||
|
{
|
||||||
|
let mut fields = self.fields.iter().map(|field| &field.ident).peekable();
|
||||||
|
if fields.peek().is_none() {
|
||||||
|
quote!()
|
||||||
|
} else if self.is_named {
|
||||||
|
quote!( { #( #fields ),* } )
|
||||||
|
} else {
|
||||||
|
quote!( ( #( #fields ),* ) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result<TokenStream2, Error>
|
||||||
|
{
|
||||||
|
let ident = &self.ident;
|
||||||
|
let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?;
|
||||||
|
|
||||||
|
// lets find all required format parameters
|
||||||
|
let display_str = display.value();
|
||||||
|
let params = DISPLAY_REGEX.captures_iter(&display_str)
|
||||||
|
.map(|cap| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, cap.name("param").unwrap().as_str()));
|
||||||
|
|
||||||
|
let fields_pat = self.fields_pat();
|
||||||
|
Ok(quote! {
|
||||||
|
#enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_match_arm(self, krate : &TokenStream2, enum_ident : &Ident) -> TokenStream2
|
||||||
|
{
|
||||||
|
let ident = &self.ident;
|
||||||
|
let fields_pat = self.fields_pat();
|
||||||
|
let status = self.status.map(|status| {
|
||||||
|
// the status might be relative to StatusCode, so let's fix that
|
||||||
|
if status.leading_colon.is_none() && status.segments.len() < 2
|
||||||
|
{
|
||||||
|
let status_ident = status.segments.first().map(|path| path.clone()).unwrap_or_else(|| path_segment("OK"));
|
||||||
|
Path {
|
||||||
|
leading_colon: Some(Default::default()),
|
||||||
|
segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { status }
|
||||||
|
});
|
||||||
|
|
||||||
|
// the response will come directly from the from_ty if present
|
||||||
|
let res = match self.from_ty {
|
||||||
|
Some((from_index, _)) => {
|
||||||
|
let from_field = &self.fields[from_index].ident;
|
||||||
|
quote!(#from_field.into_response_error())
|
||||||
|
},
|
||||||
|
None => quote!(Ok(#krate::Response {
|
||||||
|
status: { #status }.into(),
|
||||||
|
body: #krate::gotham::hyper::Body::empty(),
|
||||||
|
mime: None
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#enum_ident::#ident #fields_pat => #res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn were(&self) -> Option<TokenStream2>
|
||||||
|
{
|
||||||
|
match self.from_ty.as_ref() {
|
||||||
|
Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )),
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand(tokens : TokenStream) -> Result<TokenStream2, Error>
|
||||||
|
{
|
||||||
|
let krate = super::krate();
|
||||||
|
let input = parse_macro_input::parse::<DeriveInput>(tokens)?;
|
||||||
|
let ident = input.ident;
|
||||||
|
let generics = input.generics;
|
||||||
|
|
||||||
|
let inum = match input.data {
|
||||||
|
Data::Enum(inum) => Ok(inum),
|
||||||
|
Data::Struct(strukt) => Err(strukt.struct_token.span()),
|
||||||
|
Data::Union(uni) => Err(uni.union_token.span())
|
||||||
|
}.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?;
|
||||||
|
let variants = inum.variants.into_iter()
|
||||||
|
.map(|variant| process_variant(variant))
|
||||||
|
.collect_to_result()?;
|
||||||
|
|
||||||
|
let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else {
|
||||||
|
let were = generics.params.iter().filter_map(|param| match param {
|
||||||
|
GenericParam::Type(ty) => {
|
||||||
|
let ident = &ty.ident;
|
||||||
|
Some(quote!(#ident : ::std::fmt::Display))
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
let formatter_ident = format_ident!("resource_error_display_formatter");
|
||||||
|
let match_arms = variants.iter()
|
||||||
|
.map(|v| v.to_display_match_arm(&formatter_ident, &ident))
|
||||||
|
.collect_to_result()?;
|
||||||
|
Some(quote! {
|
||||||
|
impl #generics ::std::fmt::Display for #ident #generics
|
||||||
|
where #( #were ),*
|
||||||
|
{
|
||||||
|
fn fmt(&self, #formatter_ident: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
#( #match_arms ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut from_impls : Vec<TokenStream2> = Vec::new();
|
||||||
|
|
||||||
|
for var in &variants
|
||||||
|
{
|
||||||
|
let var_ident = &var.ident;
|
||||||
|
let (from_index, from_ty) = match var.from_ty.as_ref() {
|
||||||
|
Some(f) => f,
|
||||||
|
None => continue
|
||||||
|
};
|
||||||
|
let from_ident = &var.fields[*from_index].ident;
|
||||||
|
|
||||||
|
let fields_pat = var.fields_pat();
|
||||||
|
let fields_where = var.fields.iter().enumerate()
|
||||||
|
.filter(|(i, _)| i != from_index)
|
||||||
|
.map(|(_, field)| {
|
||||||
|
let ty = &field.ty;
|
||||||
|
quote!( #ty : Default )
|
||||||
|
})
|
||||||
|
.chain(iter::once(quote!( #from_ty : ::std::error::Error )));
|
||||||
|
let fields_let = var.fields.iter().enumerate()
|
||||||
|
.filter(|(i, _)| i != from_index)
|
||||||
|
.map(|(_, field)| {
|
||||||
|
let id = &field.ident;
|
||||||
|
let ty = &field.ty;
|
||||||
|
quote!( let #id : #ty = Default::default(); )
|
||||||
|
});
|
||||||
|
|
||||||
|
from_impls.push(quote! {
|
||||||
|
impl #generics ::std::convert::From<#from_ty> for #ident #generics
|
||||||
|
where #( #fields_where ),*
|
||||||
|
{
|
||||||
|
fn from(#from_ident : #from_ty) -> Self
|
||||||
|
{
|
||||||
|
#( #fields_let )*
|
||||||
|
Self::#var_ident #fields_pat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>();
|
||||||
|
let variants = variants.into_iter().map(|variant| variant.into_match_arm(&krate, &ident));
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#display_impl
|
||||||
|
|
||||||
|
impl #generics #krate::IntoResponseError for #ident #generics
|
||||||
|
where #( #were ),*
|
||||||
|
{
|
||||||
|
type Err = #krate::export::serde_json::Error;
|
||||||
|
|
||||||
|
fn into_response_error(self) -> Result<#krate::Response, Self::Err>
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
#( #variants ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#( #from_impls )*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_resource_error(tokens : TokenStream) -> TokenStream
|
||||||
|
{
|
||||||
|
expand(tokens)
|
||||||
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
|
.into()
|
||||||
|
}
|
|
@ -1,3 +1,9 @@
|
||||||
|
use proc_macro2::{
|
||||||
|
Delimiter,
|
||||||
|
TokenStream as TokenStream2,
|
||||||
|
TokenTree
|
||||||
|
};
|
||||||
|
use std::iter;
|
||||||
use syn::Error;
|
use syn::Error;
|
||||||
|
|
||||||
pub trait CollectToResult
|
pub trait CollectToResult
|
||||||
|
@ -25,3 +31,19 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn remove_parens(input : TokenStream2) -> TokenStream2
|
||||||
|
{
|
||||||
|
let iter = input.into_iter().flat_map(|tt| {
|
||||||
|
if let TokenTree::Group(group) = &tt
|
||||||
|
{
|
||||||
|
if group.delimiter() == Delimiter::Parenthesis
|
||||||
|
{
|
||||||
|
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box::new(iter::once(tt))
|
||||||
|
});
|
||||||
|
iter.collect()
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue