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:
|
variables:
|
||||||
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||||
|
RUST_LOG: info,gotham=debug,gotham_restful=trace
|
||||||
|
|
||||||
test-default:
|
test-default:
|
||||||
stage: test
|
stage: test
|
||||||
image: rust:1.42-slim
|
image: rust:1.43-slim
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo test
|
- cargo test
|
||||||
cache:
|
cache:
|
||||||
key: cargo-1-42-default
|
key: cargo-1-43-default
|
||||||
paths:
|
paths:
|
||||||
- cargo/
|
- cargo/
|
||||||
- target/
|
- target/
|
||||||
|
|
||||||
test-full:
|
test-full:
|
||||||
stage: test
|
stage: test
|
||||||
image: rust:1.42-slim
|
image: rust:1.43-slim
|
||||||
before_script:
|
before_script:
|
||||||
- apt update -y
|
- apt update -y
|
||||||
- apt install -y --no-install-recommends libpq-dev
|
- apt install -y --no-install-recommends libpq-dev
|
||||||
|
@ -30,7 +31,7 @@ test-full:
|
||||||
script:
|
script:
|
||||||
- cargo test --no-default-features --features full
|
- cargo test --no-default-features --features full
|
||||||
cache:
|
cache:
|
||||||
key: cargo-1-42-all
|
key: cargo-1-43-all
|
||||||
paths:
|
paths:
|
||||||
- cargo/
|
- cargo/
|
||||||
- target/
|
- target/
|
||||||
|
@ -86,6 +87,7 @@ rustfmt:
|
||||||
- cargo fmt --version
|
- cargo fmt --version
|
||||||
script:
|
script:
|
||||||
- cargo fmt -- --check
|
- cargo fmt -- --check
|
||||||
|
- ./tests/ui/rustfmt.sh --check
|
||||||
|
|
||||||
doc:
|
doc:
|
||||||
stage: build
|
stage: build
|
||||||
|
|
|
@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Support custom HTTP response headers
|
- Support custom HTTP response headers
|
||||||
|
- New `endpoint` router extension with associated `Endpoint` trait ([!18])
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- The cors handler can now copy headers from the request if desired
|
- The cors handler can now copy headers from the request if desired
|
||||||
- All fields of `Response` are now private
|
- All fields of `Response` are now private
|
||||||
- If not enabling the `openapi` feature, `without-openapi` has to be enabled
|
- 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
|
## [0.1.1] - 2020-12-28
|
||||||
### Added
|
### 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
|
## [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.
|
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 }
|
jsonwebtoken = { version = "7.1.0", optional = true }
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
|
once_cell = { version = "1.5", optional = true }
|
||||||
openapiv3 = { version = "0.3.2", optional = true }
|
openapiv3 = { version = "0.3.2", optional = true }
|
||||||
|
regex = { version = "1.4", optional = true }
|
||||||
serde = { version = "1.0.110", features = ["derive"] }
|
serde = { version = "1.0.110", features = ["derive"] }
|
||||||
serde_json = "1.0.58"
|
serde_json = "1.0.58"
|
||||||
uuid = { version = "0.8.1", optional = true }
|
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"] }
|
diesel = { version = "1.4.4", features = ["postgres"] }
|
||||||
futures-executor = "0.3.5"
|
futures-executor = "0.3.5"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
thiserror = "1.0.18"
|
thiserror = "1.0.18"
|
||||||
trybuild = "1.0.27"
|
trybuild = "1.0.27"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cors", "errorlog", "without-openapi"]
|
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"]
|
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
||||||
cors = []
|
cors = []
|
||||||
|
@ -56,7 +59,7 @@ errorlog = []
|
||||||
|
|
||||||
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
|
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
|
||||||
without-openapi = []
|
without-openapi = []
|
||||||
openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"]
|
openapi = ["gotham_restful_derive/openapi", "indexmap", "once_cell", "openapiv3", "regex"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
no-default-features = true
|
no-default-features = true
|
||||||
|
|
113
README.md
113
README.md
|
@ -17,8 +17,8 @@
|
||||||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
<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"/>
|
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
<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.42+-orange.svg"/>
|
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.43+-orange.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
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.
|
for requests.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -45,23 +45,23 @@ for requests.
|
||||||
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
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.
|
`#![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_all | | GET | /foobar |
|
||||||
| read | id | GET | /foobar/:id |
|
| read | id | GET | /foobar/:id |
|
||||||
| search | query | GET | /foobar/search |
|
| search | query | GET | /foobar/search |
|
||||||
| create | body | POST | /foobar |
|
| create | body | POST | /foobar |
|
||||||
| change_all | body | PUT | /foobar |
|
| change_all | body | PUT | /foobar |
|
||||||
| change | id, body | PUT | /foobar/:id |
|
| change | id, body | PUT | /foobar/:id |
|
||||||
| remove_all | | DELETE | /foobar |
|
| remove_all | | DELETE | /foobar |
|
||||||
| remove | id | DELETE | /foobar/:id |
|
| remove | id | DELETE | /foobar/:id |
|
||||||
|
|
||||||
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
|
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
|
||||||
simple example could look like this:
|
simple example looks like this:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
/// Our RESTful resource.
|
/// Our RESTful resource.
|
||||||
|
@ -69,14 +69,14 @@ simple example could look like this:
|
||||||
#[resource(read)]
|
#[resource(read)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
/// The return type of the foo read method.
|
/// The return type of the foo read endpoint.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Foo {
|
struct Foo {
|
||||||
id: u64
|
id: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The foo read method handler.
|
/// The foo read endpoint.
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
fn read(id: u64) -> Success<Foo> {
|
fn read(id: u64) -> Success<Foo> {
|
||||||
Foo { id }.into()
|
Foo { id }.into()
|
||||||
}
|
}
|
||||||
|
@ -84,17 +84,16 @@ fn read(id: u64) -> Success<Foo> {
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
|
|
||||||
Some methods require arguments. Those should be
|
Some endpoints require arguments. Those should be
|
||||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
|
||||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
* **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
|
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
|
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
|
||||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
|
||||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
improve.
|
||||||
`Sync` there is unfortunately no more convenient way.
|
|
||||||
|
|
||||||
## Uploads and Downloads
|
## Uploads and Downloads
|
||||||
|
|
||||||
|
@ -114,7 +113,7 @@ struct RawImage {
|
||||||
content_type: Mime
|
content_type: Mime
|
||||||
}
|
}
|
||||||
|
|
||||||
#[create(ImageResource)]
|
#[create]
|
||||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||||
Raw::new(body.content, body.content_type)
|
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
|
when you implement your web server. The complete feature list is
|
||||||
- [`auth`](#authentication-feature) Advanced JWT middleware
|
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||||
- `chrono` openapi support for chrono types
|
- `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
|
- [`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
|
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
|
||||||
- `uuid` openapi support for uuid
|
- `uuid` openapi support for uuid
|
||||||
|
- `without-openapi` (**default**) disables `openapi` support.
|
||||||
|
|
||||||
### Authentication Feature
|
### Authentication Feature
|
||||||
|
|
||||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
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
|
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.
|
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.
|
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
|
```rust
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
|
@ -159,7 +160,7 @@ struct AuthData {
|
||||||
exp: u64
|
exp: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read(SecretResource)]
|
#[read]
|
||||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||||
let intended_for = auth.ok()?.sub;
|
let intended_for = auth.ok()?.sub;
|
||||||
Ok(Secret { id, intended_for })
|
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.
|
configuration as a middleware.
|
||||||
|
|
||||||
A simple example that allows authentication from every origin (note that `*` always disallows
|
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
|
```rust
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() {
|
fn read_all() {
|
||||||
// your handler
|
// 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,
|
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.
|
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
|
```rust
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
|
@ -234,7 +235,7 @@ struct Foo {
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||||
foo::table.load(conn)
|
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
|
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
|
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
|
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
|
```rust
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
|
@ -273,7 +274,7 @@ struct Foo {
|
||||||
bar: String
|
bar: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() -> Success<Foo> {
|
fn read_all() -> Success<Foo> {
|
||||||
Foo { bar: "Hello World".to_owned() }.into()
|
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`
|
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
|
It will return the generated openapi specification in JSON format. This allows you to easily write
|
||||||
in different languages without worying to exactly replicate your api in each of those languages.
|
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,
|
However, please note that by default, the `without-openapi` feature of this crate is enabled.
|
||||||
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
|
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
|
||||||
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
|
on some of the types in [`Endpoint`] and related traits. This means that some code might only
|
||||||
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
|
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
|
||||||
In other words, put
|
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:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
|
@ -318,18 +314,15 @@ struct Foo;
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
This readme and the crate documentation contain some of example. In addition to that, there is
|
||||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
a collection of code in the [example] directory that might help you. Any help writing more
|
||||||
|
examples is highly appreciated.
|
||||||
|
|
||||||
|
|
||||||
[diesel]: https://diesel.rs/
|
[diesel]: https://diesel.rs/
|
||||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||||
[gotham]: https://gotham.rs/
|
[gotham]: https://gotham.rs/
|
||||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
[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
|
## Versioning
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
<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"/>
|
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
<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.42+-orange.svg"/>
|
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.43+-orange.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
<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,24 +5,32 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re
|
||||||
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
mod endpoint;
|
||||||
|
use endpoint::{expand_endpoint, EndpointType};
|
||||||
|
|
||||||
mod from_body;
|
mod from_body;
|
||||||
use from_body::expand_from_body;
|
use from_body::expand_from_body;
|
||||||
mod method;
|
|
||||||
use method::{expand_method, Method};
|
|
||||||
mod request_body;
|
mod request_body;
|
||||||
use request_body::expand_request_body;
|
use request_body::expand_request_body;
|
||||||
|
|
||||||
mod resource;
|
mod resource;
|
||||||
use resource::expand_resource;
|
use resource::expand_resource;
|
||||||
|
|
||||||
mod resource_error;
|
mod resource_error;
|
||||||
use resource_error::expand_resource_error;
|
use resource_error::expand_resource_error;
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
mod openapi_type;
|
mod openapi_type;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use openapi_type::expand_openapi_type;
|
use openapi_type::expand_openapi_type;
|
||||||
|
|
||||||
|
mod private_openapi_trait;
|
||||||
|
use private_openapi_trait::expand_private_openapi_trait;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
||||||
//eprintln!("{}", tokens);
|
// eprintln!("{}", tokens);
|
||||||
tokens.into()
|
tokens.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,40 +85,47 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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]
|
#[proc_macro_attribute]
|
||||||
pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream {
|
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 quote::quote;
|
||||||
use syn::{
|
use syn::{
|
||||||
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
|
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> {
|
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() {
|
if generics.params.is_empty() {
|
||||||
return quote!();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let krate = super::krate();
|
if where_clause.is_none() {
|
||||||
let idents = generics
|
*where_clause = Some(WhereClause {
|
||||||
.params
|
where_token: Default::default(),
|
||||||
.iter()
|
predicates: Default::default()
|
||||||
.map(|param| match param {
|
});
|
||||||
GenericParam::Type(ty) => Some(ty.ident.clone()),
|
}
|
||||||
_ => None
|
let where_clause = where_clause.as_mut().unwrap();
|
||||||
})
|
|
||||||
.filter(|param| param.is_some())
|
|
||||||
.map(|param| param.unwrap());
|
|
||||||
|
|
||||||
quote! {
|
for param in &generics.params {
|
||||||
where #(#idents : #krate::OpenapiType),*
|
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> {
|
fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
|
||||||
let krate = super::krate();
|
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 attrs = parse_attributes(&attrs)?;
|
||||||
let nullable = attrs.nullable;
|
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()?;
|
let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
impl #generics #krate::OpenapiType for #ident #generics
|
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
|
||||||
#where_clause
|
#where_clause
|
||||||
{
|
{
|
||||||
fn schema() -> #krate::OpenapiSchema
|
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> {
|
fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
|
||||||
let krate = super::krate();
|
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 attrs = parse_attributes(&attrs)?;
|
||||||
let nullable = attrs.nullable;
|
let nullable = attrs.nullable;
|
||||||
|
@ -229,7 +256,7 @@ fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input:
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
impl #generics #krate::OpenapiType for #ident #generics
|
impl #impl_generics #krate::OpenapiType for #ident #ty_generics
|
||||||
#where_clause
|
#where_clause
|
||||||
{
|
{
|
||||||
fn schema() -> #krate::OpenapiSchema
|
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 proc_macro2::{Ident, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use std::{iter, str::FromStr};
|
use std::iter;
|
||||||
use syn::{
|
use syn::{
|
||||||
parenthesized,
|
parenthesized,
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
punctuated::Punctuated,
|
punctuated::Punctuated,
|
||||||
DeriveInput, Error, Result, Token
|
DeriveInput, Result, Token
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MethodList(Punctuated<Ident, Token![,]>);
|
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||||
|
@ -23,29 +26,22 @@ impl Parse for MethodList {
|
||||||
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
let ident = input.ident;
|
let ident = input.ident;
|
||||||
let name = ident.to_string();
|
|
||||||
|
|
||||||
let methods =
|
let methods = input
|
||||||
input
|
.attrs
|
||||||
.attrs
|
.into_iter()
|
||||||
.into_iter()
|
.filter(|attr| attr.path.ends_with("resource"))
|
||||||
.filter(
|
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||||
|attr| {
|
.flat_map(|list| match list {
|
||||||
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string())
|
Ok(iter) => Box::new(iter.map(|method| {
|
||||||
} // TODO wtf
|
let ident = endpoint_ident(&method);
|
||||||
)
|
Ok(quote!(route.endpoint::<#ident>();))
|
||||||
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||||
.flat_map(|list| match list {
|
Err(err) => Box::new(iter::once(Err(err)))
|
||||||
Ok(iter) => Box::new(iter.map(|method| {
|
})
|
||||||
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
|
.collect_to_result()?;
|
||||||
let ident = method.setup_ident(&name);
|
|
||||||
Ok(quote!(#ident(&mut route);))
|
|
||||||
})) 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
|
impl #krate::Resource for #ident
|
||||||
{
|
{
|
||||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||||
|
@ -53,5 +49,22 @@ pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||||
#(#methods)*
|
#(#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 proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use syn::Error;
|
use syn::{Error, Path};
|
||||||
|
|
||||||
pub trait CollectToResult {
|
pub trait CollectToResult {
|
||||||
type Item;
|
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 {
|
pub fn remove_parens(input: TokenStream) -> TokenStream {
|
||||||
let iter = input.into_iter().flat_map(|tt| {
|
let iter = input.into_iter().flat_map(|tt| {
|
||||||
if let TokenTree::Group(group) = &tt {
|
if let TokenTree::Group(group) = &tt {
|
||||||
|
|
|
@ -18,7 +18,7 @@ gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
fake = "2.2.2"
|
fake = "2.2.2"
|
||||||
gotham = { version = "0.5.0", default-features = false }
|
gotham = { version = "0.5.0", default-features = false }
|
||||||
gotham_derive = "0.5.0"
|
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"
|
log = "0.4.8"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
serde = "1.0.110"
|
serde = "1.0.110"
|
||||||
|
|
|
@ -136,7 +136,7 @@ struct AuthData {
|
||||||
exp: u64
|
exp: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(AuthResource)]
|
#[read_all]
|
||||||
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
||||||
format!("{:?}", auth).into()
|
format!("{:?}", auth).into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -246,7 +246,7 @@ where
|
||||||
fn cors(&mut self, path: &str, method: Method);
|
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);
|
let config = CorsConfig::try_borrow_from(&state);
|
||||||
|
|
||||||
// prepare the response
|
// prepare the response
|
||||||
|
|
105
src/endpoint.rs
Normal file
105
src/endpoint.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use crate::{RequestBody, ResourceResult};
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
|
use gotham::{
|
||||||
|
extractor::{PathExtractor, QueryStringExtractor},
|
||||||
|
hyper::{Body, Method},
|
||||||
|
state::State
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
// TODO: Specify default types once https://github.com/rust-lang/rust/issues/29661 lands.
|
||||||
|
#[_private_openapi_trait(EndpointWithSchema)]
|
||||||
|
pub trait Endpoint {
|
||||||
|
/// The HTTP Verb of this endpoint.
|
||||||
|
fn http_method() -> Method;
|
||||||
|
/// The URI that this endpoint listens on in gotham's format.
|
||||||
|
fn uri() -> Cow<'static, str>;
|
||||||
|
|
||||||
|
/// The output type that provides the response.
|
||||||
|
type Output: ResourceResult + Send;
|
||||||
|
|
||||||
|
/// Returns `true` _iff_ the URI contains placeholders. `false` by default.
|
||||||
|
fn has_placeholders() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
/// The type that parses the URI placeholders. Use [gotham::extractor::NoopPathExtractor]
|
||||||
|
/// if `has_placeholders()` returns `false`.
|
||||||
|
#[openapi_bound("Placeholders: crate::OpenapiType")]
|
||||||
|
type Placeholders: PathExtractor<Body> + Sync;
|
||||||
|
|
||||||
|
/// Returns `true` _iff_ the request parameters should be parsed. `false` by default.
|
||||||
|
fn needs_params() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
/// The type that parses the request parameters. Use [gotham::extractor::NoopQueryStringExtractor]
|
||||||
|
/// if `needs_params()` returns `false`.
|
||||||
|
#[openapi_bound("Params: crate::OpenapiType")]
|
||||||
|
type Params: QueryStringExtractor<Body> + Sync;
|
||||||
|
|
||||||
|
/// Returns `true` _iff_ the request body should be parsed. `false` by default.
|
||||||
|
fn needs_body() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
/// The type to parse the body into. Use `()` if `needs_body()` returns `false`.
|
||||||
|
type Body: RequestBody + Send;
|
||||||
|
|
||||||
|
/// Returns `true` if the request wants to know the auth status of the client. `false` by default.
|
||||||
|
fn wants_auth() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the automatically generated operation id with a custom one. Only relevant for the
|
||||||
|
/// OpenAPI Specification.
|
||||||
|
#[openapi_only]
|
||||||
|
fn operation_id() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The handler for this endpoint.
|
||||||
|
fn handle(
|
||||||
|
state: &mut State,
|
||||||
|
placeholders: Self::Placeholders,
|
||||||
|
params: Self::Params,
|
||||||
|
body: Option<Self::Body>
|
||||||
|
) -> BoxFuture<'static, Self::Output>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
impl<E: EndpointWithSchema> Endpoint for E {
|
||||||
|
fn http_method() -> Method {
|
||||||
|
E::http_method()
|
||||||
|
}
|
||||||
|
fn uri() -> Cow<'static, str> {
|
||||||
|
E::uri()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Output = E::Output;
|
||||||
|
|
||||||
|
fn has_placeholders() -> bool {
|
||||||
|
E::has_placeholders()
|
||||||
|
}
|
||||||
|
type Placeholders = E::Placeholders;
|
||||||
|
|
||||||
|
fn needs_params() -> bool {
|
||||||
|
E::needs_params()
|
||||||
|
}
|
||||||
|
type Params = E::Params;
|
||||||
|
|
||||||
|
fn needs_body() -> bool {
|
||||||
|
E::needs_body()
|
||||||
|
}
|
||||||
|
type Body = E::Body;
|
||||||
|
|
||||||
|
fn wants_auth() -> bool {
|
||||||
|
E::wants_auth()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(
|
||||||
|
state: &mut State,
|
||||||
|
placeholders: Self::Placeholders,
|
||||||
|
params: Self::Params,
|
||||||
|
body: Option<Self::Body>
|
||||||
|
) -> BoxFuture<'static, Self::Output> {
|
||||||
|
E::handle(state, placeholders, params, body)
|
||||||
|
}
|
||||||
|
}
|
147
src/lib.rs
147
src/lib.rs
|
@ -9,7 +9,7 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
/*!
|
/*!
|
||||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
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.
|
for requests.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
@ -27,23 +27,23 @@ for requests.
|
||||||
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
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.
|
`#![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_all | | GET | /foobar |
|
||||||
| read | id | GET | /foobar/:id |
|
| read | id | GET | /foobar/:id |
|
||||||
| search | query | GET | /foobar/search |
|
| search | query | GET | /foobar/search |
|
||||||
| create | body | POST | /foobar |
|
| create | body | POST | /foobar |
|
||||||
| change_all | body | PUT | /foobar |
|
| change_all | body | PUT | /foobar |
|
||||||
| change | id, body | PUT | /foobar/:id |
|
| change | id, body | PUT | /foobar/:id |
|
||||||
| remove_all | | DELETE | /foobar |
|
| remove_all | | DELETE | /foobar |
|
||||||
| remove | id | DELETE | /foobar/:id |
|
| remove | id | DELETE | /foobar/:id |
|
||||||
|
|
||||||
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
|
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
|
||||||
simple example could look like this:
|
simple example looks like this:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -55,15 +55,15 @@ simple example could look like this:
|
||||||
#[resource(read)]
|
#[resource(read)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
/// The return type of the foo read method.
|
/// The return type of the foo read endpoint.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct Foo {
|
struct Foo {
|
||||||
id: u64
|
id: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The foo read method handler.
|
/// The foo read endpoint.
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
fn read(id: u64) -> Success<Foo> {
|
fn read(id: u64) -> Success<Foo> {
|
||||||
Foo { id }.into()
|
Foo { id }.into()
|
||||||
}
|
}
|
||||||
|
@ -76,17 +76,16 @@ fn read(id: u64) -> Success<Foo> {
|
||||||
|
|
||||||
# Arguments
|
# Arguments
|
||||||
|
|
||||||
Some methods require arguments. Those should be
|
Some endpoints require arguments. Those should be
|
||||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
|
||||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
* **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
|
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
|
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
|
||||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
|
||||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
improve.
|
||||||
`Sync` there is unfortunately no more convenient way.
|
|
||||||
|
|
||||||
# Uploads and Downloads
|
# Uploads and Downloads
|
||||||
|
|
||||||
|
@ -110,7 +109,7 @@ struct RawImage {
|
||||||
content_type: Mime
|
content_type: Mime
|
||||||
}
|
}
|
||||||
|
|
||||||
#[create(ImageResource)]
|
#[create]
|
||||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||||
Raw::new(body.content, body.content_type)
|
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
|
when you implement your web server. The complete feature list is
|
||||||
- [`auth`](#authentication-feature) Advanced JWT middleware
|
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||||
- `chrono` openapi support for chrono types
|
- `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
|
- [`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
|
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
|
||||||
- `uuid` openapi support for uuid
|
- `uuid` openapi support for uuid
|
||||||
|
- `without-openapi` (**default**) disables `openapi` support.
|
||||||
|
|
||||||
## Authentication Feature
|
## Authentication Feature
|
||||||
|
|
||||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
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
|
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.
|
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.
|
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
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -167,7 +168,7 @@ struct AuthData {
|
||||||
exp: u64
|
exp: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read(SecretResource)]
|
#[read]
|
||||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||||
let intended_for = auth.ok()?.sub;
|
let intended_for = auth.ok()?.sub;
|
||||||
Ok(Secret { id, intended_for })
|
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.
|
configuration as a middleware.
|
||||||
|
|
||||||
A simple example that allows authentication from every origin (note that `*` always disallows
|
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
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -207,7 +208,7 @@ authentication), and every content type, could look like this:
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() {
|
fn read_all() {
|
||||||
// your handler
|
// 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,
|
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.
|
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
|
```rust,no_run
|
||||||
# #[macro_use] extern crate diesel;
|
# #[macro_use] extern crate diesel;
|
||||||
|
@ -267,7 +268,7 @@ struct Foo {
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||||
foo::table.load(conn)
|
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
|
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
|
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
|
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
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -313,7 +314,7 @@ struct Foo {
|
||||||
bar: String
|
bar: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() -> Success<Foo> {
|
fn read_all() -> Success<Foo> {
|
||||||
Foo { bar: "Hello World".to_owned() }.into()
|
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`
|
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
|
It will return the generated openapi specification in JSON format. This allows you to easily write
|
||||||
in different languages without worying to exactly replicate your api in each of those languages.
|
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,
|
However, please note that by default, the `without-openapi` feature of this crate is enabled.
|
||||||
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
|
Disabling it in favour of the `openapi` feature will add an additional type bound, [`OpenapiType`],
|
||||||
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
|
on some of the types in [`Endpoint`] and related traits. This means that some code might only
|
||||||
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
|
compile on either feature, but not on both. If you are writing a library that uses gotham-restful,
|
||||||
In other words, put
|
it is strongly recommended to pass both features through and conditionally enable the openapi
|
||||||
|
code, like this:
|
||||||
|
|
||||||
```toml
|
```rust
|
||||||
[features]
|
# #[macro_use] extern crate gotham_restful;
|
||||||
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;
|
|
||||||
# use serde::{Deserialize, Serialize};
|
# use serde::{Deserialize, Serialize};
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
struct Foo;
|
struct Foo;
|
||||||
# }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
This readme and the crate documentation contain some of example. In addition to that, there is
|
||||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
a collection of code in the [example] directory that might help you. Any help writing more
|
||||||
|
examples is highly appreciated.
|
||||||
|
|
||||||
|
|
||||||
[diesel]: https://diesel.rs/
|
[diesel]: https://diesel.rs/
|
||||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||||
[gotham]: https://gotham.rs/
|
[gotham]: https://gotham.rs/
|
||||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
[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"))]
|
#[cfg(all(feature = "openapi", feature = "without-openapi"))]
|
||||||
|
@ -390,6 +380,8 @@ extern crate self as gotham_restful;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate gotham_derive;
|
extern crate gotham_derive;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
extern crate gotham_restful_derive;
|
||||||
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
@ -409,7 +401,9 @@ pub use gotham_restful_derive::*;
|
||||||
/// Not public API
|
/// Not public API
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod export {
|
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;
|
pub use serde_json;
|
||||||
|
|
||||||
|
@ -441,11 +435,10 @@ pub use openapi::{
|
||||||
types::{OpenapiSchema, OpenapiType}
|
types::{OpenapiSchema, OpenapiType}
|
||||||
};
|
};
|
||||||
|
|
||||||
mod resource;
|
mod endpoint;
|
||||||
pub use resource::{
|
pub use endpoint::Endpoint;
|
||||||
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll,
|
#[cfg(feature = "openapi")]
|
||||||
ResourceRemove, ResourceRemoveAll, ResourceSearch
|
pub use endpoint::EndpointWithSchema;
|
||||||
};
|
|
||||||
|
|
||||||
mod response;
|
mod response;
|
||||||
pub use response::Response;
|
pub use response::Response;
|
||||||
|
@ -457,9 +450,21 @@ pub use result::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod routing;
|
mod routing;
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub use routing::WithOpenapi;
|
|
||||||
pub use routing::{DrawResourceRoutes, DrawResources};
|
pub use routing::{DrawResourceRoutes, DrawResources};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi};
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use 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 super::SECURITY_NAME;
|
||||||
use crate::{resource::*, result::*, OpenapiSchema, RequestBody};
|
use crate::{result::*, EndpointWithSchema, OpenapiSchema, RequestBody};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
|
@ -8,31 +8,40 @@ use openapiv3::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct OperationParams<'a> {
|
struct OperationParams {
|
||||||
path_params: Vec<(&'a str, ReferenceOr<Schema>)>,
|
path_params: Option<OpenapiSchema>,
|
||||||
query_params: Option<OpenapiSchema>
|
query_params: Option<OpenapiSchema>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> OperationParams<'a> {
|
impl OperationParams {
|
||||||
fn add_path_params(&self, params: &mut Vec<ReferenceOr<Parameter>>) {
|
fn add_path_params(path_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||||
for param in &self.path_params {
|
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 {
|
params.push(Item(Parameter::Path {
|
||||||
parameter_data: ParameterData {
|
parameter_data: ParameterData {
|
||||||
name: (*param).0.to_string(),
|
name,
|
||||||
description: None,
|
description: None,
|
||||||
required: true,
|
required,
|
||||||
deprecated: None,
|
deprecated: None,
|
||||||
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
|
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||||
example: None,
|
example: None,
|
||||||
examples: IndexMap::new()
|
examples: IndexMap::new()
|
||||||
},
|
},
|
||||||
style: Default::default()
|
style: Default::default()
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_query_params(self, params: &mut Vec<ReferenceOr<Parameter>>) {
|
fn add_query_params(query_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||||
let query_params = match self.query_params {
|
let query_params = match query_params {
|
||||||
Some(qp) => qp.schema,
|
Some(qp) => qp.schema,
|
||||||
None => return
|
None => return
|
||||||
};
|
};
|
||||||
|
@ -61,51 +70,48 @@ impl<'a> OperationParams<'a> {
|
||||||
|
|
||||||
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
|
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
|
||||||
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
|
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
|
||||||
self.add_path_params(&mut params);
|
Self::add_path_params(self.path_params, &mut params);
|
||||||
self.add_query_params(&mut params);
|
Self::add_query_params(self.query_params, &mut params);
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OperationDescription<'a> {
|
pub struct OperationDescription {
|
||||||
operation_id: Option<String>,
|
operation_id: Option<String>,
|
||||||
default_status: crate::StatusCode,
|
default_status: crate::StatusCode,
|
||||||
accepted_types: Option<Vec<Mime>>,
|
accepted_types: Option<Vec<Mime>>,
|
||||||
schema: ReferenceOr<Schema>,
|
schema: ReferenceOr<Schema>,
|
||||||
params: OperationParams<'a>,
|
params: OperationParams,
|
||||||
body_schema: Option<ReferenceOr<Schema>>,
|
body_schema: Option<ReferenceOr<Schema>>,
|
||||||
supported_types: Option<Vec<Mime>>,
|
supported_types: Option<Vec<Mime>>,
|
||||||
requires_auth: bool
|
requires_auth: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> OperationDescription<'a> {
|
impl OperationDescription {
|
||||||
pub fn new<Handler: ResourceMethod>(schema: ReferenceOr<Schema>) -> Self {
|
pub fn new<E: EndpointWithSchema>(schema: ReferenceOr<Schema>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
operation_id: Handler::operation_id(),
|
operation_id: E::operation_id(),
|
||||||
default_status: Handler::Res::default_status(),
|
default_status: E::Output::default_status(),
|
||||||
accepted_types: Handler::Res::accepted_types(),
|
accepted_types: E::Output::accepted_types(),
|
||||||
schema,
|
schema,
|
||||||
params: Default::default(),
|
params: Default::default(),
|
||||||
body_schema: None,
|
body_schema: None,
|
||||||
supported_types: 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 {
|
pub fn set_path_params(&mut self, params: OpenapiSchema) {
|
||||||
self.params.path_params.push((name, schema));
|
self.params.path_params = Some(params);
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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.body_schema = Some(schema);
|
||||||
self.supported_types = Body::supported_types();
|
self.supported_types = Body::supported_types();
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema_to_content(types: Vec<Mime>, schema: ReferenceOr<Schema>) -> IndexMap<String, MediaType> {
|
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 super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
|
||||||
use crate::{resource::*, routing::*, OpenapiType};
|
use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceWithSchema};
|
||||||
use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*};
|
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::{Captures, Regex};
|
||||||
use std::panic::RefUnwindSafe;
|
use std::panic::RefUnwindSafe;
|
||||||
|
|
||||||
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
|
/// 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
|
where
|
||||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P: RefUnwindSafe + 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));
|
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
|
where
|
||||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P: RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn read_all<Handler: ResourceReadAll>(&mut self) {
|
fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
let schema = (self.0).openapi_builder.add_schema::<E::Output>();
|
||||||
|
let mut descr = OperationDescription::new::<E>(schema);
|
||||||
|
if E::has_placeholders() {
|
||||||
|
descr.set_path_params(E::Placeholders::schema());
|
||||||
|
}
|
||||||
|
if E::needs_params() {
|
||||||
|
descr.set_query_params(E::Params::schema());
|
||||||
|
}
|
||||||
|
if E::needs_body() {
|
||||||
|
let body_schema = (self.0).openapi_builder.add_schema::<E::Body>();
|
||||||
|
descr.set_body::<E::Body>(body_schema);
|
||||||
|
}
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
static URI_PLACEHOLDER_REGEX: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r#"(^|/):(?P<name>[^/]+)(/|$)"#).unwrap());
|
||||||
|
let uri: &str = &E::uri();
|
||||||
|
let uri =
|
||||||
|
URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| format!("{{{}}}", &captures["name"]));
|
||||||
|
let path = if uri.is_empty() {
|
||||||
|
format!("{}/{}", self.0.scope.unwrap_or_default(), self.1)
|
||||||
|
} else {
|
||||||
|
format!("{}/{}/{}", self.0.scope.unwrap_or_default(), self.1, uri)
|
||||||
|
};
|
||||||
|
|
||||||
|
let op = descr.into_operation();
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||||
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
match E::http_method() {
|
||||||
|
Method::GET => item.get = Some(op),
|
||||||
|
Method::PUT => item.put = Some(op),
|
||||||
|
Method::POST => item.post = Some(op),
|
||||||
|
Method::DELETE => item.delete = Some(op),
|
||||||
|
Method::OPTIONS => item.options = Some(op),
|
||||||
|
Method::HEAD => item.head = Some(op),
|
||||||
|
Method::PATCH => item.patch = Some(op),
|
||||||
|
Method::TRACE => item.trace = Some(op),
|
||||||
|
method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method)
|
||||||
|
};
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
(self.0).openapi_builder.add_path(path, item);
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).read_all::<Handler>()
|
(&mut *(self.0).router, self.1).endpoint::<E>()
|
||||||
}
|
|
||||||
|
|
||||||
fn read<Handler: ResourceRead>(&mut self) {
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.get = Some(
|
|
||||||
OperationDescription::new::<Handler>(schema)
|
|
||||||
.add_path_param("id", id_schema)
|
|
||||||
.into_operation()
|
|
||||||
);
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).read::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search<Handler: ResourceSearch>(&mut self) {
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.get = Some(
|
|
||||||
OperationDescription::new::<Handler>(schema)
|
|
||||||
.with_query_params(Handler::Query::schema())
|
|
||||||
.into_operation()
|
|
||||||
);
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).search::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create<Handler: ResourceCreate>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res: 'static,
|
|
||||||
Handler::Body: 'static
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.post = Some(
|
|
||||||
OperationDescription::new::<Handler>(schema)
|
|
||||||
.with_body::<Handler::Body>(body_schema)
|
|
||||||
.into_operation()
|
|
||||||
);
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).create::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res: 'static,
|
|
||||||
Handler::Body: 'static
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.put = Some(
|
|
||||||
OperationDescription::new::<Handler>(schema)
|
|
||||||
.with_body::<Handler::Body>(body_schema)
|
|
||||||
.into_operation()
|
|
||||||
);
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).change_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change<Handler: ResourceChange>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res: 'static,
|
|
||||||
Handler::Body: 'static
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
|
||||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.put = Some(
|
|
||||||
OperationDescription::new::<Handler>(schema)
|
|
||||||
.add_path_param("id", id_schema)
|
|
||||||
.with_body::<Handler::Body>(body_schema)
|
|
||||||
.into_operation()
|
|
||||||
);
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).change::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).remove_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove<Handler: ResourceRemove>(&mut self) {
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.delete = Some(
|
|
||||||
OperationDescription::new::<Handler>(schema)
|
|
||||||
.add_path_param("id", id_schema)
|
|
||||||
.into_operation()
|
|
||||||
);
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).remove::<Handler>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
||||||
|
use gotham::extractor::{NoopPathExtractor, NoopQueryStringExtractor};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType,
|
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 {
|
impl OpenapiType for bool {
|
||||||
fn schema() -> OpenapiSchema {
|
fn schema() -> OpenapiSchema {
|
||||||
OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))
|
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)]
|
# #[derive(Clone, Deserialize)]
|
||||||
# struct MyAuthData { exp : u64 }
|
# struct MyAuthData { exp : u64 }
|
||||||
#
|
#
|
||||||
#[read_all(MyResource)]
|
#[read_all]
|
||||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
||||||
let auth_data = match auth {
|
let auth_data = match auth {
|
||||||
AuthStatus::Authenticated(data) => data,
|
AuthStatus::Authenticated(data) => data,
|
||||||
|
@ -102,7 +102,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
|
||||||
# #[derive(Clone, Deserialize)]
|
# #[derive(Clone, Deserialize)]
|
||||||
# struct MyAuthData { exp : u64 }
|
# struct MyAuthData { exp : u64 }
|
||||||
#
|
#
|
||||||
#[read_all(MyResource)]
|
#[read_all]
|
||||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
||||||
let auth_data = match auth {
|
let auth_data = match auth {
|
||||||
AuthStatus::Authenticated(data) => data,
|
AuthStatus::Authenticated(data) => data,
|
||||||
|
|
|
@ -21,8 +21,8 @@ the function attributes:
|
||||||
# #[resource(read_all)]
|
# #[resource(read_all)]
|
||||||
# struct MyResource;
|
# struct MyResource;
|
||||||
#
|
#
|
||||||
#[read_all(MyResource)]
|
#[read_all]
|
||||||
fn read_all(_state: &mut State) {
|
fn read_all() {
|
||||||
// do something
|
// do something
|
||||||
}
|
}
|
||||||
# }
|
# }
|
||||||
|
|
|
@ -25,7 +25,7 @@ example that simply returns its body:
|
||||||
#[resource(create)]
|
#[resource(create)]
|
||||||
struct ImageResource;
|
struct ImageResource;
|
||||||
|
|
||||||
#[create(ImageResource)]
|
#[create]
|
||||||
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,8 @@ struct MyResponse {
|
||||||
message: &'static str
|
message: &'static str
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(MyResource)]
|
#[read_all]
|
||||||
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
fn read_all() -> Success<MyResponse> {
|
||||||
let res = MyResponse { message: "I'm always happy" };
|
let res = MyResponse { message: "I'm always happy" };
|
||||||
res.into()
|
res.into()
|
||||||
}
|
}
|
||||||
|
|
358
src/routing.rs
358
src/routing.rs
|
@ -3,37 +3,33 @@ use crate::openapi::{
|
||||||
builder::{OpenapiBuilder, OpenapiInfo},
|
builder::{OpenapiBuilder, OpenapiInfo},
|
||||||
router::OpenapiRouter
|
router::OpenapiRouter
|
||||||
};
|
};
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
use crate::CorsRoute;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
resource::{
|
|
||||||
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceRead, ResourceReadAll, ResourceRemove,
|
|
||||||
ResourceRemoveAll, ResourceSearch
|
|
||||||
},
|
|
||||||
result::{ResourceError, ResourceResult},
|
result::{ResourceError, ResourceResult},
|
||||||
RequestBody, Response, StatusCode
|
Endpoint, FromBody, Resource, Response, StatusCode
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures_util::{future, future::FutureExt};
|
|
||||||
use gotham::{
|
use gotham::{
|
||||||
handler::{HandlerError, HandlerFuture},
|
handler::HandlerError,
|
||||||
helpers::http::response::{create_empty_response, create_response},
|
helpers::http::response::{create_empty_response, create_response},
|
||||||
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method},
|
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method},
|
||||||
pipeline::chain::PipelineHandleChain,
|
pipeline::chain::PipelineHandleChain,
|
||||||
router::{
|
router::{
|
||||||
builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher, RouterBuilder, ScopeBuilder},
|
builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder},
|
||||||
non_match::RouteNonMatch,
|
non_match::RouteNonMatch,
|
||||||
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
|
route::matcher::{
|
||||||
|
AcceptHeaderRouteMatcher, AccessControlRequestMethodMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher
|
||||||
|
}
|
||||||
},
|
},
|
||||||
state::{FromState, State}
|
state::{FromState, State}
|
||||||
};
|
};
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
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.
|
/// Allow us to extract an id from a path.
|
||||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
id: ID
|
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
|
/// 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
|
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
||||||
/// any RESTful [Resource] with a path.
|
/// any RESTful [Resource] with a path.
|
||||||
|
#[_private_openapi_trait(DrawResourcesWithSchema)]
|
||||||
pub trait DrawResources {
|
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
|
/// This trait allows to draw routes within an resource. Use this only inside the
|
||||||
/// [Resource::setup] method.
|
/// [Resource::setup] method.
|
||||||
|
#[_private_openapi_trait(DrawResourceRoutesWithSchema)]
|
||||||
pub trait DrawResourceRoutes {
|
pub trait DrawResourceRoutes {
|
||||||
fn read_all<Handler: ResourceReadAll>(&mut self);
|
#[openapi_bound("E: crate::EndpointWithSchema")]
|
||||||
|
#[non_openapi_bound("E: crate::Endpoint")]
|
||||||
fn read<Handler: ResourceRead>(&mut self);
|
fn endpoint<E: 'static>(&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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
|
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
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn to_handler_future<F, R>(
|
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError> {
|
||||||
state: State,
|
trace!("entering endpoint_handler");
|
||||||
get_result: F
|
let placeholders = E::Placeholders::take_from(state);
|
||||||
) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)>
|
let params = E::Params::take_from(state);
|
||||||
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 body_to_res<B, F, R>(
|
let body = match E::needs_body() {
|
||||||
mut state: State,
|
true => {
|
||||||
get_result: F
|
let body = to_bytes(Body::take_from(state)).await?;
|
||||||
) -> (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 body {
|
let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) {
|
||||||
Ok(body) => body,
|
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||||
Err(e) => return (state, Err(e.into()))
|
None => {
|
||||||
};
|
debug!("Missing Content-Type: Returning 415 Response");
|
||||||
|
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let content_type: Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
|
match E::Body::from_body(body, content_type) {
|
||||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
Ok(body) => Some(body),
|
||||||
None => {
|
Err(e) => {
|
||||||
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
debug!("Invalid Body: Returning 400 Response");
|
||||||
return (state, Ok(res));
|
let error: ResourceError = e.into();
|
||||||
}
|
let json = serde_json::to_string(&error)?;
|
||||||
};
|
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||||
|
return Ok(res);
|
||||||
let res = {
|
}
|
||||||
let body = match B::from_body(body, content_type) {
|
|
||||||
Ok(body) => body,
|
|
||||||
Err(e) => {
|
|
||||||
let error: ResourceError = e.into();
|
|
||||||
let res = match serde_json::to_string(&error) {
|
|
||||||
Ok(json) => {
|
|
||||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
|
||||||
Ok(res)
|
|
||||||
},
|
|
||||||
Err(e) => Err(e.into())
|
|
||||||
};
|
|
||||||
return (state, res);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
get_result(state, body)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (state, res) = res.await;
|
|
||||||
let res = res.into_response().await;
|
|
||||||
|
|
||||||
let res = match res {
|
|
||||||
Ok(res) => {
|
|
||||||
let r = response_from(res, &state);
|
|
||||||
Ok(r)
|
|
||||||
},
|
},
|
||||||
Err(e) => Err(e.into())
|
false => None
|
||||||
};
|
};
|
||||||
(state, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_with_body<B, F, R>(state: State, get_result: F) -> Pin<Box<HandlerFuture>>
|
let out = E::handle(state, placeholders, params, body).await;
|
||||||
where
|
let res = out.into_response().await?;
|
||||||
B: RequestBody + 'static,
|
debug!("Returning response {:?}", res);
|
||||||
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
|
Ok(response_from(res, state))
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -267,8 +139,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
impl MaybeMatchAcceptHeader {
|
||||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||||
let types = match types {
|
let types = match types {
|
||||||
Some(types) if types.is_empty() => None,
|
Some(types) if types.is_empty() => None,
|
||||||
types => types
|
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)]
|
#[derive(Clone)]
|
||||||
struct MaybeMatchContentTypeHeader {
|
struct MaybeMatchContentTypeHeader {
|
||||||
matcher: Option<ContentTypeHeaderRouteMatcher>
|
matcher: Option<ContentTypeHeaderRouteMatcher>
|
||||||
|
@ -293,14 +171,20 @@ impl RouteMatcher for MaybeMatchContentTypeHeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
impl MaybeMatchContentTypeHeader {
|
||||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
|
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 {
|
macro_rules! implDrawResourceRoutes {
|
||||||
($implType:ident) => {
|
($implType:ident) => {
|
||||||
#[cfg(feature = "openapi")]
|
#[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)
|
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
||||||
where
|
where
|
||||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P: RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn read_all<Handler: ResourceReadAll>(&mut self) {
|
fn endpoint<E: Endpoint + 'static>(&mut self) {
|
||||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
let uri = format!("{}/{}", self.1, E::uri());
|
||||||
self.0
|
debug!("Registering endpoint for {}", uri);
|
||||||
.get(&self.1)
|
self.0.associate(&uri, |assoc| {
|
||||||
.extend_route_matcher(matcher)
|
assoc
|
||||||
.to(|state| read_all_handler::<Handler>(state));
|
.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) {
|
#[cfg(feature = "cors")]
|
||||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
if E::http_method() != Method::GET {
|
||||||
self.0
|
assoc
|
||||||
.get(&format!("{}/:id", self.1))
|
.options()
|
||||||
.extend_route_matcher(matcher)
|
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
.to(crate::cors::cors_preflight_handler);
|
||||||
.to(|state| read_handler::<Handler>(state));
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
fn search<Handler: ResourceSearch>(&mut self) {
|
|
||||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0
|
|
||||||
.get(&format!("{}/search", self.1))
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_query_string_extractor::<Handler::Query>()
|
|
||||||
.to(|state| search_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create<Handler: ResourceCreate>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res: Send + 'static,
|
|
||||||
Handler::Body: 'static
|
|
||||||
{
|
|
||||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
self.0
|
|
||||||
.post(&self.1)
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.to(|state| create_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&self.1, Method::POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res: Send + 'static,
|
|
||||||
Handler::Body: 'static
|
|
||||||
{
|
|
||||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
self.0
|
|
||||||
.put(&self.1)
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.to(|state| change_all_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&self.1, Method::PUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change<Handler: ResourceChange>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res: Send + 'static,
|
|
||||||
Handler::Body: 'static
|
|
||||||
{
|
|
||||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
let path = format!("{}/:id", self.1);
|
|
||||||
self.0
|
|
||||||
.put(&path)
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| change_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&path, Method::PUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
|
|
||||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0
|
|
||||||
.delete(&self.1)
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.to(|state| remove_all_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&self.1, Method::DELETE);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove<Handler: ResourceRemove>(&mut self) {
|
|
||||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let path = format!("{}/:id", self.1);
|
|
||||||
self.0
|
|
||||||
.delete(&path)
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| remove_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&path, Method::POST);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
11
src/types.rs
11
src/types.rs
|
@ -4,7 +4,7 @@ use crate::OpenapiType;
|
||||||
use gotham::hyper::body::Bytes;
|
use gotham::hyper::body::Bytes;
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{error::Error, panic::RefUnwindSafe};
|
use std::error::Error;
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
#[cfg(not(feature = "openapi"))]
|
||||||
pub trait ResourceType {}
|
pub trait ResourceType {}
|
||||||
|
@ -98,12 +98,3 @@ impl<T: ResourceType + DeserializeOwned> RequestBody for T {
|
||||||
Some(vec![APPLICATION_JSON])
|
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";
|
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
async fn read_all() -> Raw<&'static [u8]> {
|
async fn read_all() -> Raw<&'static [u8]> {
|
||||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
async fn read(_id: u64) -> Raw<&'static [u8]> {
|
async fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||||
#[search(FooResource)]
|
#[search]
|
||||||
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||||
#[create(FooResource)]
|
#[create]
|
||||||
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||||
#[change_all(FooResource)]
|
#[change_all]
|
||||||
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||||
#[change(FooResource)]
|
#[change]
|
||||||
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||||
#[remove_all(FooResource)]
|
#[remove_all]
|
||||||
async fn remove_all() -> Raw<&'static [u8]> {
|
async fn remove_all() -> Raw<&'static [u8]> {
|
||||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||||
#[remove(FooResource)]
|
#[remove]
|
||||||
async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn async_methods() {
|
fn async_methods() {
|
||||||
|
let _ = pretty_env_logger::try_init_timed();
|
||||||
|
|
||||||
let server = TestServer::new(build_simple_router(|router| {
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
router.resource::<FooResource>("foo");
|
router.resource::<FooResource>("foo");
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -16,10 +16,10 @@ use mime::TEXT_PLAIN;
|
||||||
#[resource(read_all, change_all)]
|
#[resource(read_all, change_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() {}
|
fn read_all() {}
|
||||||
|
|
||||||
#[change_all(FooResource)]
|
#[change_all]
|
||||||
fn change_all(_body: Raw<Vec<u8>>) {}
|
fn change_all(_body: Raw<Vec<u8>>) {}
|
||||||
|
|
||||||
fn test_server(cfg: CorsConfig) -> TestServer {
|
fn test_server(cfg: CorsConfig) -> TestServer {
|
||||||
|
|
|
@ -15,7 +15,7 @@ struct Foo {
|
||||||
content_type: Mime
|
content_type: Mime
|
||||||
}
|
}
|
||||||
|
|
||||||
#[create(FooResource)]
|
#[create]
|
||||||
fn create(body: Foo) -> Raw<Vec<u8>> {
|
fn create(body: Foo) -> Raw<Vec<u8>> {
|
||||||
Raw::new(body.content, body.content_type)
|
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=";
|
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read, change)]
|
#[resource(get_image, set_image)]
|
||||||
struct ImageResource;
|
struct ImageResource;
|
||||||
|
|
||||||
#[derive(FromBody, RequestBody)]
|
#[derive(FromBody, RequestBody)]
|
||||||
#[supported_types(IMAGE_PNG)]
|
#[supported_types(IMAGE_PNG)]
|
||||||
struct Image(Vec<u8>);
|
struct Image(Vec<u8>);
|
||||||
|
|
||||||
#[read(ImageResource, operation_id = "getImage")]
|
#[read(operation_id = "getImage")]
|
||||||
fn get_image(_id: u64) -> Raw<&'static [u8]> {
|
fn get_image(_id: u64) -> Raw<&'static [u8]> {
|
||||||
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
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) {}
|
fn set_image(_id: u64, _image: Image) {}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read, search)]
|
#[resource(read_secret, search_secret)]
|
||||||
struct SecretResource;
|
struct SecretResource;
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
|
@ -67,13 +67,13 @@ struct SecretQuery {
|
||||||
minute: Option<u16>
|
minute: Option<u16>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read(SecretResource)]
|
#[read]
|
||||||
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
|
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
|
||||||
auth.ok()?;
|
auth.ok()?;
|
||||||
Ok(Secret { code: 4.2 })
|
Ok(Secret { code: 4.2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[search(SecretResource)]
|
#[search]
|
||||||
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
|
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
|
||||||
auth.ok()?;
|
auth.ok()?;
|
||||||
Ok(Secrets {
|
Ok(Secrets {
|
||||||
|
@ -82,7 +82,7 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openapi_supports_scope() {
|
fn openapi_specification() {
|
||||||
let info = OpenapiInfo {
|
let info = OpenapiInfo {
|
||||||
title: "This is just a test".to_owned(),
|
title: "This is just a test".to_owned(),
|
||||||
version: "1.2.3".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)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() -> Raw<&'static [u8]> {
|
fn read_all() -> Raw<&'static [u8]> {
|
||||||
Raw::new(RESPONSE, TEXT_PLAIN)
|
Raw::new(RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,55 +30,57 @@ struct FooSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() -> Raw<&'static [u8]> {
|
fn read_all() -> Raw<&'static [u8]> {
|
||||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
fn read(_id: u64) -> Raw<&'static [u8]> {
|
fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||||
#[search(FooResource)]
|
#[search]
|
||||||
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||||
#[create(FooResource)]
|
#[create]
|
||||||
fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||||
#[change_all(FooResource)]
|
#[change_all]
|
||||||
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||||
#[change(FooResource)]
|
#[change]
|
||||||
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||||
#[remove_all(FooResource)]
|
#[remove_all]
|
||||||
fn remove_all() -> Raw<&'static [u8]> {
|
fn remove_all() -> Raw<&'static [u8]> {
|
||||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||||
#[remove(FooResource)]
|
#[remove]
|
||||||
fn remove(_id: u64) -> Raw<&'static [u8]> {
|
fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_methods() {
|
fn sync_methods() {
|
||||||
|
let _ = pretty_env_logger::try_init_timed();
|
||||||
|
|
||||||
let server = TestServer::new(build_simple_router(|router| {
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
router.resource::<FooResource>("foo");
|
router.resource::<FooResource>("foo");
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -6,23 +6,12 @@ fn trybuild_ui() {
|
||||||
let t = TestCases::new();
|
let t = TestCases::new();
|
||||||
|
|
||||||
// always enabled
|
// always enabled
|
||||||
t.compile_fail("tests/ui/from_body_enum.rs");
|
t.compile_fail("tests/ui/endpoint/*.rs");
|
||||||
t.compile_fail("tests/ui/method_async_state.rs");
|
t.compile_fail("tests/ui/from_body/*.rs");
|
||||||
t.compile_fail("tests/ui/method_for_unknown_resource.rs");
|
t.compile_fail("tests/ui/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");
|
|
||||||
|
|
||||||
// require the openapi feature
|
// require the openapi feature
|
||||||
if cfg!(feature = "openapi") {
|
if cfg!(feature = "openapi") {
|
||||||
t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs");
|
t.compile_fail("tests/ui/openapi_type/*.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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(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)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all]
|
#[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
|
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
|
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 },
|
7 | Pizza { pineapple: bool },
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
|
|
||||||
error: #[derive(OpenapiType)] does not support enum variants with fields
|
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)
|
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
|
error: Expected bool
|
||||||
--> $DIR/openapi_type_nullable_non_bool.rs:6:23
|
--> $DIR/nullable_non_bool.rs:6:23
|
||||||
|
|
|
|
||||||
6 | #[openapi(nullable = "yes, please")]
|
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
|
error: Expected string literal
|
||||||
--> $DIR/openapi_type_rename_non_string.rs:6:21
|
--> $DIR/rename_non_string.rs:6:21
|
||||||
|
|
|
|
||||||
6 | #[openapi(rename = 42)]
|
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
|
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
|
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
|
error: Unknown key
|
||||||
--> $DIR/openapi_type_unknown_key.rs:6:12
|
--> $DIR/unknown_key.rs:6:12
|
||||||
|
|
|
|
||||||
6 | #[openapi(like = "pizza")]
|
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,
|
hyper::Body,
|
||||||
test::TestServer
|
test::TestServer
|
||||||
};
|
};
|
||||||
|
use log::info;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use std::{fs::File, io::{Read, Write}, str};
|
use std::{fs::File, io::{Read, Write}, str};
|
||||||
|
|
||||||
pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8])
|
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 res = server.client().get(path).perform().unwrap().read_body().unwrap();
|
||||||
let body : &[u8] = res.as_ref();
|
let body : &[u8] = res.as_ref();
|
||||||
assert_eq!(body, expected);
|
assert_eq!(body, expected);
|
||||||
|
@ -17,6 +19,7 @@ pub fn test_post_response<B>(server : &TestServer, path : &str, body : B, mime :
|
||||||
where
|
where
|
||||||
B : Into<Body>
|
B : Into<Body>
|
||||||
{
|
{
|
||||||
|
info!("POST {}", path);
|
||||||
let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap();
|
let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap();
|
||||||
let body : &[u8] = res.as_ref();
|
let body : &[u8] = res.as_ref();
|
||||||
assert_eq!(body, expected);
|
assert_eq!(body, expected);
|
||||||
|
@ -26,6 +29,7 @@ pub fn test_put_response<B>(server : &TestServer, path : &str, body : B, mime :
|
||||||
where
|
where
|
||||||
B : Into<Body>
|
B : Into<Body>
|
||||||
{
|
{
|
||||||
|
info!("PUT {}", path);
|
||||||
let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap();
|
let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap();
|
||||||
let body : &[u8] = res.as_ref();
|
let body : &[u8] = res.as_ref();
|
||||||
assert_eq!(body, expected);
|
assert_eq!(body, expected);
|
||||||
|
@ -33,6 +37,7 @@ where
|
||||||
|
|
||||||
pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
|
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 res = server.client().delete(path).perform().unwrap().read_body().unwrap();
|
||||||
let body : &[u8] = res.as_ref();
|
let body : &[u8] = res.as_ref();
|
||||||
assert_eq!(body, expected);
|
assert_eq!(body, expected);
|
||||||
|
@ -41,12 +46,14 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8])
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str)
|
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 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();
|
let body = serde_json::to_string_pretty(&serde_json::from_slice::<serde_json::Value>(res.as_ref()).unwrap()).unwrap();
|
||||||
match File::open(output_file) {
|
match File::open(output_file) {
|
||||||
Ok(mut file) => {
|
Ok(mut file) => {
|
||||||
let mut expected = String::new();
|
let mut expected = String::new();
|
||||||
file.read_to_string(&mut expected).unwrap();
|
file.read_to_string(&mut expected).unwrap();
|
||||||
|
eprintln!("{}", body);
|
||||||
assert_eq!(body, expected);
|
assert_eq!(body, expected);
|
||||||
},
|
},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue