mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-05-09 08:00:41 +00:00
Replace methods with more flexible endpoints
This commit is contained in:
parent
0ac0f0f504
commit
b807ae2796
87 changed files with 1497 additions and 1512 deletions
|
@ -136,7 +136,7 @@ struct AuthData {
|
|||
exp: u64
|
||||
}
|
||||
|
||||
#[read_all(AuthResource)]
|
||||
#[read_all]
|
||||
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
||||
format!("{:?}", auth).into()
|
||||
}
|
||||
|
|
|
@ -246,7 +246,7 @@ where
|
|||
fn cors(&mut self, path: &str, method: Method);
|
||||
}
|
||||
|
||||
fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
||||
pub(crate) fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
||||
let config = CorsConfig::try_borrow_from(&state);
|
||||
|
||||
// prepare the response
|
||||
|
|
105
src/endpoint.rs
Normal file
105
src/endpoint.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use crate::{RequestBody, ResourceResult};
|
||||
use futures_util::future::BoxFuture;
|
||||
use gotham::{
|
||||
extractor::{PathExtractor, QueryStringExtractor},
|
||||
hyper::{Body, Method},
|
||||
state::State
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands.
|
||||
#[_private_openapi_trait(EndpointWithSchema)]
|
||||
pub trait Endpoint {
|
||||
/// The HTTP Verb of this endpoint.
|
||||
fn http_method() -> Method;
|
||||
/// The URI that this endpoint listens on in gotham's format.
|
||||
fn uri() -> Cow<'static, str>;
|
||||
|
||||
/// The output type that provides the response.
|
||||
type Output: ResourceResult + Send;
|
||||
|
||||
/// Returns `true` _iff_ the URI contains placeholders. `false` by default.
|
||||
fn has_placeholders() -> bool {
|
||||
false
|
||||
}
|
||||
/// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor]
|
||||
/// if `has_placeholders()` returns `false`.
|
||||
#[openapi_bound("Placeholders: crate::OpenapiType")]
|
||||
type Placeholders: PathExtractor<Body> + Sync;
|
||||
|
||||
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
|
||||
fn needs_params() -> bool {
|
||||
false
|
||||
}
|
||||
/// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor]
|
||||
/// if `needs_params()` returns `false`.
|
||||
#[openapi_bound("Params: crate::OpenapiType")]
|
||||
type Params: QueryStringExtractor<Body> + Sync;
|
||||
|
||||
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
|
||||
fn needs_body() -> bool {
|
||||
false
|
||||
}
|
||||
/// The type to parse the body into. Use `()` if `needs_body()` returns `false`.
|
||||
type Body: RequestBody + Send;
|
||||
|
||||
/// Returns `true` if the request wants to know the auth status of the client. `false` by default.
|
||||
fn wants_auth() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Replace the automatically generated operation id with a custom one. Only relevant for the
|
||||
/// OpenAPI Specification.
|
||||
#[openapi_only]
|
||||
fn operation_id() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The handler for this endpoint.
|
||||
fn handle(
|
||||
state: &mut State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: Option<Self::Body>
|
||||
) -> BoxFuture<'static, Self::Output>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<E: EndpointWithSchema> Endpoint for E {
|
||||
fn http_method() -> Method {
|
||||
E::http_method()
|
||||
}
|
||||
fn uri() -> Cow<'static, str> {
|
||||
E::uri()
|
||||
}
|
||||
|
||||
type Output = E::Output;
|
||||
|
||||
fn has_placeholders() -> bool {
|
||||
E::has_placeholders()
|
||||
}
|
||||
type Placeholders = E::Placeholders;
|
||||
|
||||
fn needs_params() -> bool {
|
||||
E::needs_params()
|
||||
}
|
||||
type Params = E::Params;
|
||||
|
||||
fn needs_body() -> bool {
|
||||
E::needs_body()
|
||||
}
|
||||
type Body = E::Body;
|
||||
|
||||
fn wants_auth() -> bool {
|
||||
E::wants_auth()
|
||||
}
|
||||
|
||||
fn handle(
|
||||
state: &mut State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: Option<Self::Body>
|
||||
) -> BoxFuture<'static, Self::Output> {
|
||||
E::handle(state, placeholders, params, body)
|
||||
}
|
||||
}
|
147
src/lib.rs
147
src/lib.rs
|
@ -9,7 +9,7 @@
|
|||
#![forbid(unsafe_code)]
|
||||
/*!
|
||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
||||
create resources with assigned methods that aim to be a more convenient way of creating handlers
|
||||
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
|
||||
for requests.
|
||||
|
||||
# Features
|
||||
|
@ -27,23 +27,23 @@ for requests.
|
|||
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
||||
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
|
||||
|
||||
# Methods
|
||||
# Endpoints
|
||||
|
||||
Assuming you assign `/foobar` to your resource, you can implement the following methods:
|
||||
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
|
||||
|
||||
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ----------- | ------------------ | --------- | ----------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
| create | body | POST | /foobar |
|
||||
| change_all | body | PUT | /foobar |
|
||||
| change | id, body | PUT | /foobar/:id |
|
||||
| remove_all | | DELETE | /foobar |
|
||||
| remove | id | DELETE | /foobar/:id |
|
||||
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ------------- | ------------------ | --------- | -------------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
| create | body | POST | /foobar |
|
||||
| change_all | body | PUT | /foobar |
|
||||
| change | id, body | PUT | /foobar/:id |
|
||||
| remove_all | | DELETE | /foobar |
|
||||
| remove | id | DELETE | /foobar/:id |
|
||||
|
||||
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
|
||||
simple example could look like this:
|
||||
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
|
||||
simple example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -55,15 +55,15 @@ simple example could look like this:
|
|||
#[resource(read)]
|
||||
struct FooResource;
|
||||
|
||||
/// The return type of the foo read method.
|
||||
/// The return type of the foo read endpoint.
|
||||
#[derive(Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
struct Foo {
|
||||
id: u64
|
||||
}
|
||||
|
||||
/// The foo read method handler.
|
||||
#[read(FooResource)]
|
||||
/// The foo read endpoint.
|
||||
#[read]
|
||||
fn read(id: u64) -> Success<Foo> {
|
||||
Foo { id }.into()
|
||||
}
|
||||
|
@ -76,17 +76,16 @@ fn read(id: u64) -> Success<Foo> {
|
|||
|
||||
# Arguments
|
||||
|
||||
Some methods require arguments. Those should be
|
||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
||||
Some endpoints require arguments. Those should be
|
||||
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
|
||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
||||
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
||||
type needs to implement [`QueryStringExtractor`].
|
||||
type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor).
|
||||
|
||||
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
|
||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
||||
`Sync` there is unfortunately no more convenient way.
|
||||
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
|
||||
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
|
||||
improve.
|
||||
|
||||
# Uploads and Downloads
|
||||
|
||||
|
@ -110,7 +109,7 @@ struct RawImage {
|
|||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(ImageResource)]
|
||||
#[create]
|
||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
|
@ -127,21 +126,23 @@ To make life easier for common use-cases, this create offers a few features that
|
|||
when you implement your web server. The complete feature list is
|
||||
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||
- `chrono` openapi support for chrono types
|
||||
- [`cors`](#cors-feature) CORS handling for all method handlers
|
||||
- `full` enables all features except `without-openapi`
|
||||
- [`cors`](#cors-feature) CORS handling for all endpoint handlers
|
||||
- [`database`](#database-feature) diesel middleware support
|
||||
- `errorlog` log errors returned from method handlers
|
||||
- `errorlog` log errors returned from endpoint handlers
|
||||
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
|
||||
- `uuid` openapi support for uuid
|
||||
- `without-openapi` (**default**) disables `openapi` support.
|
||||
|
||||
## Authentication Feature
|
||||
|
||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
||||
register a middleware that can automatically check for the existence of an JWT authentication
|
||||
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
|
||||
token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret
|
||||
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
||||
None of this is currently supported by gotham's own JWT middleware.
|
||||
|
||||
A simple example that uses only a single secret could look like this:
|
||||
A simple example that uses only a single secret looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -167,7 +168,7 @@ struct AuthData {
|
|||
exp: u64
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
#[read]
|
||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||
let intended_for = auth.ok()?.sub;
|
||||
Ok(Secret { id, intended_for })
|
||||
|
@ -194,7 +195,7 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a
|
|||
configuration as a middleware.
|
||||
|
||||
A simple example that allows authentication from every origin (note that `*` always disallows
|
||||
authentication), and every content type, could look like this:
|
||||
authentication), and every content type, looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -207,7 +208,7 @@ authentication), and every content type, could look like this:
|
|||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() {
|
||||
// your handler
|
||||
}
|
||||
|
@ -237,7 +238,7 @@ note however that due to the way gotham's diesel middleware implementation, it i
|
|||
to run async code while holding a database connection. If you need to combine async and database,
|
||||
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
|
||||
|
||||
A simple non-async example could look like this:
|
||||
A simple non-async example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate diesel;
|
||||
|
@ -267,7 +268,7 @@ struct Foo {
|
|||
value: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||
foo::table.load(conn)
|
||||
}
|
||||
|
@ -295,7 +296,7 @@ In order to automatically create an openapi specification, gotham-restful needs
|
|||
all routes and the types returned. `serde` does a great job at serialization but doesn't give
|
||||
enough type information, so all types used in the router need to implement `OpenapiType`. This
|
||||
can be derived for almoust any type and there should be no need to implement it manually. A simple
|
||||
example could look like this:
|
||||
example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -313,7 +314,7 @@ struct Foo {
|
|||
bar: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Success<Foo> {
|
||||
Foo { bar: "Hello World".to_owned() }.into()
|
||||
}
|
||||
|
@ -334,48 +335,37 @@ fn main() {
|
|||
# }
|
||||
```
|
||||
|
||||
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`
|
||||
that will return the generated openapi specification. This allows you to easily write clients
|
||||
in different languages without worying to exactly replicate your api in each of those languages.
|
||||
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`.
|
||||
It will return the generated openapi specification in JSON format. This allows you to easily write
|
||||
clients in different languages without worying to exactly replicate your api in each of those
|
||||
languages.
|
||||
|
||||
However, as of right now there is one caveat. If you wrote code before enabling the openapi feature,
|
||||
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
|
||||
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
|
||||
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
|
||||
In other words, put
|
||||
However, please note that by default, the `without-openapi` feature of this crate is enabled.
|
||||
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
|
||||
on some of the types in [`Endpoint`] and related traits. This means that some code might only
|
||||
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
|
||||
it is strongly recommended to pass both features through and conditionally enable the openapi
|
||||
code, like this:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
openapi = ["gotham-restful/openapi"]
|
||||
```
|
||||
|
||||
into your libraries `Cargo.toml` and use the following for all types used with handlers:
|
||||
|
||||
```
|
||||
# #[cfg(feature = "openapi")]
|
||||
# mod openapi_feature_enabled {
|
||||
# use gotham_restful::OpenapiType;
|
||||
```rust
|
||||
# #[macro_use] extern crate gotham_restful;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
struct Foo;
|
||||
# }
|
||||
```
|
||||
|
||||
# Examples
|
||||
|
||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
||||
This readme and the crate documentation contain some of example. In addition to that, there is
|
||||
a collection of code in the [example] directory that might help you. Any help writing more
|
||||
examples is highly appreciated.
|
||||
|
||||
|
||||
[diesel]: https://diesel.rs/
|
||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||
[gotham]: https://gotham.rs/
|
||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
||||
[`CorsRoute`]: trait.CorsRoute.html
|
||||
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
|
||||
[`RequestBody`]: trait.RequestBody.html
|
||||
[`State`]: ../gotham/state/struct.State.html
|
||||
*/
|
||||
|
||||
#[cfg(all(feature = "openapi", feature = "without-openapi"))]
|
||||
|
@ -390,6 +380,8 @@ extern crate self as gotham_restful;
|
|||
#[macro_use]
|
||||
extern crate gotham_derive;
|
||||
#[macro_use]
|
||||
extern crate gotham_restful_derive;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
@ -409,7 +401,9 @@ pub use gotham_restful_derive::*;
|
|||
/// Not public API
|
||||
#[doc(hidden)]
|
||||
pub mod export {
|
||||
pub use futures_util::future::FutureExt;
|
||||
pub use crate::routing::PathExtractor as IdPlaceholder;
|
||||
|
||||
pub use futures_util::future::{BoxFuture, FutureExt};
|
||||
|
||||
pub use serde_json;
|
||||
|
||||
|
@ -441,11 +435,10 @@ pub use openapi::{
|
|||
types::{OpenapiSchema, OpenapiType}
|
||||
};
|
||||
|
||||
mod resource;
|
||||
pub use resource::{
|
||||
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll,
|
||||
ResourceRemove, ResourceRemoveAll, ResourceSearch
|
||||
};
|
||||
mod endpoint;
|
||||
pub use endpoint::Endpoint;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use endpoint::EndpointWithSchema;
|
||||
|
||||
mod response;
|
||||
pub use response::Response;
|
||||
|
@ -457,9 +450,21 @@ pub use result::{
|
|||
};
|
||||
|
||||
mod routing;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use routing::WithOpenapi;
|
||||
pub use routing::{DrawResourceRoutes, DrawResources};
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi};
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
/// This trait must be implemented for every resource. It allows you to register the different
|
||||
/// endpoints that can be handled by this resource to be registered with the underlying router.
|
||||
///
|
||||
/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`.
|
||||
#[_private_openapi_trait(ResourceWithSchema)]
|
||||
pub trait Resource {
|
||||
/// Register all methods handled by this resource with the underlying router.
|
||||
#[openapi_bound("D: crate::DrawResourceRoutesWithSchema")]
|
||||
#[non_openapi_bound("D: crate::DrawResourceRoutes")]
|
||||
fn setup<D>(route: D);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::SECURITY_NAME;
|
||||
use crate::{resource::*, result::*, OpenapiSchema, RequestBody};
|
||||
use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody};
|
||||
use indexmap::IndexMap;
|
||||
use mime::Mime;
|
||||
use openapiv3::{
|
||||
|
@ -8,31 +8,40 @@ use openapiv3::{
|
|||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct OperationParams<'a> {
|
||||
path_params: Vec<(&'a str, ReferenceOr<Schema>)>,
|
||||
struct OperationParams {
|
||||
path_params: Option<OpenapiSchema>,
|
||||
query_params: Option<OpenapiSchema>
|
||||
}
|
||||
|
||||
impl<'a> OperationParams<'a> {
|
||||
fn add_path_params(&self, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
for param in &self.path_params {
|
||||
impl OperationParams {
|
||||
fn add_path_params(path_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
let path_params = match path_params {
|
||||
Some(pp) => pp.schema,
|
||||
None => return
|
||||
};
|
||||
let path_params = match path_params {
|
||||
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||
_ => panic!("Path Parameters needs to be a plain struct")
|
||||
};
|
||||
for (name, schema) in path_params.properties {
|
||||
let required = path_params.required.contains(&name);
|
||||
params.push(Item(Parameter::Path {
|
||||
parameter_data: ParameterData {
|
||||
name: (*param).0.to_string(),
|
||||
name,
|
||||
description: None,
|
||||
required: true,
|
||||
required,
|
||||
deprecated: None,
|
||||
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
|
||||
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||
example: None,
|
||||
examples: IndexMap::new()
|
||||
},
|
||||
style: Default::default()
|
||||
}));
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn add_query_params(self, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
let query_params = match self.query_params {
|
||||
fn add_query_params(query_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
let query_params = match query_params {
|
||||
Some(qp) => qp.schema,
|
||||
None => return
|
||||
};
|
||||
|
@ -61,51 +70,48 @@ impl<'a> OperationParams<'a> {
|
|||
|
||||
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
|
||||
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
|
||||
self.add_path_params(&mut params);
|
||||
self.add_query_params(&mut params);
|
||||
Self::add_path_params(self.path_params, &mut params);
|
||||
Self::add_query_params(self.query_params, &mut params);
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OperationDescription<'a> {
|
||||
pub struct OperationDescription {
|
||||
operation_id: Option<String>,
|
||||
default_status: crate::StatusCode,
|
||||
accepted_types: Option<Vec<Mime>>,
|
||||
schema: ReferenceOr<Schema>,
|
||||
params: OperationParams<'a>,
|
||||
params: OperationParams,
|
||||
body_schema: Option<ReferenceOr<Schema>>,
|
||||
supported_types: Option<Vec<Mime>>,
|
||||
requires_auth: bool
|
||||
}
|
||||
|
||||
impl<'a> OperationDescription<'a> {
|
||||
pub fn new<Handler: ResourceMethod>(schema: ReferenceOr<Schema>) -> Self {
|
||||
impl OperationDescription {
|
||||
pub fn new<E: EndpointWithSchema>(schema: ReferenceOr<Schema>) -> Self {
|
||||
Self {
|
||||
operation_id: Handler::operation_id(),
|
||||
default_status: Handler::Res::default_status(),
|
||||
accepted_types: Handler::Res::accepted_types(),
|
||||
operation_id: E::operation_id(),
|
||||
default_status: E::Output::default_status(),
|
||||
accepted_types: E::Output::accepted_types(),
|
||||
schema,
|
||||
params: Default::default(),
|
||||
body_schema: None,
|
||||
supported_types: None,
|
||||
requires_auth: Handler::wants_auth()
|
||||
requires_auth: E::wants_auth()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path_param(mut self, name: &'a str, schema: ReferenceOr<Schema>) -> Self {
|
||||
self.params.path_params.push((name, schema));
|
||||
self
|
||||
pub fn set_path_params(&mut self, params: OpenapiSchema) {
|
||||
self.params.path_params = Some(params);
|
||||
}
|
||||
|
||||
pub fn with_query_params(mut self, params: OpenapiSchema) -> Self {
|
||||
pub fn set_query_params(&mut self, params: OpenapiSchema) {
|
||||
self.params.query_params = Some(params);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_body<Body: RequestBody>(mut self, schema: ReferenceOr<Schema>) -> Self {
|
||||
pub fn set_body<Body: RequestBody>(&mut self, schema: ReferenceOr<Schema>) {
|
||||
self.body_schema = Some(schema);
|
||||
self.supported_types = Body::supported_types();
|
||||
self
|
||||
}
|
||||
|
||||
fn schema_to_content(types: Vec<Mime>, schema: ReferenceOr<Schema>) -> IndexMap<String, MediaType> {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
|
||||
use crate::{resource::*, routing::*, OpenapiType};
|
||||
use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||
use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema};
|
||||
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use std::panic::RefUnwindSafe;
|
||||
|
||||
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
|
||||
|
@ -51,150 +53,62 @@ macro_rules! implOpenapiRouter {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
where
|
||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P: RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn resource<R: Resource>(&mut self, path: &str) {
|
||||
fn resource<R: ResourceWithSchema>(&mut self, path: &str) {
|
||||
R::setup((self, path));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||
impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||
where
|
||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P: RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn read_all<Handler: ResourceReadAll>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<E::Output>();
|
||||
let mut descr = OperationDescription::new::<E>(schema);
|
||||
if E::has_placeholders() {
|
||||
descr.set_path_params(E::Placeholders::schema());
|
||||
}
|
||||
if E::needs_params() {
|
||||
descr.set_query_params(E::Params::schema());
|
||||
}
|
||||
if E::needs_body() {
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<E::Body>();
|
||||
descr.set_body::<E::Body>(body_schema);
|
||||
}
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
static URI_PLACEHOLDER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(^|/):(?P<name>[^/]+)(/|$)"#).unwrap());
|
||||
let uri: &str = &E::uri();
|
||||
let uri =
|
||||
URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| format!("{{{}}}", &captures["name"]));
|
||||
let path = if uri.is_empty() {
|
||||
format!("{}/{}", self.0.scope.unwrap_or_default(), self.1)
|
||||
} else {
|
||||
format!("{}/{}/{}", self.0.scope.unwrap_or_default(), self.1, uri)
|
||||
};
|
||||
|
||||
let op = descr.into_operation();
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
match E::http_method() {
|
||||
Method::GET => item.get = Some(op),
|
||||
Method::PUT => item.put = Some(op),
|
||||
Method::POST => item.post = Some(op),
|
||||
Method::DELETE => item.delete = Some(op),
|
||||
Method::OPTIONS => item.options = Some(op),
|
||||
Method::HEAD => item.head = Some(op),
|
||||
Method::PATCH => item.patch = Some(op),
|
||||
Method::TRACE => item.trace = Some(op),
|
||||
method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method)
|
||||
};
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read_all::<Handler>()
|
||||
}
|
||||
|
||||
fn read<Handler: ResourceRead>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.add_path_param("id", id_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read::<Handler>()
|
||||
}
|
||||
|
||||
fn search<Handler: ResourceSearch>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.with_query_params(Handler::Query::schema())
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).search::<Handler>()
|
||||
}
|
||||
|
||||
fn create<Handler: ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.post = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.with_body::<Handler::Body>(body_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).create::<Handler>()
|
||||
}
|
||||
|
||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.put = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.with_body::<Handler::Body>(body_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).change_all::<Handler>()
|
||||
}
|
||||
|
||||
fn change<Handler: ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.put = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.add_path_param("id", id_schema)
|
||||
.with_body::<Handler::Body>(body_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).change::<Handler>()
|
||||
}
|
||||
|
||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).remove_all::<Handler>()
|
||||
}
|
||||
|
||||
fn remove<Handler: ResourceRemove>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.delete = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.add_path_param("id", id_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).remove::<Handler>()
|
||||
(&mut *(self.0).router, self.1).endpoint::<E>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#[cfg(feature = "chrono")]
|
||||
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
||||
use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor};
|
||||
use indexmap::IndexMap;
|
||||
use openapiv3::{
|
||||
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType,
|
||||
|
@ -86,6 +87,20 @@ impl OpenapiType for () {
|
|||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for NoopPathExtractor {
|
||||
fn schema() -> OpenapiSchema {
|
||||
warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopPathExtractor. This is probably not what you want.");
|
||||
<()>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for NoopQueryStringExtractor {
|
||||
fn schema() -> OpenapiSchema {
|
||||
warn!("You're asking for the OpenAPI Schema for gotham::extractor::NoopQueryStringExtractor. This is probably not what you want.");
|
||||
<()>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for bool {
|
||||
fn schema() -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType};
|
||||
use gotham::{extractor::QueryStringExtractor, hyper::Body, state::State};
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
/// This trait must be implemented for every resource. It allows you to register the different
|
||||
/// methods that can be handled by this resource to be registered with the underlying router.
|
||||
///
|
||||
/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`.
|
||||
pub trait Resource {
|
||||
/// Register all methods handled by this resource with the underlying router.
|
||||
fn setup<D: DrawResourceRoutes>(route: D);
|
||||
}
|
||||
|
||||
/// A common trait for every resource method. It defines the return type as well as some general
|
||||
/// information about a resource method.
|
||||
///
|
||||
/// It is not recommended to implement this yourself. Rather, just write your handler method and
|
||||
/// annotate it with `#[<method>(YourResource)]`, where `<method>` is one of the supported
|
||||
/// resource methods.
|
||||
pub trait ResourceMethod {
|
||||
type Res: ResourceResult + Send + 'static;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn operation_id() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn wants_auth() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The read_all [ResourceMethod].
|
||||
pub trait ResourceReadAll: ResourceMethod {
|
||||
/// Handle a GET request on the Resource root.
|
||||
fn read_all(state: State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The read [ResourceMethod].
|
||||
pub trait ResourceRead: ResourceMethod {
|
||||
/// The ID type to be parsed from the request path.
|
||||
type ID: ResourceID + 'static;
|
||||
|
||||
/// Handle a GET request on the Resource with an id.
|
||||
fn read(state: State, id: Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The search [ResourceMethod].
|
||||
pub trait ResourceSearch: ResourceMethod {
|
||||
/// The Query type to be parsed from the request parameters.
|
||||
type Query: ResourceType + QueryStringExtractor<Body> + Sync;
|
||||
|
||||
/// Handle a GET request on the Resource with additional search parameters.
|
||||
fn search(state: State, query: Self::Query) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The create [ResourceMethod].
|
||||
pub trait ResourceCreate: ResourceMethod {
|
||||
/// The Body type to be parsed from the request body.
|
||||
type Body: RequestBody;
|
||||
|
||||
/// Handle a POST request on the Resource root.
|
||||
fn create(state: State, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The change_all [ResourceMethod].
|
||||
pub trait ResourceChangeAll: ResourceMethod {
|
||||
/// The Body type to be parsed from the request body.
|
||||
type Body: RequestBody;
|
||||
|
||||
/// Handle a PUT request on the Resource root.
|
||||
fn change_all(state: State, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The change [ResourceMethod].
|
||||
pub trait ResourceChange: ResourceMethod {
|
||||
/// The Body type to be parsed from the request body.
|
||||
type Body: RequestBody;
|
||||
/// The ID type to be parsed from the request path.
|
||||
type ID: ResourceID + 'static;
|
||||
|
||||
/// Handle a PUT request on the Resource with an id.
|
||||
fn change(state: State, id: Self::ID, body: Self::Body) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The remove_all [ResourceMethod].
|
||||
pub trait ResourceRemoveAll: ResourceMethod {
|
||||
/// Handle a DELETE request on the Resource root.
|
||||
fn remove_all(state: State) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
||||
|
||||
/// The remove [ResourceMethod].
|
||||
pub trait ResourceRemove: ResourceMethod {
|
||||
/// The ID type to be parsed from the request path.
|
||||
type ID: ResourceID + 'static;
|
||||
|
||||
/// Handle a DELETE request on the Resource with an id.
|
||||
fn remove(state: State, id: Self::ID) -> Pin<Box<dyn Future<Output = (State, Self::Res)> + Send>>;
|
||||
}
|
|
@ -33,7 +33,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
|
|||
# #[derive(Clone, Deserialize)]
|
||||
# struct MyAuthData { exp : u64 }
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
#[read_all]
|
||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
||||
let auth_data = match auth {
|
||||
AuthStatus::Authenticated(data) => data,
|
||||
|
@ -102,7 +102,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
|
|||
# #[derive(Clone, Deserialize)]
|
||||
# struct MyAuthData { exp : u64 }
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
#[read_all]
|
||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
||||
let auth_data = match auth {
|
||||
AuthStatus::Authenticated(data) => data,
|
||||
|
|
|
@ -21,8 +21,8 @@ the function attributes:
|
|||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(_state: &mut State) {
|
||||
#[read_all]
|
||||
fn read_all() {
|
||||
// do something
|
||||
}
|
||||
# }
|
||||
|
|
|
@ -25,7 +25,7 @@ example that simply returns its body:
|
|||
#[resource(create)]
|
||||
struct ImageResource;
|
||||
|
||||
#[create(ImageResource)]
|
||||
#[create]
|
||||
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||
body
|
||||
}
|
||||
|
|
|
@ -34,8 +34,8 @@ struct MyResponse {
|
|||
message: &'static str
|
||||
}
|
||||
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
||||
#[read_all]
|
||||
fn read_all() -> Success<MyResponse> {
|
||||
let res = MyResponse { message: "I'm always happy" };
|
||||
res.into()
|
||||
}
|
||||
|
|
358
src/routing.rs
358
src/routing.rs
|
@ -3,37 +3,33 @@ use crate::openapi::{
|
|||
builder::{OpenapiBuilder, OpenapiInfo},
|
||||
router::OpenapiRouter
|
||||
};
|
||||
#[cfg(feature = "cors")]
|
||||
use crate::CorsRoute;
|
||||
use crate::{
|
||||
resource::{
|
||||
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceRead, ResourceReadAll, ResourceRemove,
|
||||
ResourceRemoveAll, ResourceSearch
|
||||
},
|
||||
result::{ResourceError, ResourceResult},
|
||||
RequestBody, Response, StatusCode
|
||||
Endpoint, FromBody, Resource, Response, StatusCode
|
||||
};
|
||||
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::{
|
||||
handler::{HandlerError, HandlerFuture},
|
||||
handler::HandlerError,
|
||||
helpers::http::response::{create_empty_response, create_response},
|
||||
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method},
|
||||
pipeline::chain::PipelineHandleChain,
|
||||
router::{
|
||||
builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher, RouterBuilder, ScopeBuilder},
|
||||
builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder},
|
||||
non_match::RouteNonMatch,
|
||||
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
|
||||
route::matcher::{
|
||||
AcceptHeaderRouteMatcher, AccessControlRequestMethodMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher
|
||||
}
|
||||
},
|
||||
state::{FromState, State}
|
||||
};
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{future::Future, panic::RefUnwindSafe, pin::Pin};
|
||||
use std::panic::RefUnwindSafe;
|
||||
|
||||
/// Allow us to extract an id from a path.
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||
id: ID
|
||||
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||
pub id: ID
|
||||
}
|
||||
|
||||
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
|
||||
|
@ -48,37 +44,20 @@ pub trait WithOpenapi<D> {
|
|||
|
||||
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
||||
/// any RESTful [Resource] with a path.
|
||||
#[_private_openapi_trait(DrawResourcesWithSchema)]
|
||||
pub trait DrawResources {
|
||||
fn resource<R: Resource>(&mut self, path: &str);
|
||||
#[openapi_bound("R: crate::ResourceWithSchema")]
|
||||
#[non_openapi_bound("R: crate::Resource")]
|
||||
fn resource<R>(&mut self, path: &str);
|
||||
}
|
||||
|
||||
/// This trait allows to draw routes within an resource. Use this only inside the
|
||||
/// [Resource::setup] method.
|
||||
#[_private_openapi_trait(DrawResourceRoutesWithSchema)]
|
||||
pub trait DrawResourceRoutes {
|
||||
fn read_all<Handler: ResourceReadAll>(&mut self);
|
||||
|
||||
fn read<Handler: ResourceRead>(&mut self);
|
||||
|
||||
fn search<Handler: ResourceSearch>(&mut self);
|
||||
|
||||
fn create<Handler: ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static;
|
||||
|
||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static;
|
||||
|
||||
fn change<Handler: ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static;
|
||||
|
||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self);
|
||||
|
||||
fn remove<Handler: ResourceRemove>(&mut self);
|
||||
#[openapi_bound("E: crate::EndpointWithSchema")]
|
||||
#[non_openapi_bound("E: crate::Endpoint")]
|
||||
fn endpoint<E: 'static>(&mut self);
|
||||
}
|
||||
|
||||
fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
|
||||
|
@ -108,149 +87,42 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body>
|
|||
r
|
||||
}
|
||||
|
||||
async fn to_handler_future<F, R>(
|
||||
state: State,
|
||||
get_result: F
|
||||
) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)>
|
||||
where
|
||||
F: FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
||||
R: ResourceResult
|
||||
{
|
||||
let (state, res) = get_result(state).await;
|
||||
let res = res.into_response().await;
|
||||
match res {
|
||||
Ok(res) => {
|
||||
let r = response_from(res, &state);
|
||||
Ok((state, r))
|
||||
},
|
||||
Err(e) => Err((state, e.into()))
|
||||
}
|
||||
}
|
||||
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError> {
|
||||
trace!("entering endpoint_handler");
|
||||
let placeholders = E::Placeholders::take_from(state);
|
||||
let params = E::Params::take_from(state);
|
||||
|
||||
async fn body_to_res<B, F, R>(
|
||||
mut state: State,
|
||||
get_result: F
|
||||
) -> (State, Result<gotham::hyper::Response<Body>, HandlerError>)
|
||||
where
|
||||
B: RequestBody,
|
||||
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
||||
R: ResourceResult
|
||||
{
|
||||
let body = to_bytes(Body::take_from(&mut state)).await;
|
||||
let body = match E::needs_body() {
|
||||
true => {
|
||||
let body = to_bytes(Body::take_from(state)).await?;
|
||||
|
||||
let body = match body {
|
||||
Ok(body) => body,
|
||||
Err(e) => return (state, Err(e.into()))
|
||||
};
|
||||
let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) {
|
||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||
None => {
|
||||
debug!("Missing Content-Type: Returning 415 Response");
|
||||
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
|
||||
let content_type: Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
|
||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||
None => {
|
||||
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
return (state, Ok(res));
|
||||
}
|
||||
};
|
||||
|
||||
let res = {
|
||||
let body = match B::from_body(body, content_type) {
|
||||
Ok(body) => body,
|
||||
Err(e) => {
|
||||
let error: ResourceError = e.into();
|
||||
let res = match serde_json::to_string(&error) {
|
||||
Ok(json) => {
|
||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
Ok(res)
|
||||
},
|
||||
Err(e) => Err(e.into())
|
||||
};
|
||||
return (state, res);
|
||||
match E::Body::from_body(body, content_type) {
|
||||
Ok(body) => Some(body),
|
||||
Err(e) => {
|
||||
debug!("Invalid Body: Returning 400 Response");
|
||||
let error: ResourceError = e.into();
|
||||
let json = serde_json::to_string(&error)?;
|
||||
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
};
|
||||
get_result(state, body)
|
||||
};
|
||||
|
||||
let (state, res) = res.await;
|
||||
let res = res.into_response().await;
|
||||
|
||||
let res = match res {
|
||||
Ok(res) => {
|
||||
let r = response_from(res, &state);
|
||||
Ok(r)
|
||||
},
|
||||
Err(e) => Err(e.into())
|
||||
false => None
|
||||
};
|
||||
(state, res)
|
||||
}
|
||||
|
||||
fn handle_with_body<B, F, R>(state: State, get_result: F) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
B: RequestBody + 'static,
|
||||
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
|
||||
R: ResourceResult + Send + 'static
|
||||
{
|
||||
body_to_res(state, get_result)
|
||||
.then(|(state, res)| match res {
|
||||
Ok(ok) => future::ok((state, ok)),
|
||||
Err(err) => future::err((state, err))
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn read_all_handler<Handler: ResourceReadAll>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
to_handler_future(state, |state| Handler::read_all(state)).boxed()
|
||||
}
|
||||
|
||||
fn read_handler<Handler: ResourceRead>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let id = {
|
||||
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
to_handler_future(state, |state| Handler::read(state, id)).boxed()
|
||||
}
|
||||
|
||||
fn search_handler<Handler: ResourceSearch>(mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let query = Handler::Query::take_from(&mut state);
|
||||
to_handler_future(state, |state| Handler::search(state, query)).boxed()
|
||||
}
|
||||
|
||||
fn create_handler<Handler: ResourceCreate>(state: State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
|
||||
}
|
||||
|
||||
fn change_all_handler<Handler: ResourceChangeAll>(state: State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body))
|
||||
}
|
||||
|
||||
fn change_handler<Handler: ResourceChange>(state: State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let id = {
|
||||
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body))
|
||||
}
|
||||
|
||||
fn remove_all_handler<Handler: ResourceRemoveAll>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
to_handler_future(state, |state| Handler::remove_all(state)).boxed()
|
||||
}
|
||||
|
||||
fn remove_handler<Handler: ResourceRemove>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let id = {
|
||||
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
to_handler_future(state, |state| Handler::remove(state, id)).boxed()
|
||||
let out = E::handle(state, placeholders, params, body).await;
|
||||
let res = out.into_response().await?;
|
||||
debug!("Returning response {:?}", res);
|
||||
Ok(response_from(res, state))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -267,8 +139,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
impl MaybeMatchAcceptHeader {
|
||||
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||
let types = match types {
|
||||
Some(types) if types.is_empty() => None,
|
||||
types => types
|
||||
|
@ -279,6 +151,12 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
Self::new(types)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MaybeMatchContentTypeHeader {
|
||||
matcher: Option<ContentTypeHeaderRouteMatcher>
|
||||
|
@ -293,14 +171,20 @@ impl RouteMatcher for MaybeMatchContentTypeHeader {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
impl MaybeMatchContentTypeHeader {
|
||||
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||
Self {
|
||||
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
Self::new(types)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! implDrawResourceRoutes {
|
||||
($implType:ident) => {
|
||||
#[cfg(feature = "openapi")]
|
||||
|
@ -332,108 +216,30 @@ macro_rules! implDrawResourceRoutes {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters
|
||||
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
||||
where
|
||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P: RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn read_all<Handler: ResourceReadAll>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.get(&self.1)
|
||||
.extend_route_matcher(matcher)
|
||||
.to(|state| read_all_handler::<Handler>(state));
|
||||
}
|
||||
fn endpoint<E: Endpoint + 'static>(&mut self) {
|
||||
let uri = format!("{}/{}", self.1, E::uri());
|
||||
debug!("Registering endpoint for {}", uri);
|
||||
self.0.associate(&uri, |assoc| {
|
||||
assoc
|
||||
.request(vec![E::http_method()])
|
||||
.add_route_matcher(MaybeMatchAcceptHeader::new(E::Output::accepted_types()))
|
||||
.with_path_extractor::<E::Placeholders>()
|
||||
.with_query_string_extractor::<E::Params>()
|
||||
.to_async_borrowing(endpoint_handler::<E>);
|
||||
|
||||
fn read<Handler: ResourceRead>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.get(&format!("{}/:id", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| read_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn search<Handler: ResourceSearch>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.get(&format!("{}/search", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_query_string_extractor::<Handler::Query>()
|
||||
.to(|state| search_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn create<Handler: ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res: Send + 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0
|
||||
.post(&self.1)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.to(|state| create_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&self.1, Method::POST);
|
||||
}
|
||||
|
||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res: Send + 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0
|
||||
.put(&self.1)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.to(|state| change_all_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&self.1, Method::PUT);
|
||||
}
|
||||
|
||||
fn change<Handler: ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res: Send + 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
let path = format!("{}/:id", self.1);
|
||||
self.0
|
||||
.put(&path)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| change_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&path, Method::PUT);
|
||||
}
|
||||
|
||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.delete(&self.1)
|
||||
.extend_route_matcher(matcher)
|
||||
.to(|state| remove_all_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&self.1, Method::DELETE);
|
||||
}
|
||||
|
||||
fn remove<Handler: ResourceRemove>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let path = format!("{}/:id", self.1);
|
||||
self.0
|
||||
.delete(&path)
|
||||
.extend_route_matcher(matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| remove_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&path, Method::POST);
|
||||
#[cfg(feature = "cors")]
|
||||
if E::http_method() != Method::GET {
|
||||
assoc
|
||||
.options()
|
||||
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
|
||||
.to(crate::cors::cors_preflight_handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
11
src/types.rs
11
src/types.rs
|
@ -4,7 +4,7 @@ use crate::OpenapiType;
|
|||
use gotham::hyper::body::Bytes;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{error::Error, panic::RefUnwindSafe};
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
pub trait ResourceType {}
|
||||
|
@ -98,12 +98,3 @@ impl<T: ResourceType + DeserializeOwned> RequestBody for T {
|
|||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
}
|
||||
|
||||
/// A type than can be used as a parameter to a resource method. Implemented for every type
|
||||
/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of
|
||||
/// type [OpenapiType].
|
||||
///
|
||||
/// [OpenapiType]: trait.OpenapiType.html
|
||||
pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {}
|
||||
|
||||
impl<T: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue