mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 20:52:27 +00:00
Merge branch 'file-upload' into 'master'
Support File up/download See merge request msrd0/gotham-restful!2
This commit is contained in:
commit
d159363334
10 changed files with 659 additions and 110 deletions
18
README.md
18
README.md
|
@ -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
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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(¶ms).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"}}"#);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
59
gotham_restful_derive/src/from_body.rs
Normal file
59
gotham_restful_derive/src/from_body.rs
Normal 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()
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
91
gotham_restful_derive/src/request_body.rs
Normal file
91
gotham_restful_derive/src/request_body.rs
Normal 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()
|
||||
}
|
Loading…
Add table
Reference in a new issue