mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-02-22 20:52:27 +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
|
@ -6,23 +6,24 @@ stages:
|
|||
|
||||
variables:
|
||||
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||
RUST_LOG: info,gotham=debug,gotham_restful=trace
|
||||
|
||||
test-default:
|
||||
stage: test
|
||||
image: rust:1.42-slim
|
||||
image: rust:1.43-slim
|
||||
before_script:
|
||||
- cargo -V
|
||||
script:
|
||||
- cargo test
|
||||
cache:
|
||||
key: cargo-1-42-default
|
||||
key: cargo-1-43-default
|
||||
paths:
|
||||
- cargo/
|
||||
- target/
|
||||
|
||||
test-full:
|
||||
stage: test
|
||||
image: rust:1.42-slim
|
||||
image: rust:1.43-slim
|
||||
before_script:
|
||||
- apt update -y
|
||||
- apt install -y --no-install-recommends libpq-dev
|
||||
|
@ -30,7 +31,7 @@ test-full:
|
|||
script:
|
||||
- cargo test --no-default-features --features full
|
||||
cache:
|
||||
key: cargo-1-42-all
|
||||
key: cargo-1-43-all
|
||||
paths:
|
||||
- cargo/
|
||||
- target/
|
||||
|
@ -86,6 +87,7 @@ rustfmt:
|
|||
- cargo fmt --version
|
||||
script:
|
||||
- cargo fmt -- --check
|
||||
- ./tests/ui/rustfmt.sh --check
|
||||
|
||||
doc:
|
||||
stage: build
|
||||
|
|
|
@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
### Added
|
||||
- Support custom HTTP response headers
|
||||
- New `endpoint` router extension with associated `Endpoint` trait ([!18])
|
||||
|
||||
### Changed
|
||||
- The cors handler can now copy headers from the request if desired
|
||||
- All fields of `Response` are now private
|
||||
- If not enabling the `openapi` feature, `without-openapi` has to be enabled
|
||||
- The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18])
|
||||
|
||||
### Removed
|
||||
- All pre-defined methods (`read`, `create`, ...) from our router extensions ([!18])
|
||||
- All pre-defined method traits (`ResourceRead`, ...) ([!18])
|
||||
|
||||
## [0.1.1] - 2020-12-28
|
||||
### Added
|
||||
|
@ -25,3 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [0.1.0] - 2020-10-02
|
||||
Previous changes are not tracked by this changelog file. Refer to the [releases](https://gitlab.com/msrd0/gotham-restful/-/releases) for the changelog.
|
||||
|
||||
|
||||
[!18]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/18
|
||||
|
|
|
@ -33,7 +33,9 @@ indexmap = { version = "1.3.2", optional = true }
|
|||
jsonwebtoken = { version = "7.1.0", optional = true }
|
||||
log = "0.4.8"
|
||||
mime = "0.3.16"
|
||||
once_cell = { version = "1.5", optional = true }
|
||||
openapiv3 = { version = "0.3.2", optional = true }
|
||||
regex = { version = "1.4", optional = true }
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
serde_json = "1.0.58"
|
||||
uuid = { version = "0.8.1", optional = true }
|
||||
|
@ -42,12 +44,13 @@ uuid = { version = "0.8.1", optional = true }
|
|||
diesel = { version = "1.4.4", features = ["postgres"] }
|
||||
futures-executor = "0.3.5"
|
||||
paste = "1.0"
|
||||
pretty_env_logger = "0.4"
|
||||
thiserror = "1.0.18"
|
||||
trybuild = "1.0.27"
|
||||
|
||||
[features]
|
||||
default = ["cors", "errorlog", "without-openapi"]
|
||||
full = ["auth", "cors", "database", "errorlog", "openapi"]
|
||||
full = ["auth", "chrono", "cors", "database", "errorlog", "openapi", "uuid"]
|
||||
|
||||
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
||||
cors = []
|
||||
|
@ -56,7 +59,7 @@ errorlog = []
|
|||
|
||||
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
|
||||
without-openapi = []
|
||||
openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"]
|
||||
openapi = ["gotham_restful_derive/openapi", "indexmap", "once_cell", "openapiv3", "regex"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
no-default-features = true
|
||||
|
|
97
README.md
97
README.md
|
@ -17,8 +17,8 @@
|
|||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
||||
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||
</a>
|
||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
|
||||
<a href="https://blog.rust-lang.org/2020/04/23/Rust-1.43.0.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.43+-orange.svg"/>
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<br/>
|
||||
|
||||
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
|
||||
|
@ -45,12 +45,12 @@ 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 |
|
||||
| ----------- | ------------------ | --------- | ----------- |
|
||||
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ------------- | ------------------ | --------- | -------------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
|
@ -60,8 +60,8 @@ Assuming you assign `/foobar` to your resource, you can implement the following
|
|||
| 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
|
||||
/// Our RESTful resource.
|
||||
|
@ -69,14 +69,14 @@ 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)]
|
||||
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()
|
||||
}
|
||||
|
@ -84,17 +84,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
|
||||
|
||||
|
@ -114,7 +113,7 @@ struct RawImage {
|
|||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(ImageResource)]
|
||||
#[create]
|
||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
|
@ -126,21 +125,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
|
||||
#[derive(Resource)]
|
||||
|
@ -159,7 +160,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 })
|
||||
|
@ -185,14 +186,14 @@ 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
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() {
|
||||
// your handler
|
||||
}
|
||||
|
@ -221,7 +222,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
|
||||
#[derive(Resource)]
|
||||
|
@ -234,7 +235,7 @@ struct Foo {
|
|||
value: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||
foo::table.load(conn)
|
||||
}
|
||||
|
@ -261,7 +262,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
|
||||
#[derive(Resource)]
|
||||
|
@ -273,7 +274,7 @@ struct Foo {
|
|||
bar: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Success<Foo> {
|
||||
Foo { bar: "Hello World".to_owned() }.into()
|
||||
}
|
||||
|
@ -293,22 +294,17 @@ 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
|
||||
|
||||
```toml
|
||||
[features]
|
||||
openapi = ["gotham-restful/openapi"]
|
||||
```
|
||||
|
||||
into your libraries `Cargo.toml` and use the following for all types used with handlers:
|
||||
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:
|
||||
|
||||
```rust
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
@ -318,18 +314,15 @@ 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
|
||||
|
||||
## Versioning
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
||||
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||
</a>
|
||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
|
||||
<a href="https://blog.rust-lang.org/2020/04/23/Rust-1.43.0.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.43+-orange.svg"/>
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
||||
|
|
469
derive/src/endpoint.rs
Normal file
469
derive/src/endpoint.rs
Normal file
|
@ -0,0 +1,469 @@
|
|||
use crate::util::{CollectToResult, PathEndsWith};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Result,
|
||||
ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum EndpointType {
|
||||
ReadAll,
|
||||
Read,
|
||||
Search,
|
||||
Create,
|
||||
UpdateAll,
|
||||
Update,
|
||||
DeleteAll,
|
||||
Delete
|
||||
}
|
||||
|
||||
impl FromStr for EndpointType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self> {
|
||||
match str {
|
||||
"ReadAll" | "read_all" => Ok(Self::ReadAll),
|
||||
"Read" | "read" => Ok(Self::Read),
|
||||
"Search" | "search" => Ok(Self::Search),
|
||||
"Create" | "create" => Ok(Self::Create),
|
||||
"ChangeAll" | "change_all" => Ok(Self::UpdateAll),
|
||||
"Change" | "change" => Ok(Self::Update),
|
||||
"RemoveAll" | "remove_all" => Ok(Self::DeleteAll),
|
||||
"Remove" | "remove" => Ok(Self::Delete),
|
||||
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EndpointType {
|
||||
fn http_method(&self) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search => quote!(::gotham_restful::gotham::hyper::Method::GET),
|
||||
Self::Create => quote!(::gotham_restful::gotham::hyper::Method::POST),
|
||||
Self::UpdateAll | Self::Update => quote!(::gotham_restful::gotham::hyper::Method::PUT),
|
||||
Self::DeleteAll | Self::Delete => quote!(::gotham_restful::gotham::hyper::Method::DELETE)
|
||||
}
|
||||
}
|
||||
|
||||
fn uri(&self) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => quote!(""),
|
||||
Self::Read | Self::Update | Self::Delete => quote!(":id"),
|
||||
Self::Search => quote!("search")
|
||||
}
|
||||
}
|
||||
|
||||
fn has_placeholders(&self) -> LitBool {
|
||||
match self {
|
||||
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
},
|
||||
Self::Read | Self::Update | Self::Delete => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholders_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Search | Self::Create | Self::UpdateAll | Self::DeleteAll => {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopPathExtractor)
|
||||
},
|
||||
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::export::IdPlaceholder::<#arg_ty>)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_params(&self) -> LitBool {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
||||
LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
}
|
||||
},
|
||||
Self::Search => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn params_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Create | Self::UpdateAll | Self::Update | Self::DeleteAll | Self::Delete => {
|
||||
quote!(::gotham_restful::gotham::extractor::NoopQueryStringExtractor)
|
||||
},
|
||||
Self::Search => quote!(#arg_ty)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_body(&self) -> LitBool {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => LitBool {
|
||||
value: false,
|
||||
span: Span::call_site()
|
||||
},
|
||||
Self::Create | Self::UpdateAll | Self::Update => LitBool {
|
||||
value: true,
|
||||
span: Span::call_site()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn body_ty(&self, arg_ty: Option<&Type>) -> TokenStream {
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search | Self::DeleteAll | Self::Delete => quote!(()),
|
||||
Self::Create | Self::UpdateAll | Self::Update => quote!(#arg_ty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum HandlerArgType {
|
||||
StateRef,
|
||||
StateMutRef,
|
||||
MethodArg(Type),
|
||||
DatabaseConnection(Type),
|
||||
AuthStatus(Type),
|
||||
AuthStatusRef(Type)
|
||||
}
|
||||
|
||||
impl HandlerArgType {
|
||||
fn is_method_arg(&self) -> bool {
|
||||
matches!(self, Self::MethodArg(_))
|
||||
}
|
||||
|
||||
fn is_database_conn(&self) -> bool {
|
||||
matches!(self, Self::DatabaseConnection(_))
|
||||
}
|
||||
|
||||
fn is_auth_status(&self) -> bool {
|
||||
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
|
||||
}
|
||||
|
||||
fn ty(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_ty(&self) -> Option<TokenStream> {
|
||||
self.ty().map(|ty| quote!(#ty))
|
||||
}
|
||||
}
|
||||
|
||||
struct HandlerArg {
|
||||
ident_span: Span,
|
||||
ty: HandlerArgType
|
||||
}
|
||||
|
||||
impl Spanned for HandlerArg {
|
||||
fn span(&self) -> Span {
|
||||
self.ident_span
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<HandlerArgType> {
|
||||
let attr = attrs
|
||||
.iter()
|
||||
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
|
||||
.map(|arg| arg.tokens.to_string());
|
||||
|
||||
// TODO issue a warning for _state usage once diagnostics become stable
|
||||
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
|
||||
return match ty {
|
||||
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
|
||||
HandlerArgType::StateRef
|
||||
} else {
|
||||
HandlerArgType::StateMutRef
|
||||
}),
|
||||
_ => Err(Error::new(
|
||||
ty.span(),
|
||||
"The state parameter has to be a (mutable) reference to gotham_restful::State"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
|
||||
return Ok(match ty {
|
||||
Type::Reference(ty) => HandlerArgType::AuthStatusRef(*ty.elem),
|
||||
ty => HandlerArgType::AuthStatus(ty)
|
||||
});
|
||||
}
|
||||
|
||||
if cfg!(feature = "database")
|
||||
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
|
||||
{
|
||||
return Ok(HandlerArgType::DatabaseConnection(match ty {
|
||||
Type::Reference(ty) => *ty.elem,
|
||||
ty => ty
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(HandlerArgType::MethodArg(ty))
|
||||
}
|
||||
|
||||
fn interpret_arg(_index: usize, arg: &PatType) -> Result<HandlerArg> {
|
||||
let pat = &arg.pat;
|
||||
let orig_name = quote!(#pat);
|
||||
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
|
||||
|
||||
Ok(HandlerArg {
|
||||
ident_span: arg.pat.span(),
|
||||
ty
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn expand_operation_id(operation_id: Option<Lit>) -> Option<TokenStream> {
|
||||
match operation_id {
|
||||
Some(operation_id) => Some(quote! {
|
||||
fn operation_id() -> Option<String> {
|
||||
Some(#operation_id.to_string())
|
||||
}
|
||||
}),
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn expand_operation_id(_: Option<Lit>) -> Option<TokenStream> {
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_wants_auth(wants_auth: Option<Lit>, default: bool) -> TokenStream {
|
||||
let wants_auth = wants_auth.unwrap_or_else(|| {
|
||||
Lit::Bool(LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
})
|
||||
});
|
||||
|
||||
quote! {
|
||||
fn wants_auth() -> bool {
|
||||
#wants_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endpoint_ident(fn_ident: &Ident) -> Ident {
|
||||
format_ident!("{}___gotham_restful_endpoint", fn_ident)
|
||||
}
|
||||
|
||||
fn expand_endpoint_type(ty: EndpointType, attrs: AttributeArgs, fun: &ItemFn) -> Result<TokenStream> {
|
||||
// reject unsafe functions
|
||||
if let Some(unsafety) = fun.sig.unsafety {
|
||||
return Err(Error::new(unsafety.span(), "Endpoint handler methods must not be unsafe"));
|
||||
}
|
||||
|
||||
// parse arguments
|
||||
let mut operation_id: Option<Lit> = None;
|
||||
let mut wants_auth: Option<Lit> = None;
|
||||
for meta in attrs {
|
||||
match meta {
|
||||
NestedMeta::Meta(Meta::NameValue(kv)) => {
|
||||
if kv.path.ends_with("operation_id") {
|
||||
operation_id = Some(kv.lit);
|
||||
} else if kv.path.ends_with("wants_auth") {
|
||||
wants_auth = Some(kv.lit);
|
||||
} else {
|
||||
return Err(Error::new(kv.path.span(), "Unknown attribute"));
|
||||
}
|
||||
},
|
||||
_ => return Err(Error::new(meta.span(), "Invalid attribute syntax"))
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
if let Some(operation_id) = operation_id {
|
||||
return Err(Error::new(
|
||||
operation_id.span(),
|
||||
"`operation_id` is only supported with the openapi feature"
|
||||
));
|
||||
}
|
||||
|
||||
// extract arguments into pattern, ident and type
|
||||
let args = fun
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match arg {
|
||||
FnArg::Typed(arg) => interpret_arg(i, arg),
|
||||
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
let fun_vis = &fun.vis;
|
||||
let fun_ident = &fun.sig.ident;
|
||||
let fun_is_async = fun.sig.asyncness.is_some();
|
||||
|
||||
let ident = endpoint_ident(fun_ident);
|
||||
let dummy_ident = format_ident!("_IMPL_Endpoint_for_{}", ident);
|
||||
let (output_ty, is_no_content) = match &fun.sig.output {
|
||||
ReturnType::Default => (quote!(::gotham_restful::NoContent), true),
|
||||
ReturnType::Type(_, ty) => (quote!(#ty), false)
|
||||
};
|
||||
|
||||
let arg_tys = args.iter().filter(|arg| arg.ty.is_method_arg()).collect::<Vec<_>>();
|
||||
let mut arg_ty_idx = 0;
|
||||
let mut next_arg_ty = |return_none: bool| {
|
||||
if return_none {
|
||||
return Ok(None);
|
||||
}
|
||||
if arg_ty_idx >= arg_tys.len() {
|
||||
return Err(Error::new(fun_ident.span(), "Too few arguments"));
|
||||
}
|
||||
let ty = arg_tys[arg_ty_idx].ty.ty().unwrap();
|
||||
arg_ty_idx += 1;
|
||||
Ok(Some(ty))
|
||||
};
|
||||
|
||||
let http_method = ty.http_method();
|
||||
let uri = ty.uri();
|
||||
let has_placeholders = ty.has_placeholders();
|
||||
let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?);
|
||||
let needs_params = ty.needs_params();
|
||||
let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?);
|
||||
let needs_body = ty.needs_body();
|
||||
let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?);
|
||||
|
||||
if arg_ty_idx < arg_tys.len() {
|
||||
return Err(Error::new(fun_ident.span(), "Too many arguments"));
|
||||
}
|
||||
|
||||
let mut handle_args: Vec<TokenStream> = Vec::new();
|
||||
if has_placeholders.value {
|
||||
handle_args.push(quote!(placeholders.id));
|
||||
}
|
||||
if needs_params.value {
|
||||
handle_args.push(quote!(params));
|
||||
}
|
||||
if needs_body.value {
|
||||
handle_args.push(quote!(body.unwrap()));
|
||||
}
|
||||
let handle_args = args.iter().map(|arg| match arg.ty {
|
||||
HandlerArgType::StateRef | HandlerArgType::StateMutRef => quote!(state),
|
||||
HandlerArgType::MethodArg(_) => handle_args.remove(0),
|
||||
HandlerArgType::DatabaseConnection(_) => quote!(&conn),
|
||||
HandlerArgType::AuthStatus(_) => quote!(auth),
|
||||
HandlerArgType::AuthStatusRef(_) => quote!(&auth)
|
||||
});
|
||||
|
||||
let expand_handle_content = || {
|
||||
let mut state_block = quote!();
|
||||
if let Some(arg) = args.iter().find(|arg| arg.ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let auth: #auth_ty = state.borrow::<#auth_ty>().clone();
|
||||
}
|
||||
}
|
||||
|
||||
let mut handle_content = quote!(#fun_ident(#(#handle_args),*));
|
||||
if fun_is_async {
|
||||
if let Some(arg) = args.iter().find(|arg| matches!(arg.ty, HandlerArgType::StateRef)) {
|
||||
return Err(Error::new(arg.span(), "Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`"));
|
||||
}
|
||||
handle_content = quote!(#handle_content.await);
|
||||
}
|
||||
if is_no_content {
|
||||
handle_content = quote!(#handle_content; ::gotham_restful::NoContent)
|
||||
}
|
||||
|
||||
if let Some(arg) = args.iter().find(|arg| arg.ty.is_database_conn()) {
|
||||
let conn_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let repo = <::gotham_restful::export::Repo<#conn_ty>>::borrow_from(state).clone();
|
||||
};
|
||||
handle_content = quote! {
|
||||
repo.run::<_, _, ()>(move |conn| {
|
||||
Ok({ #handle_content })
|
||||
}).await.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
use ::gotham_restful::export::FutureExt as _;
|
||||
#state_block
|
||||
async move {
|
||||
#handle_content
|
||||
}.boxed()
|
||||
})
|
||||
};
|
||||
let handle_content = match expand_handle_content() {
|
||||
Ok(content) => content,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
|
||||
let tr8 = if cfg!(feature = "openapi") {
|
||||
quote!(::gotham_restful::EndpointWithSchema)
|
||||
} else {
|
||||
quote!(::gotham_restful::Endpoint)
|
||||
};
|
||||
let operation_id = expand_operation_id(operation_id);
|
||||
let wants_auth = expand_wants_auth(wants_auth, args.iter().any(|arg| arg.ty.is_auth_status()));
|
||||
Ok(quote! {
|
||||
#[doc(hidden)]
|
||||
/// `gotham_restful` implementation detail
|
||||
#[allow(non_camel_case_types)]
|
||||
#fun_vis struct #ident;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #dummy_ident: () = {
|
||||
impl #tr8 for #ident {
|
||||
fn http_method() -> ::gotham_restful::gotham::hyper::Method {
|
||||
#http_method
|
||||
}
|
||||
|
||||
fn uri() -> ::std::borrow::Cow<'static, str> {
|
||||
{ #uri }.into()
|
||||
}
|
||||
|
||||
type Output = #output_ty;
|
||||
|
||||
fn has_placeholders() -> bool {
|
||||
#has_placeholders
|
||||
}
|
||||
type Placeholders = #placeholder_ty;
|
||||
|
||||
fn needs_params() -> bool {
|
||||
#needs_params
|
||||
}
|
||||
type Params = #params_ty;
|
||||
|
||||
fn needs_body() -> bool {
|
||||
#needs_body
|
||||
}
|
||||
type Body = #body_ty;
|
||||
|
||||
fn handle(
|
||||
state: &mut ::gotham_restful::gotham::state::State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: ::std::option::Option<Self::Body>
|
||||
) -> ::gotham_restful::export::BoxFuture<'static, Self::Output> {
|
||||
#handle_content
|
||||
}
|
||||
|
||||
#operation_id
|
||||
#wants_auth
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expand_endpoint(ty: EndpointType, attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
|
||||
let endpoint_type = match expand_endpoint_type(ty, attrs, &fun) {
|
||||
Ok(code) => code,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
Ok(quote! {
|
||||
#fun
|
||||
#endpoint_type
|
||||
})
|
||||
}
|
|
@ -5,21 +5,29 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re
|
|||
|
||||
mod util;
|
||||
|
||||
mod endpoint;
|
||||
use endpoint::{expand_endpoint, EndpointType};
|
||||
|
||||
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;
|
||||
|
||||
mod resource_error;
|
||||
use resource_error::expand_resource_error;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
mod openapi_type;
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::expand_openapi_type;
|
||||
|
||||
mod private_openapi_trait;
|
||||
use private_openapi_trait::expand_private_openapi_trait;
|
||||
|
||||
#[inline]
|
||||
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
||||
// eprintln!("{}", tokens);
|
||||
|
@ -77,40 +85,47 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
|||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Delete, attr, item))
|
||||
}
|
||||
|
||||
/// PRIVATE MACRO - DO NOT USE
|
||||
#[doc(hidden)]
|
||||
#[proc_macro_attribute]
|
||||
pub fn _private_openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, expand_private_openapi_trait)
|
||||
}
|
||||
|
|
|
@ -1,466 +0,0 @@
|
|||
use crate::util::CollectToResult;
|
||||
use heck::{CamelCase, SnakeCase};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Path, Result,
|
||||
ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum Method {
|
||||
ReadAll,
|
||||
Read,
|
||||
Search,
|
||||
Create,
|
||||
ChangeAll,
|
||||
Change,
|
||||
RemoveAll,
|
||||
Remove
|
||||
}
|
||||
|
||||
impl FromStr for Method {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self> {
|
||||
match str {
|
||||
"ReadAll" | "read_all" => Ok(Self::ReadAll),
|
||||
"Read" | "read" => Ok(Self::Read),
|
||||
"Search" | "search" => Ok(Self::Search),
|
||||
"Create" | "create" => Ok(Self::Create),
|
||||
"ChangeAll" | "change_all" => Ok(Self::ChangeAll),
|
||||
"Change" | "change" => Ok(Self::Change),
|
||||
"RemoveAll" | "remove_all" => Ok(Self::RemoveAll),
|
||||
"Remove" | "remove" => Ok(Self::Remove),
|
||||
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn type_names(&self) -> Vec<&'static str> {
|
||||
use Method::*;
|
||||
|
||||
match self {
|
||||
ReadAll | RemoveAll => vec![],
|
||||
Read | Remove => vec!["ID"],
|
||||
Search => vec!["Query"],
|
||||
Create | ChangeAll => vec!["Body"],
|
||||
Change => vec!["ID", "Body"]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trait_ident(&self) -> Ident {
|
||||
use Method::*;
|
||||
|
||||
let name = match self {
|
||||
ReadAll => "ReadAll",
|
||||
Read => "Read",
|
||||
Search => "Search",
|
||||
Create => "Create",
|
||||
ChangeAll => "ChangeAll",
|
||||
Change => "Change",
|
||||
RemoveAll => "RemoveAll",
|
||||
Remove => "Remove"
|
||||
};
|
||||
format_ident!("Resource{}", name)
|
||||
}
|
||||
|
||||
pub fn fn_ident(&self) -> Ident {
|
||||
use Method::*;
|
||||
|
||||
let name = match self {
|
||||
ReadAll => "read_all",
|
||||
Read => "read",
|
||||
Search => "search",
|
||||
Create => "create",
|
||||
ChangeAll => "change_all",
|
||||
Change => "change",
|
||||
RemoveAll => "remove_all",
|
||||
Remove => "remove"
|
||||
};
|
||||
format_ident!("{}", name)
|
||||
}
|
||||
|
||||
pub fn handler_struct_ident(&self, resource: &str) -> Ident {
|
||||
format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident())
|
||||
}
|
||||
|
||||
pub fn setup_ident(&self, resource: &str) -> Ident {
|
||||
format_ident!("_gotham_restful_{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MethodArgumentType {
|
||||
StateRef,
|
||||
StateMutRef,
|
||||
MethodArg(Type),
|
||||
DatabaseConnection(Type),
|
||||
AuthStatus(Type),
|
||||
AuthStatusRef(Type)
|
||||
}
|
||||
|
||||
impl MethodArgumentType {
|
||||
fn is_method_arg(&self) -> bool {
|
||||
matches!(self, Self::MethodArg(_))
|
||||
}
|
||||
|
||||
fn is_database_conn(&self) -> bool {
|
||||
matches!(self, Self::DatabaseConnection(_))
|
||||
}
|
||||
|
||||
fn is_auth_status(&self) -> bool {
|
||||
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
|
||||
}
|
||||
|
||||
fn ty(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_ty(&self) -> Option<TokenStream> {
|
||||
self.ty().map(|ty| quote!(#ty))
|
||||
}
|
||||
}
|
||||
|
||||
struct MethodArgument {
|
||||
ident: Ident,
|
||||
ident_span: Span,
|
||||
ty: MethodArgumentType
|
||||
}
|
||||
|
||||
impl Spanned for MethodArgument {
|
||||
fn span(&self) -> Span {
|
||||
self.ident_span
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<MethodArgumentType> {
|
||||
let attr = attrs
|
||||
.iter()
|
||||
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
|
||||
.map(|arg| arg.tokens.to_string());
|
||||
|
||||
// TODO issue a warning for _state usage once diagnostics become stable
|
||||
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
|
||||
return match ty {
|
||||
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
|
||||
MethodArgumentType::StateRef
|
||||
} else {
|
||||
MethodArgumentType::StateMutRef
|
||||
}),
|
||||
_ => Err(Error::new(
|
||||
ty.span(),
|
||||
"The state parameter has to be a (mutable) reference to gotham_restful::State"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
|
||||
return Ok(match ty {
|
||||
Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem),
|
||||
ty => MethodArgumentType::AuthStatus(ty)
|
||||
});
|
||||
}
|
||||
|
||||
if cfg!(feature = "database")
|
||||
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
|
||||
{
|
||||
return Ok(MethodArgumentType::DatabaseConnection(match ty {
|
||||
Type::Reference(ty) => *ty.elem,
|
||||
ty => ty
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(MethodArgumentType::MethodArg(ty))
|
||||
}
|
||||
|
||||
fn interpret_arg(index: usize, arg: &PatType) -> Result<MethodArgument> {
|
||||
let pat = &arg.pat;
|
||||
let ident = format_ident!("arg{}", index);
|
||||
let orig_name = quote!(#pat);
|
||||
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
|
||||
|
||||
Ok(MethodArgument {
|
||||
ident,
|
||||
ident_span: arg.pat.span(),
|
||||
ty
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream {
|
||||
let mut operation_id: Option<&Lit> = None;
|
||||
for meta in attrs {
|
||||
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
|
||||
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) {
|
||||
operation_id = Some(&kv.lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match operation_id {
|
||||
Some(operation_id) => quote! {
|
||||
fn operation_id() -> Option<String>
|
||||
{
|
||||
Some(#operation_id.to_string())
|
||||
}
|
||||
},
|
||||
None => quote!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn expand_operation_id(_: &[NestedMeta]) -> TokenStream {
|
||||
quote!()
|
||||
}
|
||||
|
||||
fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream {
|
||||
let default_lit = Lit::Bool(LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
});
|
||||
let mut wants_auth = &default_lit;
|
||||
for meta in attrs {
|
||||
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
|
||||
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) {
|
||||
wants_auth = &kv.lit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
fn wants_auth() -> bool
|
||||
{
|
||||
#wants_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::comparison_chain)]
|
||||
fn setup_body(
|
||||
method: &Method,
|
||||
fun: &ItemFn,
|
||||
attrs: &[NestedMeta],
|
||||
resource_name: &str,
|
||||
resource_path: &Path
|
||||
) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
|
||||
let fun_ident = &fun.sig.ident;
|
||||
let fun_is_async = fun.sig.asyncness.is_some();
|
||||
|
||||
if let Some(unsafety) = fun.sig.unsafety {
|
||||
return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe"));
|
||||
}
|
||||
|
||||
let trait_ident = method.trait_ident();
|
||||
let method_ident = method.fn_ident();
|
||||
let handler_ident = method.handler_struct_ident(resource_name);
|
||||
|
||||
let (ret, is_no_content) = match &fun.sig.output {
|
||||
ReturnType::Default => (quote!(#krate::NoContent), true),
|
||||
ReturnType::Type(_, ty) => (quote!(#ty), false)
|
||||
};
|
||||
|
||||
// some default idents we'll need
|
||||
let state_ident = format_ident!("state");
|
||||
let repo_ident = format_ident!("repo");
|
||||
let conn_ident = format_ident!("conn");
|
||||
let auth_ident = format_ident!("auth");
|
||||
let res_ident = format_ident!("res");
|
||||
|
||||
// extract arguments into pattern, ident and type
|
||||
let args = fun
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match arg {
|
||||
FnArg::Typed(arg) => interpret_arg(i, arg),
|
||||
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
// extract the generic parameters to use
|
||||
let ty_names = method.type_names();
|
||||
let ty_len = ty_names.len();
|
||||
let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect();
|
||||
if generics_args.len() > ty_len {
|
||||
return Err(Error::new(generics_args[ty_len].span(), "Too many arguments"));
|
||||
} else if generics_args.len() < ty_len {
|
||||
return Err(Error::new(fun_ident.span(), "Too few arguments"));
|
||||
}
|
||||
let generics: Vec<TokenStream> = generics_args
|
||||
.iter()
|
||||
.map(|arg| arg.ty.quote_ty().unwrap())
|
||||
.zip(ty_names)
|
||||
.map(|(arg, name)| {
|
||||
let ident = format_ident!("{}", name);
|
||||
quote!(type #ident = #arg;)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// extract the definition of our method
|
||||
let mut args_def: Vec<TokenStream> = args
|
||||
.iter()
|
||||
.filter(|arg| (*arg).ty.is_method_arg())
|
||||
.map(|arg| {
|
||||
let ident = &arg.ident;
|
||||
let ty = arg.ty.quote_ty();
|
||||
quote!(#ident : #ty)
|
||||
})
|
||||
.collect();
|
||||
args_def.insert(0, quote!(mut #state_ident : #krate::State));
|
||||
|
||||
// extract the arguments to pass over to the supplied method
|
||||
let args_pass: Vec<TokenStream> = args
|
||||
.iter()
|
||||
.map(|arg| match (&arg.ty, &arg.ident) {
|
||||
(MethodArgumentType::StateRef, _) => quote!(&#state_ident),
|
||||
(MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident),
|
||||
(MethodArgumentType::MethodArg(_), ident) => quote!(#ident),
|
||||
(MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident),
|
||||
(MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident),
|
||||
(MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// prepare the method block
|
||||
let mut block = quote!(#fun_ident(#(#args_pass),*));
|
||||
let mut state_block = quote!();
|
||||
if fun_is_async {
|
||||
if let Some(arg) = args.iter().find(|arg| matches!((*arg).ty, MethodArgumentType::StateRef)) {
|
||||
return Err(Error::new(
|
||||
arg.span(),
|
||||
"async fn must not take &State as an argument as State is not Sync, consider taking &mut State"
|
||||
));
|
||||
}
|
||||
block = quote!(#block.await);
|
||||
}
|
||||
if is_no_content {
|
||||
block = quote!(#block; Default::default())
|
||||
}
|
||||
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) {
|
||||
if fun_is_async {
|
||||
return Err(Error::new(
|
||||
arg.span(),
|
||||
"async fn is not supported when database support is required, consider boxing"
|
||||
));
|
||||
}
|
||||
let conn_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone();
|
||||
};
|
||||
block = quote! {
|
||||
{
|
||||
let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| {
|
||||
let #res_ident = { #block };
|
||||
Ok((#state_ident, #res_ident))
|
||||
}).await.unwrap();
|
||||
#state_ident = #res_ident.0;
|
||||
#res_ident.1
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone();
|
||||
};
|
||||
}
|
||||
|
||||
// prepare the where clause
|
||||
let mut where_clause = quote!(#resource_path : #krate::Resource,);
|
||||
for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
where_clause = quote!(#where_clause #auth_ty : Clone,);
|
||||
}
|
||||
|
||||
// attribute generated code
|
||||
let operation_id = expand_operation_id(attrs);
|
||||
let wants_auth = expand_wants_auth(attrs, args.iter().any(|arg| (*arg).ty.is_auth_status()));
|
||||
|
||||
// put everything together
|
||||
let mut dummy = format_ident!("_IMPL_RESOURCEMETHOD_FOR_{}", fun_ident);
|
||||
dummy.set_span(Span::call_site());
|
||||
Ok(quote! {
|
||||
struct #handler_ident;
|
||||
|
||||
impl #krate::ResourceMethod for #handler_ident {
|
||||
type Res = #ret;
|
||||
|
||||
#operation_id
|
||||
#wants_auth
|
||||
}
|
||||
|
||||
impl #krate::#trait_ident for #handler_ident
|
||||
where #where_clause
|
||||
{
|
||||
#(#generics)*
|
||||
|
||||
fn #method_ident(#(#args_def),*) -> std::pin::Pin<Box<dyn std::future::Future<Output = (#krate::State, #ret)> + Send>> {
|
||||
#[allow(unused_imports)]
|
||||
use #krate::{export::FutureExt, FromState};
|
||||
|
||||
#state_block
|
||||
|
||||
async move {
|
||||
let #res_ident = { #block };
|
||||
(#state_ident, #res_ident)
|
||||
}.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
route.#method_ident::<#handler_ident>();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
|
||||
// parse attributes
|
||||
if attrs.len() < 1 {
|
||||
return Err(Error::new(
|
||||
Span::call_site(),
|
||||
"Missing Resource struct. Example: #[read_all(MyResource)]"
|
||||
));
|
||||
}
|
||||
let resource_path = match attrs.remove(0) {
|
||||
NestedMeta::Meta(Meta::Path(path)) => path,
|
||||
p => {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"Expected name of the Resource struct this method belongs to"
|
||||
))
|
||||
},
|
||||
};
|
||||
let resource_name = resource_path
|
||||
.segments
|
||||
.last()
|
||||
.map(|s| s.ident.to_string())
|
||||
.ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?;
|
||||
|
||||
let fun_vis = &fun.vis;
|
||||
let setup_ident = method.setup_ident(&resource_name);
|
||||
let setup_body = match setup_body(&method, &fun, &attrs, &resource_name, &resource_path) {
|
||||
Ok(body) => body,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#fun
|
||||
|
||||
#[deny(dead_code)]
|
||||
#[doc(hidden)]
|
||||
/// `gotham_restful` implementation detail.
|
||||
#fun_vis fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D) {
|
||||
#setup_body
|
||||
}
|
||||
})
|
||||
}
|
|
@ -3,7 +3,8 @@ use proc_macro2::{Ident, TokenStream};
|
|||
use quote::quote;
|
||||
use syn::{
|
||||
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
|
||||
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant
|
||||
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Path, PathSegment, PredicateType, Result, TraitBound,
|
||||
TraitBoundModifier, Type, TypeParamBound, TypePath, Variant, WhereClause, WherePredicate
|
||||
};
|
||||
|
||||
pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
|
||||
|
@ -17,24 +18,46 @@ pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
fn expand_where(generics: &Generics) -> TokenStream {
|
||||
fn update_generics(generics: &Generics, where_clause: &mut Option<WhereClause>) {
|
||||
if generics.params.is_empty() {
|
||||
return quote!();
|
||||
return;
|
||||
}
|
||||
|
||||
let krate = super::krate();
|
||||
let idents = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|param| match param {
|
||||
GenericParam::Type(ty) => Some(ty.ident.clone()),
|
||||
_ => None
|
||||
})
|
||||
.filter(|param| param.is_some())
|
||||
.map(|param| param.unwrap());
|
||||
if where_clause.is_none() {
|
||||
*where_clause = Some(WhereClause {
|
||||
where_token: Default::default(),
|
||||
predicates: Default::default()
|
||||
});
|
||||
}
|
||||
let where_clause = where_clause.as_mut().unwrap();
|
||||
|
||||
quote! {
|
||||
where #(#idents : #krate::OpenapiType),*
|
||||
for param in &generics.params {
|
||||
if let GenericParam::Type(ty_param) = param {
|
||||
where_clause.predicates.push(WherePredicate::Type(PredicateType {
|
||||
lifetimes: None,
|
||||
bounded_ty: Type::Path(TypePath {
|
||||
qself: None,
|
||||
path: Path {
|
||||
leading_colon: None,
|
||||
segments: vec![PathSegment {
|
||||
ident: ty_param.ident.clone(),
|
||||
arguments: Default::default()
|
||||
}]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}),
|
||||
colon_token: Default::default(),
|
||||
bounds: vec![TypeParamBound::Trait(TraitBound {
|
||||
paren_token: None,
|
||||
modifier: TraitBoundModifier::None,
|
||||
lifetimes: None,
|
||||
path: syn::parse_str("::gotham_restful::OpenapiType").unwrap()
|
||||
})]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +129,9 @@ fn expand_variant(variant: &Variant) -> Result<TokenStream> {
|
|||
|
||||
fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let where_clause = expand_where(&generics);
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
let mut where_clause = where_clause.cloned();
|
||||
update_generics(&generics, &mut where_clause);
|
||||
|
||||
let attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
|
@ -118,7 +143,7 @@ fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: D
|
|||
let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
|
@ -208,7 +233,9 @@ fn expand_field(field: &Field) -> Result<TokenStream> {
|
|||
|
||||
fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let where_clause = expand_where(&generics);
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
let mut where_clause = where_clause.cloned();
|
||||
update_generics(&generics, &mut where_clause);
|
||||
|
||||
let attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
|
@ -229,7 +256,7 @@ fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input:
|
|||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
|
|
165
derive/src/private_openapi_trait.rs
Normal file
165
derive/src/private_openapi_trait.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use crate::util::{remove_parens, CollectToResult, PathEndsWith};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{
|
||||
parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, ItemTrait, LitStr, Meta, NestedMeta, PredicateType,
|
||||
Result, TraitItem, WherePredicate
|
||||
};
|
||||
|
||||
struct TraitItemAttrs {
|
||||
openapi_only: bool,
|
||||
openapi_bound: Vec<PredicateType>,
|
||||
non_openapi_bound: Vec<PredicateType>,
|
||||
other_attrs: Vec<Attribute>
|
||||
}
|
||||
|
||||
impl TraitItemAttrs {
|
||||
fn parse(attrs: Vec<Attribute>) -> Result<Self> {
|
||||
let mut openapi_only = false;
|
||||
let mut openapi_bound = Vec::new();
|
||||
let mut non_openapi_bound = Vec::new();
|
||||
let mut other = Vec::new();
|
||||
|
||||
for attr in attrs {
|
||||
if attr.path.ends_with("openapi_only") {
|
||||
openapi_only = true;
|
||||
} else if attr.path.ends_with("openapi_bound") {
|
||||
let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?;
|
||||
let predicate = attr_arg.parse_with(WherePredicate::parse)?;
|
||||
openapi_bound.push(match predicate {
|
||||
WherePredicate::Type(ty) => ty,
|
||||
_ => return Err(Error::new(predicate.span(), "Expected type bound"))
|
||||
});
|
||||
} else if attr.path.ends_with("non_openapi_bound") {
|
||||
let attr_arg: LitStr = syn::parse2(remove_parens(attr.tokens))?;
|
||||
let predicate = attr_arg.parse_with(WherePredicate::parse)?;
|
||||
non_openapi_bound.push(match predicate {
|
||||
WherePredicate::Type(ty) => ty,
|
||||
_ => return Err(Error::new(predicate.span(), "Expected type bound"))
|
||||
});
|
||||
} else {
|
||||
other.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
openapi_only,
|
||||
openapi_bound,
|
||||
non_openapi_bound,
|
||||
other_attrs: other
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_private_openapi_trait(mut attrs: AttributeArgs, tr8: ItemTrait) -> Result<TokenStream> {
|
||||
let tr8_attrs = &tr8.attrs;
|
||||
let vis = &tr8.vis;
|
||||
let ident = &tr8.ident;
|
||||
let generics = &tr8.generics;
|
||||
let colon_token = &tr8.colon_token;
|
||||
let supertraits = &tr8.supertraits;
|
||||
|
||||
if attrs.len() != 1 {
|
||||
return Err(Error::new(
|
||||
Span::call_site(),
|
||||
"Expected one argument. Example: #[_private_openapi_trait(OpenapiTraitName)]"
|
||||
));
|
||||
}
|
||||
let openapi_ident = match attrs.remove(0) {
|
||||
NestedMeta::Meta(Meta::Path(path)) => path,
|
||||
p => {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"Expected name of the Resource struct this method belongs to"
|
||||
))
|
||||
},
|
||||
};
|
||||
|
||||
let orig_trait = {
|
||||
let items = tr8
|
||||
.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
Ok(match item {
|
||||
TraitItem::Method(mut method) => {
|
||||
let attrs = TraitItemAttrs::parse(method.attrs)?;
|
||||
method.attrs = attrs.other_attrs;
|
||||
for bound in attrs.non_openapi_bound {
|
||||
method
|
||||
.sig
|
||||
.generics
|
||||
.type_params_mut()
|
||||
.filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string())
|
||||
.for_each(|param| param.bounds.extend(bound.bounds.clone()));
|
||||
}
|
||||
if attrs.openapi_only {
|
||||
None
|
||||
} else {
|
||||
Some(TraitItem::Method(method))
|
||||
}
|
||||
},
|
||||
TraitItem::Type(mut ty) => {
|
||||
let attrs = TraitItemAttrs::parse(ty.attrs)?;
|
||||
ty.attrs = attrs.other_attrs;
|
||||
Some(TraitItem::Type(ty))
|
||||
},
|
||||
item => Some(item)
|
||||
})
|
||||
})
|
||||
.collect_to_result()?;
|
||||
quote! {
|
||||
#(#tr8_attrs)*
|
||||
#vis trait #ident #generics #colon_token #supertraits {
|
||||
#(#items)*
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let openapi_trait = if !cfg!(feature = "openapi") {
|
||||
None
|
||||
} else {
|
||||
let items = tr8
|
||||
.items
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
Ok(match item {
|
||||
TraitItem::Method(mut method) => {
|
||||
let attrs = TraitItemAttrs::parse(method.attrs)?;
|
||||
method.attrs = attrs.other_attrs;
|
||||
for bound in attrs.openapi_bound {
|
||||
method
|
||||
.sig
|
||||
.generics
|
||||
.type_params_mut()
|
||||
.filter(|param| param.ident.to_string() == bound.bounded_ty.to_token_stream().to_string())
|
||||
.for_each(|param| param.bounds.extend(bound.bounds.clone()));
|
||||
}
|
||||
TraitItem::Method(method)
|
||||
},
|
||||
TraitItem::Type(mut ty) => {
|
||||
let attrs = TraitItemAttrs::parse(ty.attrs)?;
|
||||
ty.attrs = attrs.other_attrs;
|
||||
for bound in attrs.openapi_bound {
|
||||
ty.bounds.extend(bound.bounds.clone());
|
||||
}
|
||||
TraitItem::Type(ty)
|
||||
},
|
||||
item => item
|
||||
})
|
||||
})
|
||||
.collect_to_result()?;
|
||||
Some(quote! {
|
||||
#(#tr8_attrs)*
|
||||
#vis trait #openapi_ident #generics #colon_token #supertraits {
|
||||
#(#items)*
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#orig_trait
|
||||
#openapi_trait
|
||||
})
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
use crate::{method::Method, util::CollectToResult};
|
||||
use crate::{
|
||||
endpoint::endpoint_ident,
|
||||
util::{CollectToResult, PathEndsWith}
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use std::{iter, str::FromStr};
|
||||
use std::iter;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
DeriveInput, Error, Result, Token
|
||||
DeriveInput, Result, Token
|
||||
};
|
||||
|
||||
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||
|
@ -23,29 +26,22 @@ impl Parse for MethodList {
|
|||
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let ident = input.ident;
|
||||
let name = ident.to_string();
|
||||
|
||||
let methods =
|
||||
input
|
||||
let methods = input
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter(
|
||||
|attr| {
|
||||
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string())
|
||||
} // TODO wtf
|
||||
)
|
||||
.filter(|attr| attr.path.ends_with("resource"))
|
||||
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||
.flat_map(|list| match list {
|
||||
Ok(iter) => Box::new(iter.map(|method| {
|
||||
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
|
||||
let ident = method.setup_ident(&name);
|
||||
Ok(quote!(#ident(&mut route);))
|
||||
let ident = endpoint_ident(&method);
|
||||
Ok(quote!(route.endpoint::<#ident>();))
|
||||
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||
Err(err) => Box::new(iter::once(Err(err)))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
let non_openapi_impl = quote! {
|
||||
impl #krate::Resource for #ident
|
||||
{
|
||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||
|
@ -53,5 +49,22 @@ pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
|||
#(#methods)*
|
||||
}
|
||||
}
|
||||
};
|
||||
let openapi_impl = if !cfg!(feature = "openapi") {
|
||||
None
|
||||
} else {
|
||||
Some(quote! {
|
||||
impl #krate::ResourceWithSchema for #ident
|
||||
{
|
||||
fn setup<D : #krate::DrawResourceRoutesWithSchema>(mut route : D)
|
||||
{
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok(quote! {
|
||||
#non_openapi_impl
|
||||
#openapi_impl
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||
use std::iter;
|
||||
use syn::Error;
|
||||
use syn::{Error, Path};
|
||||
|
||||
pub trait CollectToResult {
|
||||
type Item;
|
||||
|
@ -30,6 +30,16 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PathEndsWith {
|
||||
fn ends_with(&self, s: &str) -> bool;
|
||||
}
|
||||
|
||||
impl PathEndsWith for Path {
|
||||
fn ends_with(&self, s: &str) -> bool {
|
||||
self.segments.last().map(|segment| segment.ident.to_string()).as_deref() == Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_parens(input: TokenStream) -> TokenStream {
|
||||
let iter = input.into_iter().flat_map(|tt| {
|
||||
if let TokenTree::Group(group) = &tt {
|
||||
|
|
|
@ -18,7 +18,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
|||
fake = "2.2.2"
|
||||
gotham = { version = "0.5.0", default-features = false }
|
||||
gotham_derive = "0.5.0"
|
||||
gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"] }
|
||||
gotham_restful = { version = "0.2.0-dev", features = ["auth", "openapi"], default-features = false }
|
||||
log = "0.4.8"
|
||||
pretty_env_logger = "0.4"
|
||||
serde = "1.0.110"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
131
src/lib.rs
131
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,12 +27,12 @@ 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 |
|
||||
| ----------- | ------------------ | --------- | ----------- |
|
||||
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ------------- | ------------------ | --------- | -------------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
|
@ -42,8 +42,8 @@ Assuming you assign `/foobar` to your resource, you can implement the following
|
|||
| 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>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read_all::<Handler>()
|
||||
fn 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);
|
||||
}
|
||||
|
||||
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>();
|
||||
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 path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let op = descr.into_operation();
|
||||
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()
|
||||
);
|
||||
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::<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()
|
||||
}
|
||||
|
|
344
src/routing.rs
344
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) {
|
||||
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));
|
||||
debug!("Missing Content-Type: Returning 415 Response");
|
||||
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
|
||||
let res = {
|
||||
let body = match B::from_body(body, content_type) {
|
||||
Ok(body) => body,
|
||||
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 res = match serde_json::to_string(&error) {
|
||||
Ok(json) => {
|
||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
Ok(res)
|
||||
let json = serde_json::to_string(&error)?;
|
||||
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e.into())
|
||||
};
|
||||
return (state, res);
|
||||
}
|
||||
};
|
||||
get_result(state, body)
|
||||
false => None
|
||||
};
|
||||
|
||||
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())
|
||||
};
|
||||
(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);
|
||||
if E::http_method() != Method::GET {
|
||||
assoc
|
||||
.options()
|
||||
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
|
||||
.to(crate::cors::cors_preflight_handler);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
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 {}
|
||||
|
|
|
@ -30,55 +30,57 @@ struct FooSearch {
|
|||
}
|
||||
|
||||
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
async fn read_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||
#[read(FooResource)]
|
||||
#[read]
|
||||
async fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||
#[search(FooResource)]
|
||||
#[search]
|
||||
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||
#[create(FooResource)]
|
||||
#[create]
|
||||
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||
#[change_all(FooResource)]
|
||||
#[change_all]
|
||||
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||
#[change(FooResource)]
|
||||
#[change]
|
||||
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||
#[remove_all(FooResource)]
|
||||
#[remove_all]
|
||||
async fn remove_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||
#[remove(FooResource)]
|
||||
#[remove]
|
||||
async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_methods() {
|
||||
let _ = pretty_env_logger::try_init_timed();
|
||||
|
||||
let server = TestServer::new(build_simple_router(|router| {
|
||||
router.resource::<FooResource>("foo");
|
||||
}))
|
||||
|
|
|
@ -16,10 +16,10 @@ use mime::TEXT_PLAIN;
|
|||
#[resource(read_all, change_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() {}
|
||||
|
||||
#[change_all(FooResource)]
|
||||
#[change_all]
|
||||
fn change_all(_body: Raw<Vec<u8>>) {}
|
||||
|
||||
fn test_server(cfg: CorsConfig) -> TestServer {
|
||||
|
|
|
@ -15,7 +15,7 @@ struct Foo {
|
|||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(FooResource)]
|
||||
#[create]
|
||||
fn create(body: Foo) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
|
|
|
@ -22,23 +22,23 @@ use util::{test_get_response, test_openapi_response};
|
|||
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read, change)]
|
||||
#[resource(get_image, set_image)]
|
||||
struct ImageResource;
|
||||
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(IMAGE_PNG)]
|
||||
struct Image(Vec<u8>);
|
||||
|
||||
#[read(ImageResource, operation_id = "getImage")]
|
||||
#[read(operation_id = "getImage")]
|
||||
fn get_image(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
||||
}
|
||||
|
||||
#[change(ImageResource, operation_id = "setImage")]
|
||||
#[change(operation_id = "setImage")]
|
||||
fn set_image(_id: u64, _image: Image) {}
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read, search)]
|
||||
#[resource(read_secret, search_secret)]
|
||||
struct SecretResource;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
|
@ -67,13 +67,13 @@ struct SecretQuery {
|
|||
minute: Option<u16>
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
#[read]
|
||||
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
|
||||
auth.ok()?;
|
||||
Ok(Secret { code: 4.2 })
|
||||
}
|
||||
|
||||
#[search(SecretResource)]
|
||||
#[search]
|
||||
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
|
||||
auth.ok()?;
|
||||
Ok(Secrets {
|
||||
|
@ -82,7 +82,7 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets>
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn openapi_supports_scope() {
|
||||
fn openapi_specification() {
|
||||
let info = OpenapiInfo {
|
||||
title: "This is just a test".to_owned(),
|
||||
version: "1.2.3".to_owned(),
|
||||
|
|
|
@ -15,7 +15,7 @@ const RESPONSE: &[u8] = b"This is the only valid response.";
|
|||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
|
|
@ -30,55 +30,57 @@ struct FooSearch {
|
|||
}
|
||||
|
||||
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||
#[read(FooResource)]
|
||||
#[read]
|
||||
fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||
#[search(FooResource)]
|
||||
#[search]
|
||||
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||
#[create(FooResource)]
|
||||
#[create]
|
||||
fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||
#[change_all(FooResource)]
|
||||
#[change_all]
|
||||
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||
#[change(FooResource)]
|
||||
#[change]
|
||||
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||
#[remove_all(FooResource)]
|
||||
#[remove_all]
|
||||
fn remove_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||
#[remove(FooResource)]
|
||||
#[remove]
|
||||
fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_methods() {
|
||||
let _ = pretty_env_logger::try_init_timed();
|
||||
|
||||
let server = TestServer::new(build_simple_router(|router| {
|
||||
router.resource::<FooResource>("foo");
|
||||
}))
|
||||
|
|
|
@ -6,23 +6,12 @@ fn trybuild_ui() {
|
|||
let t = TestCases::new();
|
||||
|
||||
// always enabled
|
||||
t.compile_fail("tests/ui/from_body_enum.rs");
|
||||
t.compile_fail("tests/ui/method_async_state.rs");
|
||||
t.compile_fail("tests/ui/method_for_unknown_resource.rs");
|
||||
t.compile_fail("tests/ui/method_no_resource.rs");
|
||||
t.compile_fail("tests/ui/method_self.rs");
|
||||
t.compile_fail("tests/ui/method_too_few_args.rs");
|
||||
t.compile_fail("tests/ui/method_too_many_args.rs");
|
||||
t.compile_fail("tests/ui/method_unsafe.rs");
|
||||
t.compile_fail("tests/ui/resource_unknown_method.rs");
|
||||
t.compile_fail("tests/ui/endpoint/*.rs");
|
||||
t.compile_fail("tests/ui/from_body/*.rs");
|
||||
t.compile_fail("tests/ui/resource/*.rs");
|
||||
|
||||
// require the openapi feature
|
||||
if cfg!(feature = "openapi") {
|
||||
t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_nullable_non_bool.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_rename_non_string.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_tuple_struct.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_union.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_unknown_key.rs");
|
||||
t.compile_fail("tests/ui/openapi_type/*.rs");
|
||||
}
|
||||
}
|
||||
|
|
12
tests/ui/endpoint/async_state.rs
Normal file
12
tests/ui/endpoint/async_state.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
use gotham_restful::State;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all]
|
||||
async fn read_all(state: &State) {}
|
||||
|
||||
fn main() {}
|
5
tests/ui/endpoint/async_state.stderr
Normal file
5
tests/ui/endpoint/async_state.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State`
|
||||
--> $DIR/async_state.rs:10:19
|
||||
|
|
||||
10 | async fn read_all(state: &State) {}
|
||||
| ^^^^^
|
|
@ -1,14 +1,11 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all(self)
|
||||
{
|
||||
}
|
||||
fn read_all() {}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
||||
fn main() {}
|
11
tests/ui/endpoint/invalid_attribute.stderr
Normal file
11
tests/ui/endpoint/invalid_attribute.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: Invalid attribute syntax
|
||||
--> $DIR/invalid_attribute.rs:8:12
|
||||
|
|
||||
8 | #[read_all(FooResource)]
|
||||
| ^^^^^^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/invalid_attribute.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
|
@ -1,14 +1,11 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all]
|
||||
fn read_all()
|
||||
{
|
||||
}
|
||||
fn read_all(self) {}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
||||
fn main() {}
|
19
tests/ui/endpoint/self.stderr
Normal file
19
tests/ui/endpoint/self.stderr
Normal file
|
@ -0,0 +1,19 @@
|
|||
error: Didn't expect self parameter
|
||||
--> $DIR/self.rs:9:13
|
||||
|
|
||||
9 | fn read_all(self) {}
|
||||
| ^^^^
|
||||
|
||||
error: `self` parameter is only allowed in associated functions
|
||||
--> $DIR/self.rs:9:13
|
||||
|
|
||||
9 | fn read_all(self) {}
|
||||
| ^^^^ not semantically valid as function parameter
|
||||
|
|
||||
= note: associated functions are those in `impl` or `trait` definitions
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/self.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/too_few_args.rs
Normal file
11
tests/ui/endpoint/too_few_args.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read)]
|
||||
struct FooResource;
|
||||
|
||||
#[read]
|
||||
fn read() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/too_few_args.stderr
Normal file
11
tests/ui/endpoint/too_few_args.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: Too few arguments
|
||||
--> $DIR/too_few_args.rs:9:4
|
||||
|
|
||||
9 | fn read() {}
|
||||
| ^^^^
|
||||
|
||||
error[E0412]: cannot find type `read___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/too_few_args.rs:5:12
|
||||
|
|
||||
5 | #[resource(read)]
|
||||
| ^^^^ not found in this scope
|
11
tests/ui/endpoint/too_many_args.rs
Normal file
11
tests/ui/endpoint/too_many_args.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all]
|
||||
fn read_all(_id: u64) {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/too_many_args.stderr
Normal file
11
tests/ui/endpoint/too_many_args.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: Too many arguments
|
||||
--> $DIR/too_many_args.rs:9:4
|
||||
|
|
||||
9 | fn read_all(_id: u64) {}
|
||||
| ^^^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/too_many_args.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/unknown_attribute.rs
Normal file
11
tests/ui/endpoint/unknown_attribute.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(pineapple = "on pizza")]
|
||||
fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/unknown_attribute.stderr
Normal file
11
tests/ui/endpoint/unknown_attribute.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: Unknown attribute
|
||||
--> $DIR/unknown_attribute.rs:8:12
|
||||
|
|
||||
8 | #[read_all(pineapple = "on pizza")]
|
||||
| ^^^^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/unknown_attribute.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/unsafe.rs
Normal file
11
tests/ui/endpoint/unsafe.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all]
|
||||
unsafe fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/unsafe.stderr
Normal file
11
tests/ui/endpoint/unsafe.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: Endpoint handler methods must not be unsafe
|
||||
--> $DIR/unsafe.rs:9:1
|
||||
|
|
||||
9 | unsafe fn read_all() {}
|
||||
| ^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/unsafe.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
10
tests/ui/from_body/enum.rs
Normal file
10
tests/ui/from_body/enum.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(FromBody)]
|
||||
enum FromBodyEnum {
|
||||
SomeVariant(Vec<u8>),
|
||||
OtherVariant(String)
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +1,5 @@
|
|||
error: #[derive(FromBody)] only works for structs
|
||||
--> $DIR/from_body_enum.rs:4:1
|
||||
--> $DIR/enum.rs:5:1
|
||||
|
|
||||
4 | enum FromBodyEnum
|
||||
5 | enum FromBodyEnum {
|
||||
| ^^^^
|
|
@ -1,12 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(FromBody)]
|
||||
enum FromBodyEnum
|
||||
{
|
||||
SomeVariant(Vec<u8>),
|
||||
OtherVariant(String)
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
use gotham_restful::State;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
async fn read_all(state : &State)
|
||||
{
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error: async fn must not take &State as an argument as State is not Sync, consider taking &mut State
|
||||
--> $DIR/method_async_state.rs:9:19
|
||||
|
|
||||
9 | async fn read_all(state : &State)
|
||||
| ^^^^^
|
|
@ -1,10 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[read_all(UnknownResource)]
|
||||
fn read_all()
|
||||
{
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error[E0412]: cannot find type `UnknownResource` in this scope
|
||||
--> $DIR/method_for_unknown_resource.rs:3:12
|
||||
|
|
||||
3 | #[read_all(UnknownResource)]
|
||||
| ^^^^^^^^^^^^^^^ not found in this scope
|
|
@ -1,15 +0,0 @@
|
|||
error: Missing Resource struct. Example: #[read_all(MyResource)]
|
||||
--> $DIR/method_no_resource.rs:7:1
|
||||
|
|
||||
7 | #[read_all]
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0425]: cannot find function `_gotham_restful_foo_resource_read_all_setup_impl` in this scope
|
||||
--> $DIR/method_no_resource.rs:3:10
|
||||
|
|
||||
3 | #[derive(Resource)]
|
||||
| ^^^^^^^^ not found in this scope
|
||||
|
|
||||
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
@ -1,13 +0,0 @@
|
|||
error: Didn't expect self parameter
|
||||
--> $DIR/method_self.rs:8:13
|
||||
|
|
||||
8 | fn read_all(self)
|
||||
| ^^^^
|
||||
|
||||
error: `self` parameter is only allowed in associated functions
|
||||
--> $DIR/method_self.rs:8:13
|
||||
|
|
||||
8 | fn read_all(self)
|
||||
| ^^^^ not semantically valid as function parameter
|
||||
|
|
||||
= note: associated functions are those in `impl` or `trait` definitions
|
|
@ -1,14 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read)]
|
||||
struct FooResource;
|
||||
|
||||
#[read(FooResource)]
|
||||
fn read()
|
||||
{
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error: Too few arguments
|
||||
--> $DIR/method_too_few_args.rs:8:4
|
||||
|
|
||||
8 | fn read()
|
||||
| ^^^^
|
|
@ -1,14 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all(_id : u64)
|
||||
{
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error: Too many arguments
|
||||
--> $DIR/method_too_many_args.rs:8:13
|
||||
|
|
||||
8 | fn read_all(_id : u64)
|
||||
| ^^^
|
|
@ -1,14 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
unsafe fn read_all()
|
||||
{
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
error: Resource methods must not be unsafe
|
||||
--> $DIR/method_unsafe.rs:8:1
|
||||
|
|
||||
8 | unsafe fn read_all()
|
||||
| ^^^^^^
|
12
tests/ui/openapi_type/enum_with_fields.rs
Normal file
12
tests/ui/openapi_type/enum_with_fields.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Food {
|
||||
Pasta,
|
||||
Pizza { pineapple: bool },
|
||||
Rice,
|
||||
Other(String)
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,11 +1,11 @@
|
|||
error: #[derive(OpenapiType)] does not support enum variants with fields
|
||||
--> $DIR/openapi_type_enum_with_fields.rs:7:2
|
||||
--> $DIR/enum_with_fields.rs:7:2
|
||||
|
|
||||
7 | Pizza { pineapple: bool },
|
||||
| ^^^^^
|
||||
|
||||
error: #[derive(OpenapiType)] does not support enum variants with fields
|
||||
--> $DIR/openapi_type_enum_with_fields.rs:9:2
|
||||
--> $DIR/enum_with_fields.rs:9:2
|
||||
|
|
||||
9 | Other(String)
|
||||
| ^^^^^
|
10
tests/ui/openapi_type/nullable_non_bool.rs
Normal file
10
tests/ui/openapi_type/nullable_non_bool.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
#[openapi(nullable = "yes, please")]
|
||||
bar: String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +1,5 @@
|
|||
error: Expected bool
|
||||
--> $DIR/openapi_type_nullable_non_bool.rs:6:23
|
||||
--> $DIR/nullable_non_bool.rs:6:23
|
||||
|
|
||||
6 | #[openapi(nullable = "yes, please")]
|
||||
| ^^^^^^^^^^^^^
|
10
tests/ui/openapi_type/rename_non_string.rs
Normal file
10
tests/ui/openapi_type/rename_non_string.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
#[openapi(rename = 42)]
|
||||
bar: String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +1,5 @@
|
|||
error: Expected string literal
|
||||
--> $DIR/openapi_type_rename_non_string.rs:6:21
|
||||
--> $DIR/rename_non_string.rs:6:21
|
||||
|
|
||||
6 | #[openapi(rename = 42)]
|
||||
| ^^
|
7
tests/ui/openapi_type/tuple_struct.rs
Normal file
7
tests/ui/openapi_type/tuple_struct.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo(String);
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +1,5 @@
|
|||
error: #[derive(OpenapiType)] does not support unnamed fields
|
||||
--> $DIR/openapi_type_tuple_struct.rs:4:11
|
||||
--> $DIR/tuple_struct.rs:5:11
|
||||
|
|
||||
4 | struct Foo(String);
|
||||
5 | struct Foo(String);
|
||||
| ^^^^^^^^
|
10
tests/ui/openapi_type/union.rs
Normal file
10
tests/ui/openapi_type/union.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
union IntOrPointer {
|
||||
int: u64,
|
||||
pointer: *mut String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +1,5 @@
|
|||
error: #[derive(OpenapiType)] only works for structs and enums
|
||||
--> $DIR/openapi_type_union.rs:4:1
|
||||
--> $DIR/union.rs:5:1
|
||||
|
|
||||
4 | union IntOrPointer
|
||||
5 | union IntOrPointer {
|
||||
| ^^^^^
|
10
tests/ui/openapi_type/unknown_key.rs
Normal file
10
tests/ui/openapi_type/unknown_key.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
#[openapi(like = "pizza")]
|
||||
bar: String
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,5 +1,5 @@
|
|||
error: Unknown key
|
||||
--> $DIR/openapi_type_unknown_key.rs:6:12
|
||||
--> $DIR/unknown_key.rs:6:12
|
||||
|
|
||||
6 | #[openapi(like = "pizza")]
|
||||
| ^^^^
|
|
@ -1,14 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Food
|
||||
{
|
||||
Pasta,
|
||||
Pizza { pineapple : bool },
|
||||
Rice,
|
||||
Other(String)
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo
|
||||
{
|
||||
#[openapi(nullable = "yes, please")]
|
||||
bar : String
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo
|
||||
{
|
||||
#[openapi(rename = 42)]
|
||||
bar : String
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo(String);
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
union IntOrPointer
|
||||
{
|
||||
int: u64,
|
||||
pointer: *mut String
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo
|
||||
{
|
||||
#[openapi(like = "pizza")]
|
||||
bar : String
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
11
tests/ui/resource/unknown_method.rs
Normal file
11
tests/ui/resource/unknown_method.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_any)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all]
|
||||
fn read_all() {}
|
||||
|
||||
fn main() {}
|
8
tests/ui/resource/unknown_method.stderr
Normal file
8
tests/ui/resource/unknown_method.stderr
Normal file
|
@ -0,0 +1,8 @@
|
|||
error[E0412]: cannot find type `read_any___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/unknown_method.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_any)]
|
||||
| ^^^^^^^^ help: a struct with a similar name exists: `read_all___gotham_restful_endpoint`
|
||||
...
|
||||
8 | #[read_all]
|
||||
| ----------- similarly named struct `read_all___gotham_restful_endpoint` defined here
|
|
@ -1,14 +0,0 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_any)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all()
|
||||
{
|
||||
}
|
||||
|
||||
fn main()
|
||||
{
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
error: Unknown method: `read_any'
|
||||
--> $DIR/resource_unknown_method.rs:4:12
|
||||
|
|
||||
4 | #[resource(read_any)]
|
||||
| ^^^^^^^^
|
||||
|
||||
error[E0277]: the trait bound `FooResource: Resource` is not satisfied
|
||||
--> $DIR/resource_unknown_method.rs:7:1
|
||||
|
|
||||
7 | #[read_all(FooResource)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Resource` is not implemented for `FooResource`
|
||||
|
|
||||
= help: see issue #48214
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
15
tests/ui/rustfmt.sh
Executable file
15
tests/ui/rustfmt.sh
Executable file
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
rustfmt=${RUSTFMT:-rustfmt}
|
||||
version="$($rustfmt -V)"
|
||||
if [[ $version != *nightly* ]]; then
|
||||
rustfmt="$rustfmt +nightly"
|
||||
fi
|
||||
|
||||
return=0
|
||||
find "$(dirname "$0")" -name '*.rs' -type f | while read file; do
|
||||
$rustfmt --config-path "$(dirname "$0")/../../rustfmt.toml" "$@" "$file" || return=1
|
||||
done
|
||||
|
||||
exit $return
|
|
@ -2,12 +2,14 @@ use gotham::{
|
|||
hyper::Body,
|
||||
test::TestServer
|
||||
};
|
||||
use log::info;
|
||||
use mime::Mime;
|
||||
#[allow(unused_imports)]
|
||||
use std::{fs::File, io::{Read, Write}, str};
|
||||
|
||||
pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8])
|
||||
{
|
||||
info!("GET {}", path);
|
||||
let res = server.client().get(path).perform().unwrap().read_body().unwrap();
|
||||
let body : &[u8] = res.as_ref();
|
||||
assert_eq!(body, expected);
|
||||
|
@ -17,6 +19,7 @@ pub fn test_post_response<B>(server : &TestServer, path : &str, body : B, mime :
|
|||
where
|
||||
B : Into<Body>
|
||||
{
|
||||
info!("POST {}", path);
|
||||
let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap();
|
||||
let body : &[u8] = res.as_ref();
|
||||
assert_eq!(body, expected);
|
||||
|
@ -26,6 +29,7 @@ pub fn test_put_response<B>(server : &TestServer, path : &str, body : B, mime :
|
|||
where
|
||||
B : Into<Body>
|
||||
{
|
||||
info!("PUT {}", path);
|
||||
let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap();
|
||||
let body : &[u8] = res.as_ref();
|
||||
assert_eq!(body, expected);
|
||||
|
@ -33,6 +37,7 @@ where
|
|||
|
||||
pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
|
||||
{
|
||||
info!("DELETE {}", path);
|
||||
let res = server.client().delete(path).perform().unwrap().read_body().unwrap();
|
||||
let body : &[u8] = res.as_ref();
|
||||
assert_eq!(body, expected);
|
||||
|
@ -41,12 +46,14 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
|
|||
#[cfg(feature = "openapi")]
|
||||
pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str)
|
||||
{
|
||||
info!("GET {}", path);
|
||||
let res = server.client().get(path).perform().unwrap().read_body().unwrap();
|
||||
let body = serde_json::to_string_pretty(&serde_json::from_slice::<serde_json::Value>(res.as_ref()).unwrap()).unwrap();
|
||||
match File::open(output_file) {
|
||||
Ok(mut file) => {
|
||||
let mut expected = String::new();
|
||||
file.read_to_string(&mut expected).unwrap();
|
||||
eprintln!("{}", body);
|
||||
assert_eq!(body, expected);
|
||||
},
|
||||
Err(_) => {
|
||||
|
|
Loading…
Add table
Reference in a new issue