1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-02-22 20:52:27 +00:00

Support File up/download

This commit is contained in:
msrd0 2019-10-20 14:49:53 +00:00
parent 25117a035f
commit d030fa539f
10 changed files with 659 additions and 110 deletions

View file

@ -22,7 +22,6 @@ gotham_restful = "0.0.1"
A basic server with only one resource, handling a simple `GET` request, could look like this:
```rust
#
/// Our RESTful Resource.
#[derive(Resource)]
#[rest_resource(read_all)]
@ -54,6 +53,23 @@ fn main() {
}
```
Uploads and Downloads can also be handled, but you need to specify the mime type manually:
```rust
#[derive(Resource)]
#[rest_resource(create)]
struct ImageResource;
#[derive(FromBody, RequestBody)]
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
struct RawImage(Vec<u8>);
#[rest_create(ImageResource)]
fn create(_state : &mut State, body : RawImage) -> Raw<Vec<u8>> {
Raw::new(body.0, mime::APPLICATION_OCTET_STREAM)
}
```
Look at the [example] for more methods and usage with the `openapi` feature.
## License

View file

@ -22,7 +22,6 @@ A basic server with only one resource, handling a simple `GET` request, could lo
# use gotham::{router::builder::*, state::State};
# use gotham_restful::{DrawResources, Resource, Success};
# use serde::{Deserialize, Serialize};
#
/// Our RESTful Resource.
#[derive(Resource)]
#[rest_resource(read_all)]
@ -55,6 +54,32 @@ fn main() {
}
```
Uploads and Downloads can also be handled, but you need to specify the mime type manually:
```rust,no_run
# #[macro_use] extern crate gotham_restful_derive;
# use gotham::{router::builder::*, state::State};
# use gotham_restful::{DrawResources, Raw, Resource, Success};
# use serde::{Deserialize, Serialize};
#[derive(Resource)]
#[rest_resource(create)]
struct ImageResource;
#[derive(FromBody, RequestBody)]
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
struct RawImage(Vec<u8>);
#[rest_create(ImageResource)]
fn create(_state : &mut State, body : RawImage) -> Raw<Vec<u8>> {
Raw::new(body.0, mime::APPLICATION_OCTET_STREAM)
}
# fn main() {
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
# route.resource::<ImageResource, _>("image");
# }));
# }
```
Look at the [example] for more methods and usage with the `openapi` feature.
# License
@ -76,7 +101,10 @@ extern crate self as gotham_restful;
#[macro_use] extern crate gotham_derive;
#[macro_use] extern crate serde;
pub use hyper::StatusCode;
#[doc(no_inline)]
pub use hyper::{Chunk, StatusCode};
#[doc(no_inline)]
pub use mime::Mime;
pub use gotham_restful_derive::*;
@ -114,7 +142,9 @@ pub use resource::{
mod result;
pub use result::{
NoContent,
Raw,
ResourceResult,
Response,
Success
};
@ -124,4 +154,4 @@ pub use routing::{DrawResources, DrawResourceRoutes};
pub use routing::WithOpenapi;
mod types;
pub use types::ResourceType;
pub use types::*;

View file

@ -4,6 +4,7 @@ use crate::{
routing::*,
OpenapiSchema,
OpenapiType,
RequestBody,
ResourceType
};
use futures::future::ok;
@ -18,10 +19,10 @@ use gotham::{
use hyper::Body;
use indexmap::IndexMap;
use log::error;
use mime::{APPLICATION_JSON, TEXT_PLAIN};
use mime::{Mime, APPLICATION_JSON, TEXT_PLAIN};
use openapiv3::{
Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody, Response, Responses, Schema,
Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema,
SchemaKind, Server, StatusCode, Type
};
use serde::de::DeserializeOwned;
@ -171,15 +172,18 @@ pub trait GetOpenapi
fn get_openapi(&mut self, path : &str);
}
fn schema_to_content(schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType>
fn schema_to_content(types : Vec<Mime>, schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType>
{
let mut content : IndexMap<String, MediaType> = IndexMap::new();
content.insert(APPLICATION_JSON.to_string(), MediaType {
schema: Some(schema),
example: None,
examples: IndexMap::new(),
encoding: IndexMap::new()
});
for ty in types
{
content.insert(ty.to_string(), MediaType {
schema: Some(schema.clone()),
example: None,
examples: IndexMap::new(),
encoding: IndexMap::new()
});
}
content
}
@ -265,12 +269,9 @@ impl<'a> OperationParams<'a>
}
}
fn new_operation(default_status : hyper::StatusCode, schema : ReferenceOr<Schema>, params : OperationParams, body_schema : Option<ReferenceOr<Schema>>) -> Operation
fn new_operation(default_status : hyper::StatusCode, accepted_types : Option<Vec<Mime>>, schema : ReferenceOr<Schema>, params : OperationParams, body_schema : Option<ReferenceOr<Schema>>, supported_types : Option<Vec<Mime>>) -> Operation
{
let content = match default_status.as_u16() {
204 => IndexMap::new(),
_ => schema_to_content(schema)
};
let content = schema_to_content(accepted_types.unwrap_or_default(), schema);
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response {
@ -280,9 +281,9 @@ fn new_operation(default_status : hyper::StatusCode, schema : ReferenceOr<Schema
links: IndexMap::new()
}));
let request_body = body_schema.map(|schema| Item(RequestBody {
let request_body = body_schema.map(|schema| Item(OARequestBody {
description: None,
content: schema_to_content(schema),
content: schema_to_content(supported_types.unwrap_or_default(), schema),
required: true
}));
@ -343,7 +344,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(Res::default_status(), schema, OperationParams::default(), None));
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>()
@ -359,7 +360,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(Res::default_status(), schema, OperationParams::from_path_params(vec!["id"]), None));
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>()
@ -367,7 +368,7 @@ macro_rules! implOpenapiRouter {
fn search<Handler, Query, Res>(&mut self)
where
Query : ResourceType + QueryStringExtractor<Body> + Send + Sync + 'static,
Query : ResourceType + DeserializeOwned + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>
{
@ -375,7 +376,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}/search", &self.1);
let mut item = (self.0).1.remove_path(&self.1);
item.get = Some(new_operation(Res::default_status(), schema, OperationParams::from_query_params(Query::schema()), None));
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_query_params(Query::schema()), None, None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).search::<Handler, Query, Res>()
@ -383,7 +384,7 @@ macro_rules! implOpenapiRouter {
fn create<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
@ -392,7 +393,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.post = Some(new_operation(Res::default_status(), schema, OperationParams::default(), Some(body_schema)));
item.post = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>()
@ -400,7 +401,7 @@ macro_rules! implOpenapiRouter {
fn update_all<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
@ -409,7 +410,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(Res::default_status(), schema, OperationParams::default(), Some(body_schema)));
item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>()
@ -418,7 +419,7 @@ macro_rules! implOpenapiRouter {
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
@ -427,7 +428,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(Res::default_status(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema)));
item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Body::supported_types()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>()
@ -442,7 +443,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(Res::default_status(), schema, OperationParams::default(), None));
item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>()
@ -458,7 +459,7 @@ macro_rules! implOpenapiRouter {
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(Res::default_status(), schema, OperationParams::from_path_params(vec!["id"]), None));
item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>()
@ -475,6 +476,7 @@ implOpenapiRouter!(ScopeBuilder);
#[cfg(test)]
mod test
{
use crate::ResourceResult;
use super::*;
#[derive(OpenapiType)]
@ -520,4 +522,24 @@ mod test
let json = serde_json::to_string(&params).unwrap();
assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name));
}
#[test]
fn no_content_schema_to_content()
{
let types = NoContent::accepted_types();
let schema = <NoContent as OpenapiType>::schema();
let content = schema_to_content(types.unwrap_or_default(), 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 = schema_to_content(types.unwrap_or_default(), 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"}}"#);
}
}

View file

@ -1,4 +1,4 @@
use crate::{DrawResourceRoutes, ResourceResult, ResourceType};
use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType};
use gotham::{
router::response::extender::StaticResponseExtender,
state::{State, StateData}
@ -35,25 +35,25 @@ where
/// Handle a GET request on the Resource with additional search parameters.
pub trait ResourceSearch<Query : ResourceType, R : ResourceResult>
where
Query : ResourceType + StateData + StaticResponseExtender
Query : ResourceType + DeserializeOwned + StateData + StaticResponseExtender
{
fn search(state : &mut State, query : Query) -> R;
}
/// Handle a POST request on the Resource root.
pub trait ResourceCreate<Body : ResourceType, R : ResourceResult>
pub trait ResourceCreate<Body : RequestBody, R : ResourceResult>
{
fn create(state : &mut State, body : Body) -> R;
}
/// Handle a PUT request on the Resource root.
pub trait ResourceUpdateAll<Body : ResourceType, R : ResourceResult>
pub trait ResourceUpdateAll<Body : RequestBody, R : ResourceResult>
{
fn update_all(state : &mut State, body : Body) -> R;
}
/// Handle a PUT request on the Resource with an id.
pub trait ResourceUpdate<ID, Body : ResourceType, R : ResourceResult>
pub trait ResourceUpdate<ID, Body : RequestBody, R : ResourceResult>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static
{

View file

@ -1,14 +1,76 @@
use crate::{ResourceType, StatusCode};
use crate::{ResponseBody, StatusCode};
#[cfg(feature = "openapi")]
use crate::{OpenapiSchema, OpenapiType};
use hyper::Body;
use mime::{Mime, APPLICATION_JSON, STAR_STAR};
#[cfg(feature = "openapi")]
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
use serde::Serialize;
use serde_json::error::Error as SerdeJsonError;
use std::error::Error;
/// 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
}
}
#[cfg(test)]
fn full_body(self) -> Vec<u8>
{
use futures::{future::Future, stream::Stream};
let bytes : &[u8] = &self.body.concat2().wait().unwrap().into_bytes();
bytes.to_vec()
}
}
/// A trait provided to convert a resource's result to json.
pub trait ResourceResult
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>;
/// Turn this into a response that can be returned to the browser. This api will likely
/// change in the future.
fn into_response(self) -> Result<Response, SerdeJsonError>;
/// Return a list of supported mime types.
fn accepted_types() -> Option<Vec<Mime>>
{
None
}
#[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema;
@ -48,19 +110,24 @@ impl<T : ToString> From<T> for ResourceError
}
}
impl<R : ResourceType, E : Error> ResourceResult for Result<R, E>
impl<R : ResponseBody, E : Error> ResourceResult for Result<R, E>
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>
fn into_response(self) -> Result<Response, SerdeJsonError>
{
Ok(match self {
Ok(r) => (StatusCode::OK, serde_json::to_string(r)?),
Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?),
Err(e) => {
let err : ResourceError = e.into();
(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)
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
{
@ -103,11 +170,16 @@ impl<T> From<T> for Success<T>
}
}
impl<T : ResourceType> ResourceResult for Success<T>
impl<T : ResponseBody> ResourceResult for Success<T>
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>
fn into_response(self) -> Result<Response, SerdeJsonError>
{
Ok((StatusCode::OK, serde_json::to_string(&self.0)?))
Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?))
}
fn accepted_types() -> Option<Vec<Mime>>
{
Some(vec![APPLICATION_JSON])
}
#[cfg(feature = "openapi")]
@ -150,9 +222,9 @@ impl From<()> for NoContent
impl ResourceResult for NoContent
{
/// This will always be a _204 No Content_ together with an empty string.
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>
fn into_response(self) -> Result<Response, SerdeJsonError>
{
Ok((Self::default_status(), "".to_string()))
Ok(Response::no_content())
}
/// Returns the schema of the `()` type.
@ -172,15 +244,15 @@ impl ResourceResult for NoContent
impl<E : Error> ResourceResult for Result<NoContent, E>
{
fn to_json(&self) -> Result<(StatusCode, String), SerdeJsonError>
fn into_response(self) -> Result<Response, SerdeJsonError>
{
Ok(match self {
Ok(_) => (Self::default_status(), "".to_string()),
match self {
Ok(nc) => nc.into_response(),
Err(e) => {
let err : ResourceError = e.into();
(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
}
})
}
}
#[cfg(feature = "openapi")]
@ -196,10 +268,76 @@ impl<E : Error> ResourceResult for Result<NoContent, E>
}
}
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 : Into<Body>> ResourceResult for Raw<T>
{
fn into_response(self) -> Result<Response, SerdeJsonError>
{
Ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone())))
}
fn accepted_types() -> Option<Vec<Mime>>
{
Some(vec![STAR_STAR])
}
#[cfg(feature = "openapi")]
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
pattern: None,
enumeration: Vec::new()
})))
}
}
impl<T, E : Error> ResourceResult for Result<Raw<T>, E>
where
Raw<T> : ResourceResult
{
fn into_response(self) -> Result<Response, SerdeJsonError>
{
match self {
Ok(raw) => raw.into_response(),
Err(e) => {
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 mime::TEXT_PLAIN;
use thiserror::Error;
#[derive(Debug, Default, Deserialize, Serialize)]
@ -217,44 +355,60 @@ mod test
fn resource_result_ok()
{
let ok : Result<Msg, MsgError> = Ok(Msg::default());
let (status, json) = ok.to_json().expect("didn't expect error response");
assert_eq!(status, StatusCode::OK);
assert_eq!(json, r#"{"msg":""}"#);
let res = 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(), r#"{"msg":""}"#.as_bytes());
}
#[test]
fn resource_result_err()
{
let err : Result<Msg, MsgError> = Err(MsgError::default());
let (status, json) = err.to_json().expect("didn't expect error response");
assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(json, format!(r#"{{"error":true,"message":"{}"}}"#, err.unwrap_err()));
let res = 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(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes());
}
#[test]
fn success_always_successfull()
{
let success : Success<Msg> = Msg::default().into();
let (status, json) = success.to_json().expect("didn't expect error response");
assert_eq!(status, StatusCode::OK);
assert_eq!(json, r#"{"msg":""}"#);
let res = 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(), r#"{"msg":""}"#.as_bytes());
}
#[test]
fn no_content_has_empty_json()
fn no_content_has_empty_response()
{
let no_content = NoContent::default();
let (status, json) = no_content.to_json().expect("didn't expect error response");
assert_eq!(status, StatusCode::NO_CONTENT);
assert_eq!(json, "");
let res = 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(), &[] as &[u8]);
}
#[test]
fn no_content_result()
{
let no_content = NoContent::default();
let res_def = no_content.to_json().expect("didn't expect error response");
let res_err = Result::<NoContent, MsgError>::Ok(no_content).to_json().expect("didn't expect error response");
assert_eq!(res_def, res_err);
let no_content : Result<NoContent, MsgError> = Ok(NoContent::default());
let res = 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(), &[] as &[u8]);
}
#[test]
fn raw_response()
{
let msg = "Test";
let raw = Raw::new(msg, TEXT_PLAIN);
let res = 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(), msg.as_bytes());
}
}

View file

@ -1,6 +1,7 @@
use crate::{
resource::*,
result::{ResourceError, ResourceResult},
result::{ResourceError, ResourceResult, Response},
RequestBody,
ResourceType,
StatusCode
};
@ -14,13 +15,26 @@ use futures::{
use gotham::{
extractor::QueryStringExtractor,
handler::{HandlerFuture, IntoHandlerError},
helpers::http::response::create_response,
helpers::http::response::{create_empty_response, create_response},
pipeline::chain::PipelineHandleChain,
router::builder::*,
router::{
builder::*,
non_match::RouteNonMatch,
route::matcher::{
content_type::ContentTypeHeaderRouteMatcher,
AcceptHeaderRouteMatcher,
RouteMatcher
}
},
state::{FromState, State}
};
use hyper::Body;
use mime::APPLICATION_JSON;
use hyper::{
header::CONTENT_TYPE,
Body,
HeaderMap,
Method
};
use mime::{Mime, APPLICATION_JSON};
use serde::de::DeserializeOwned;
use std::panic::RefUnwindSafe;
@ -69,26 +83,26 @@ pub trait DrawResourceRoutes
fn search<Handler, Query, Res>(&mut self)
where
Query : ResourceType + QueryStringExtractor<Body> + Send + Sync + 'static,
Query : ResourceType + DeserializeOwned + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>;
fn create<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>;
fn update_all<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>;
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>;
@ -104,16 +118,30 @@ pub trait DrawResourceRoutes
Handler : ResourceDelete<ID, Res>;
}
fn response_from(res : Response, state : &State) -> 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
}
fn to_handler_future<F, R>(mut state : State, get_result : F) -> Box<HandlerFuture>
where
F : FnOnce(&mut State) -> R,
R : ResourceResult
{
let res = get_result(&mut state).to_json();
let res = get_result(&mut state).into_response();
match res {
Ok((status, body)) => {
let res = create_response(&state, status, APPLICATION_JSON, body);
Box::new(ok((state, res)))
Ok(res) => {
let r = response_from(res, &state);
Box::new(ok((state, r)))
},
Err(e) => Box::new(err((state, e.into_handler_error())))
}
@ -121,7 +149,7 @@ where
fn handle_with_body<Body, F, R>(mut state : State, get_result : F) -> Box<HandlerFuture>
where
Body : DeserializeOwned,
Body : RequestBody,
F : FnOnce(&mut State, Body) -> R + Send + 'static,
R : ResourceResult
{
@ -133,8 +161,16 @@ where
Ok(body) => body,
Err(e) => return err((state, 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 ok((state, res))
}
};
let body = match serde_json::from_slice(&body) {
let body = match Body::from_body(body, content_type) {
Ok(body) => body,
Err(e) => return {
let error : ResourceError = e.into();
@ -148,11 +184,11 @@ where
}
};
let res = get_result(&mut state, body).to_json();
let res = get_result(&mut state, body).into_response();
match res {
Ok((status, body)) => {
let res = create_response(&state, status, APPLICATION_JSON, body);
ok((state, res))
Ok(res) => {
let r = response_from(res, &state);
ok((state, r))
},
Err(e) => err((state, e.into_handler_error()))
}
@ -195,7 +231,7 @@ where
fn create_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture>
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
@ -204,7 +240,7 @@ where
fn update_all_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture>
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
@ -214,7 +250,7 @@ where
fn update_handler<Handler, ID, Body, Res>(state : State) -> Box<HandlerFuture>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
@ -246,6 +282,60 @@ where
to_handler_future(state, |state| Handler::delete(state, id))
}
#[derive(Clone)]
struct MaybeMatchAcceptHeader
{
matcher : Option<AcceptHeaderRouteMatcher>
}
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
{
Self {
matcher: types.map(AcceptHeaderRouteMatcher::new)
}
}
}
#[derive(Clone)]
struct MaybeMatchContentTypeHeader
{
matcher : Option<ContentTypeHeaderRouteMatcher>
}
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(ContentTypeHeaderRouteMatcher::new)
}
}
}
macro_rules! implDrawResourceRoutes {
($implType:ident) => {
@ -289,7 +379,9 @@ macro_rules! implDrawResourceRoutes {
Res : ResourceResult,
Handler : ResourceReadAll<Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.get(&self.1)
.extend_route_matcher(matcher)
.to(|state| read_all_handler::<Handler, Res>(state));
}
@ -299,7 +391,9 @@ macro_rules! implDrawResourceRoutes {
Res : ResourceResult,
Handler : ResourceRead<ID, Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.get(&format!("{}/:id", self.1))
.extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<ID>>()
.to(|state| read_handler::<Handler, ID, Res>(state));
}
@ -310,39 +404,53 @@ macro_rules! implDrawResourceRoutes {
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.get(&format!("{}/search", self.1))
.extend_route_matcher(matcher)
.with_query_string_extractor::<Query>()
.to(|state| search_handler::<Handler, Query, Res>(state));
}
fn create<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into();
self.0.post(&self.1)
.extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher)
.to(|state| create_handler::<Handler, Body, Res>(state));
}
fn update_all<Handler, Body, Res>(&mut self)
where
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into();
self.0.put(&self.1)
.extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher)
.to(|state| update_all_handler::<Handler, Body, Res>(state));
}
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : ResourceType,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = 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<ID>>()
.to(|state| update_handler::<Handler, ID, Body, Res>(state));
}
@ -352,7 +460,9 @@ macro_rules! implDrawResourceRoutes {
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.delete(&self.1)
.extend_route_matcher(matcher)
.to(|state| delete_all_handler::<Handler, Res>(state));
}
@ -362,7 +472,9 @@ macro_rules! implDrawResourceRoutes {
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.delete(&format!("{}/:id", self.1))
.extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<ID>>()
.to(|state| delete_handler::<Handler, ID, Res>(state));
}

View file

@ -1,30 +1,79 @@
#[cfg(feature = "openapi")]
use crate::OpenapiType;
use crate::{OpenapiType, result::ResourceError};
use hyper::Chunk;
use mime::{Mime, APPLICATION_JSON};
use serde::{de::DeserializeOwned, Serialize};
/// A type that can be used inside a request or response body. Implemented for every type
/// that is serializable with serde, however, it is recommended to use the rest_struct!
/// macro to create one.
#[cfg(not(feature = "openapi"))]
pub trait ResourceType : DeserializeOwned + Serialize
pub trait ResourceType
{
}
#[cfg(not(feature = "openapi"))]
impl<T : DeserializeOwned + Serialize> ResourceType for T
{
}
/// A type that can be used inside a request or response body. Implemented for every type
/// that is serializable with serde, however, it is recommended to use the rest_struct!
/// macro to create one.
#[cfg(feature = "openapi")]
pub trait ResourceType : OpenapiType + DeserializeOwned + Serialize
impl<T> ResourceType for T
{
}
#[cfg(feature = "openapi")]
impl<T : OpenapiType + DeserializeOwned + Serialize> ResourceType for T
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 must be implemented by every type that can be used as a request body. It allows
/// to create the type from a hyper body chunk and it's content type.
pub trait FromBody : Sized
{
type Err : Into<ResourceError>;
/// Create the request body from a raw body and the content type.
fn from_body(body : Chunk, content_type : Mime) -> Result<Self, Self::Err>;
}
impl<T : DeserializeOwned> FromBody for T
{
type Err = serde_json::Error;
fn from_body(body : Chunk, _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`.
pub trait RequestBody : ResourceType + FromBody
{
/// Return all types that are supported as content types.
fn supported_types() -> Option<Vec<Mime>>
{
None
}
}
impl<T : ResourceType + DeserializeOwned> RequestBody for T
{
fn supported_types() -> Option<Vec<Mime>>
{
Some(vec![APPLICATION_JSON])
}
}

View file

@ -0,0 +1,59 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Fields,
ItemStruct,
parse_macro_input
};
pub fn expand_from_body(tokens : TokenStream) -> TokenStream
{
let krate = super::krate();
let input = parse_macro_input!(tokens as ItemStruct);
let ident = input.ident;
let generics = input.generics;
let (were, body) = match input.fields {
Fields::Named(named) => {
let fields = named.named;
match fields.len() {
0 => (quote!(), quote!(Self{})),
1 => {
let field = fields.first().unwrap();
let field_ident = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
(quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self { #field_ident: body.into() }))
},
_ => panic!("FromBody can only be derived for structs with at most one field")
}
},
Fields::Unnamed(unnamed) => {
let fields = unnamed.unnamed;
match fields.len() {
0 => (quote!(), quote!(Self{})),
1 => {
let field = fields.first().unwrap();
let field_ty = &field.ty;
(quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self(body.into())))
},
_ => panic!("FromBody can only be derived for structs with at most one field")
}
},
Fields::Unit => (quote!(), quote!(Self{}))
};
let output = quote! {
impl #generics #krate::FromBody for #ident #generics
#were
{
type Err = String;
fn from_body(body : #krate::Chunk, _content_type : #krate::Mime) -> Result<Self, Self::Err>
{
let body : &[u8] = &body;
Ok(#body)
}
}
};
output.into()
}

View file

@ -4,8 +4,12 @@ use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
mod from_body;
use from_body::expand_from_body;
mod method;
use method::{expand_method, Method};
mod request_body;
use request_body::expand_request_body;
mod resource;
use resource::expand_resource;
#[cfg(feature = "openapi")]
@ -16,6 +20,12 @@ fn krate() -> TokenStream2
quote!(::gotham_restful)
}
#[proc_macro_derive(FromBody)]
pub fn derive_from_body(tokens : TokenStream) -> TokenStream
{
expand_from_body(tokens)
}
#[cfg(feature = "openapi")]
#[proc_macro_derive(OpenapiType)]
pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream
@ -23,6 +33,12 @@ pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream
openapi_type::expand(tokens)
}
#[proc_macro_derive(RequestBody, attributes(supported_types))]
pub fn derive_request_body(tokens : TokenStream) -> TokenStream
{
expand_request_body(tokens)
}
#[proc_macro_derive(Resource, attributes(rest_resource))]
pub fn derive_resource(tokens : TokenStream) -> TokenStream
{

View file

@ -0,0 +1,91 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse::{Parse, ParseStream, Result as SynResult},
punctuated::Punctuated,
token::Comma,
Generics,
Ident,
ItemStruct,
Path,
parenthesized,
parse_macro_input
};
struct MimeList(Punctuated<Path, Comma>);
impl Parse for MimeList
{
fn parse(input: ParseStream) -> SynResult<Self>
{
let content;
let _paren = parenthesized!(content in input);
let list : Punctuated<Path, Comma> = Punctuated::parse_separated_nonempty(&content)?;
Ok(Self(list))
}
}
#[cfg(not(feature = "openapi"))]
fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream2
{
quote!()
}
#[cfg(feature = "openapi")]
fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2
{
let krate = super::krate();
quote! {
impl #generics #krate::OpenapiType for #ident #generics
{
fn schema() -> #krate::OpenapiSchema
{
use #krate::{export::openapi::*, OpenapiSchema};
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
pattern: None,
enumeration: Vec::new()
})))
}
}
}
}
pub fn expand_request_body(tokens : TokenStream) -> TokenStream
{
let krate = super::krate();
let input = parse_macro_input!(tokens as ItemStruct);
let ident = input.ident;
let generics = input.generics;
let types : Vec<Path> = input.attrs.into_iter().filter(|attr|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()) // TODO wtf
).flat_map(|attr| {
let m : MimeList = syn::parse2(attr.tokens).expect("unable to parse attributes");
m.0.into_iter()
}).collect();
let types = match types {
ref types if types.is_empty() => quote!(None),
types => quote!(Some(vec![#(#types),*]))
};
let impl_openapi_type = impl_openapi_type(&ident, &generics);
let output = quote! {
impl #generics #krate::RequestBody for #ident #generics
where #ident #generics : #krate::FromBody
{
fn supported_types() -> Option<Vec<#krate::Mime>>
{
#types
}
}
#impl_openapi_type
};
output.into()
}