mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-19 22:44:38 +00:00
Compare commits
59 commits
Author | SHA1 | Date | |
---|---|---|---|
7bac379e05 | |||
2dd3f3e21a | |||
e206ab10eb | |||
a5257608e3 | |||
9c7f681e3d | |||
63567f5480 | |||
3a3f743369 | |||
ebea39fe0d | |||
eecd192458 | |||
2a35e044db | |||
a57f1c097d | |||
5f60599c41 | |||
43d3a1cd89 | |||
667009bd22 | |||
d9c7f4135f | |||
90870e3b6a | |||
2251c29d7b | |||
09dee5a673 | |||
fabcbc4e78 | |||
960ba0e8bc | |||
bb6f5b0fdd | |||
![]() |
31f92c07cd | ||
28ae4dfdee | |||
![]() |
7ed98c82e8 | ||
e7ef6bdf5a | |||
666514c8e2 | |||
![]() |
7de11cdae1 | ||
c640efcb88 | |||
30edd349ed | |||
90fc17e57d | |||
3b95b9f495 | |||
1bd398c7ee | |||
8b73701405 | |||
9e65540cd8 | |||
44e2f0317c | |||
![]() |
af28e0d916 | ||
![]() |
441a42c75e | ||
cf0223473f | |||
f2bcc8438f | |||
681ef5d894 | |||
70914d107b | |||
![]() |
5261aa9931 | ||
002cfb1b4d | |||
![]() |
b807ae2796 | ||
0ac0f0f504 | |||
0251c03ceb | |||
75cd7e2c96 | |||
edd8bb618d | |||
daea3ba9ec | |||
b7a1193333 | |||
44f3c9fe84 | |||
3600a115d0 | |||
388bf8b49c | |||
6ee382242b | |||
813c12614f | |||
766bc9d17d | |||
b005346e54 | |||
141e5ac2d7 | |||
2b8796b9c9 |
157 changed files with 4915 additions and 3458 deletions
|
@ -6,31 +6,47 @@ stages:
|
|||
|
||||
variables:
|
||||
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||
RUST_LOG: info,gotham=debug,gotham_restful=trace
|
||||
|
||||
test-default:
|
||||
check-example:
|
||||
stage: test
|
||||
image: rust:1.42-slim
|
||||
image: rust:slim
|
||||
before_script:
|
||||
- cargo -V
|
||||
script:
|
||||
- cargo test --workspace
|
||||
- cargo check --manifest-path example/Cargo.toml
|
||||
cache:
|
||||
key: cargo-1-42-default
|
||||
key: cargo-stable-example
|
||||
paths:
|
||||
- cargo/
|
||||
- target/
|
||||
|
||||
test-all:
|
||||
test-default:
|
||||
stage: test
|
||||
image: rust:1.42-slim
|
||||
image: rust:1.49-slim
|
||||
before_script:
|
||||
- cargo -V
|
||||
script:
|
||||
- cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild
|
||||
- cargo test
|
||||
cache:
|
||||
key: cargo-1-49-default
|
||||
paths:
|
||||
- cargo/
|
||||
- target/
|
||||
|
||||
test-full:
|
||||
stage: test
|
||||
image: rust:1.49-slim
|
||||
before_script:
|
||||
- apt update -y
|
||||
- apt install -y --no-install-recommends libpq-dev
|
||||
- cargo -V
|
||||
script:
|
||||
- cargo test --workspace --all-features
|
||||
- cargo test --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild
|
||||
- cargo test --no-default-features --features full
|
||||
cache:
|
||||
key: cargo-1-42-all
|
||||
key: cargo-1-49-all
|
||||
paths:
|
||||
- cargo/
|
||||
- target/
|
||||
|
@ -44,7 +60,7 @@ test-tarpaulin:
|
|||
- cargo -V
|
||||
- cargo install cargo-tarpaulin
|
||||
script:
|
||||
- cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v
|
||||
- cargo tarpaulin --target-dir target/tarpaulin --no-default-features --features full --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v
|
||||
artifacts:
|
||||
paths:
|
||||
- tarpaulin-report.html
|
||||
|
@ -58,34 +74,44 @@ test-tarpaulin:
|
|||
|
||||
test-trybuild-ui:
|
||||
stage: test
|
||||
image: rust:1.48-slim
|
||||
image: rust:1.50-slim
|
||||
before_script:
|
||||
- apt update -y
|
||||
- apt install -y --no-install-recommends libpq-dev
|
||||
- cargo -V
|
||||
script:
|
||||
- cargo test --workspace --all-features --tests -- --ignored
|
||||
- cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild
|
||||
- cargo test --no-default-features --features full --tests -- --ignored
|
||||
cache:
|
||||
key: cargo-1-48-all
|
||||
key: cargo-1-50-all
|
||||
paths:
|
||||
- cargo/
|
||||
- target/
|
||||
|
||||
readme:
|
||||
stage: test
|
||||
image: msrd0/cargo-readme
|
||||
image: ghcr.io/msrd0/cargo-readme
|
||||
before_script:
|
||||
- cargo readme -V
|
||||
script:
|
||||
- cargo readme -t README.tpl >README.md.new
|
||||
- cargo readme -t README.tpl -o README.md.new
|
||||
- diff README.md README.md.new
|
||||
|
||||
rustfmt:
|
||||
stage: test
|
||||
image: rustlang/rust:nightly-slim
|
||||
image:
|
||||
name: alpine:3.13
|
||||
before_script:
|
||||
- apk add rustup
|
||||
- rustup-init -qy --default-host x86_64-unknown-linux-musl --default-toolchain none </dev/null
|
||||
- source $CARGO_HOME/env
|
||||
- rustup toolchain install nightly --profile minimal --component rustfmt
|
||||
- cargo -V
|
||||
- cargo fmt --version
|
||||
script:
|
||||
- cargo fmt -- --check
|
||||
- cargo fmt --all -- --check
|
||||
- ./tests/ui/rustfmt.sh --check
|
||||
- ./openapi_type/tests/fail/rustfmt.sh --check
|
||||
|
||||
doc:
|
||||
stage: build
|
||||
|
@ -93,7 +119,7 @@ doc:
|
|||
before_script:
|
||||
- cargo -V
|
||||
script:
|
||||
- cargo doc --all-features
|
||||
- cargo doc --no-default-features --features full
|
||||
artifacts:
|
||||
paths:
|
||||
- target/doc/
|
||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -6,6 +6,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.1] - 2021-03-04
|
||||
### Changed
|
||||
- Pin version of `openapiv3` dependency to `0.3.2`
|
||||
|
||||
## [0.2.0] - 2021-02-27
|
||||
### Added
|
||||
- Support custom HTTP response headers
|
||||
- New `endpoint` router extension with associated `Endpoint` trait ([!18])
|
||||
- Support for custom endpoints using the `#[endpoint]` macro ([!19])
|
||||
- Support for `anyhow::Error` (or any type implementing `Into<HandlerError>`) in most responses
|
||||
- `swagger_ui` method to the OpenAPI router to render the specification using Swagger UI
|
||||
|
||||
### Changed
|
||||
- The cors handler can now copy headers from the request if desired
|
||||
- All fields of `Response` are now private
|
||||
- If not enabling the `openapi` feature, `without-openapi` has to be enabled
|
||||
- The endpoint macro attributes (`read`, `create`, ...) no longer take the resource ident and reject all unknown attributes ([!18])
|
||||
- The `ResourceResult` trait has been split into `IntoResponse` and `ResponseSchema`
|
||||
- `HashMap`'s keys are included in the generated OpenAPI spec (they defaulted to `type: string` previously)
|
||||
|
||||
### Removed
|
||||
- All pre-defined methods (`read`, `create`, ...) from our router extensions ([!18])
|
||||
- All pre-defined method traits (`ResourceRead`, ...) ([!18])
|
||||
|
||||
## [0.1.1] - 2020-12-28
|
||||
### Added
|
||||
- Support for `&mut State` parameters in method handlers
|
||||
|
@ -18,3 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [0.1.0] - 2020-10-02
|
||||
Previous changes are not tracked by this changelog file. Refer to the [releases](https://gitlab.com/msrd0/gotham-restful/-/releases) for the changelog.
|
||||
|
||||
|
||||
[!18]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/18
|
||||
[!19]: https://gitlab.com/msrd0/gotham-restful/-/merge_requests/19
|
||||
|
|
56
Cargo.toml
56
Cargo.toml
|
@ -1,61 +1,77 @@
|
|||
# -*- eval: (cargo-minor-mode 1) -*-
|
||||
|
||||
[workspace]
|
||||
members = ["derive", "example"]
|
||||
members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"]
|
||||
|
||||
[package]
|
||||
name = "gotham_restful"
|
||||
version = "0.1.1"
|
||||
version = "0.3.0-dev"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
edition = "2018"
|
||||
description = "RESTful additions for the gotham web framework"
|
||||
keywords = ["gotham", "rest", "restful", "web", "http"]
|
||||
license = "EPL-2.0 OR Apache-2.0"
|
||||
categories = ["web-programming", "web-programming::http-server"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||
include = ["src/**/*", "LICENSE.md", "LICENSE-*", "README.md", "CHANGELOG.md"]
|
||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
|
||||
[badges]
|
||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.13.0", optional = true }
|
||||
chrono = { version = "0.4.19", features = ["serde"], optional = true }
|
||||
cookie = { version = "0.14", optional = true }
|
||||
futures-core = "0.3.7"
|
||||
futures-util = "0.3.7"
|
||||
gotham = { version = "0.5.0", default-features = false }
|
||||
gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false }
|
||||
gotham_derive = "0.5.0"
|
||||
gotham_middleware_diesel = { version = "0.2.0", optional = true }
|
||||
gotham_restful_derive = { version = "0.1.1" }
|
||||
indexmap = { version = "1.3.2", optional = true }
|
||||
itertools = { version = "0.10.0", optional = true }
|
||||
jsonwebtoken = { version = "7.1.0", optional = true }
|
||||
gotham_restful_derive = "0.3.0-dev"
|
||||
log = "0.4.8"
|
||||
mime = "0.3.16"
|
||||
openapiv3 = { version = "0.3.2", optional = true }
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
serde_json = "1.0.58"
|
||||
uuid = { version = "0.8.1", optional = true }
|
||||
thiserror = "1.0"
|
||||
|
||||
# non-feature optional dependencies
|
||||
base64 = { version = "0.13.0", optional = true }
|
||||
cookie = { version = "0.15", optional = true }
|
||||
gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", optional = true }
|
||||
indexmap = { version = "1.3.2", optional = true }
|
||||
indoc = { version = "1.0", optional = true }
|
||||
jsonwebtoken = { version = "7.1.0", optional = true }
|
||||
once_cell = { version = "1.5", optional = true }
|
||||
openapiv3 = { version = "=0.3.2", optional = true }
|
||||
openapi_type = { version = "0.1.0-dev", optional = true }
|
||||
regex = { version = "1.4", optional = true }
|
||||
sha2 = { version = "0.9.3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
diesel = { version = "1.4.4", features = ["postgres"] }
|
||||
futures-executor = "0.3.5"
|
||||
paste = "1.0"
|
||||
pretty_env_logger = "0.4"
|
||||
tokio = { version = "1.0", features = ["time"], default-features = false }
|
||||
thiserror = "1.0.18"
|
||||
trybuild = "1.0.27"
|
||||
|
||||
[features]
|
||||
default = ["cors", "errorlog"]
|
||||
default = ["cors", "errorlog", "without-openapi"]
|
||||
full = ["auth", "cors", "database", "errorlog", "openapi"]
|
||||
|
||||
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
||||
cors = ["itertools"]
|
||||
errorlog = []
|
||||
cors = []
|
||||
database = ["gotham_restful_derive/database", "gotham_middleware_diesel"]
|
||||
openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"]
|
||||
errorlog = []
|
||||
|
||||
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
|
||||
without-openapi = []
|
||||
openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "openapi_type", "regex", "sha2"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
no-default-features = true
|
||||
features = ["full"]
|
||||
|
||||
[patch.crates-io]
|
||||
gotham_restful = { path = "." }
|
||||
gotham_restful_derive = { path = "./derive" }
|
||||
openapi_type = { path = "./openapi_type" }
|
||||
openapi_type_derive = { path = "./openapi_type_derive" }
|
||||
|
|
277
LICENSE-EPL
277
LICENSE-EPL
|
@ -1,277 +0,0 @@
|
|||
Eclipse Public License - v 2.0
|
||||
|
||||
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
|
||||
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
|
||||
|
||||
1. DEFINITIONS
|
||||
|
||||
"Contribution" means:
|
||||
|
||||
a) in the case of the initial Contributor, the initial content
|
||||
Distributed under this Agreement, and
|
||||
|
||||
b) in the case of each subsequent Contributor:
|
||||
i) changes to the Program, and
|
||||
ii) additions to the Program;
|
||||
where such changes and/or additions to the Program originate from
|
||||
and are Distributed by that particular Contributor. A Contribution
|
||||
"originates" from a Contributor if it was added to the Program by
|
||||
such Contributor itself or anyone acting on such Contributor's behalf.
|
||||
Contributions do not include changes or additions to the Program that
|
||||
are not Modified Works.
|
||||
|
||||
"Contributor" means any person or entity that Distributes the Program.
|
||||
|
||||
"Licensed Patents" mean patent claims licensable by a Contributor which
|
||||
are necessarily infringed by the use or sale of its Contribution alone
|
||||
or when combined with the Program.
|
||||
|
||||
"Program" means the Contributions Distributed in accordance with this
|
||||
Agreement.
|
||||
|
||||
"Recipient" means anyone who receives the Program under this Agreement
|
||||
or any Secondary License (as applicable), including Contributors.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source Code or other
|
||||
form, that is based on (or derived from) the Program and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship.
|
||||
|
||||
"Modified Works" shall mean any work in Source Code or other form that
|
||||
results from an addition to, deletion from, or modification of the
|
||||
contents of the Program, including, for purposes of clarity any new file
|
||||
in Source Code form that contains any contents of the Program. Modified
|
||||
Works shall not include works that contain only declarations,
|
||||
interfaces, types, classes, structures, or files of the Program solely
|
||||
in each case in order to link to, bind by name, or subclass the Program
|
||||
or Modified Works thereof.
|
||||
|
||||
"Distribute" means the acts of a) distributing or b) making available
|
||||
in any manner that enables the transfer of a copy.
|
||||
|
||||
"Source Code" means the form of a Program preferred for making
|
||||
modifications, including but not limited to software source code,
|
||||
documentation source, and configuration files.
|
||||
|
||||
"Secondary License" means either the GNU General Public License,
|
||||
Version 2.0, or any later versions of that license, including any
|
||||
exceptions or additional permissions as identified by the initial
|
||||
Contributor.
|
||||
|
||||
2. GRANT OF RIGHTS
|
||||
|
||||
a) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free copyright
|
||||
license to reproduce, prepare Derivative Works of, publicly display,
|
||||
publicly perform, Distribute and sublicense the Contribution of such
|
||||
Contributor, if any, and such Derivative Works.
|
||||
|
||||
b) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free patent
|
||||
license under Licensed Patents to make, use, sell, offer to sell,
|
||||
import and otherwise transfer the Contribution of such Contributor,
|
||||
if any, in Source Code or other form. This patent license shall
|
||||
apply to the combination of the Contribution and the Program if, at
|
||||
the time the Contribution is added by the Contributor, such addition
|
||||
of the Contribution causes such combination to be covered by the
|
||||
Licensed Patents. The patent license shall not apply to any other
|
||||
combinations which include the Contribution. No hardware per se is
|
||||
licensed hereunder.
|
||||
|
||||
c) Recipient understands that although each Contributor grants the
|
||||
licenses to its Contributions set forth herein, no assurances are
|
||||
provided by any Contributor that the Program does not infringe the
|
||||
patent or other intellectual property rights of any other entity.
|
||||
Each Contributor disclaims any liability to Recipient for claims
|
||||
brought by any other entity based on infringement of intellectual
|
||||
property rights or otherwise. As a condition to exercising the
|
||||
rights and licenses granted hereunder, each Recipient hereby
|
||||
assumes sole responsibility to secure any other intellectual
|
||||
property rights needed, if any. For example, if a third party
|
||||
patent license is required to allow Recipient to Distribute the
|
||||
Program, it is Recipient's responsibility to acquire that license
|
||||
before distributing the Program.
|
||||
|
||||
d) Each Contributor represents that to its knowledge it has
|
||||
sufficient copyright rights in its Contribution, if any, to grant
|
||||
the copyright license set forth in this Agreement.
|
||||
|
||||
e) Notwithstanding the terms of any Secondary License, no
|
||||
Contributor makes additional grants to any Recipient (other than
|
||||
those set forth in this Agreement) as a result of such Recipient's
|
||||
receipt of the Program under the terms of a Secondary License
|
||||
(if permitted under the terms of Section 3).
|
||||
|
||||
3. REQUIREMENTS
|
||||
|
||||
3.1 If a Contributor Distributes the Program in any form, then:
|
||||
|
||||
a) the Program must also be made available as Source Code, in
|
||||
accordance with section 3.2, and the Contributor must accompany
|
||||
the Program with a statement that the Source Code for the Program
|
||||
is available under this Agreement, and informs Recipients how to
|
||||
obtain it in a reasonable manner on or through a medium customarily
|
||||
used for software exchange; and
|
||||
|
||||
b) the Contributor may Distribute the Program under a license
|
||||
different than this Agreement, provided that such license:
|
||||
i) effectively disclaims on behalf of all other Contributors all
|
||||
warranties and conditions, express and implied, including
|
||||
warranties or conditions of title and non-infringement, and
|
||||
implied warranties or conditions of merchantability and fitness
|
||||
for a particular purpose;
|
||||
|
||||
ii) effectively excludes on behalf of all other Contributors all
|
||||
liability for damages, including direct, indirect, special,
|
||||
incidental and consequential damages, such as lost profits;
|
||||
|
||||
iii) does not attempt to limit or alter the recipients' rights
|
||||
in the Source Code under section 3.2; and
|
||||
|
||||
iv) requires any subsequent distribution of the Program by any
|
||||
party to be under a license that satisfies the requirements
|
||||
of this section 3.
|
||||
|
||||
3.2 When the Program is Distributed as Source Code:
|
||||
|
||||
a) it must be made available under this Agreement, or if the
|
||||
Program (i) is combined with other material in a separate file or
|
||||
files made available under a Secondary License, and (ii) the initial
|
||||
Contributor attached to the Source Code the notice described in
|
||||
Exhibit A of this Agreement, then the Program may be made available
|
||||
under the terms of such Secondary Licenses, and
|
||||
|
||||
b) a copy of this Agreement must be included with each copy of
|
||||
the Program.
|
||||
|
||||
3.3 Contributors may not remove or alter any copyright, patent,
|
||||
trademark, attribution notices, disclaimers of warranty, or limitations
|
||||
of liability ("notices") contained within the Program from any copy of
|
||||
the Program which they Distribute, provided that Contributors may add
|
||||
their own appropriate notices.
|
||||
|
||||
4. COMMERCIAL DISTRIBUTION
|
||||
|
||||
Commercial distributors of software may accept certain responsibilities
|
||||
with respect to end users, business partners and the like. While this
|
||||
license is intended to facilitate the commercial use of the Program,
|
||||
the Contributor who includes the Program in a commercial product
|
||||
offering should do so in a manner which does not create potential
|
||||
liability for other Contributors. Therefore, if a Contributor includes
|
||||
the Program in a commercial product offering, such Contributor
|
||||
("Commercial Contributor") hereby agrees to defend and indemnify every
|
||||
other Contributor ("Indemnified Contributor") against any losses,
|
||||
damages and costs (collectively "Losses") arising from claims, lawsuits
|
||||
and other legal actions brought by a third party against the Indemnified
|
||||
Contributor to the extent caused by the acts or omissions of such
|
||||
Commercial Contributor in connection with its distribution of the Program
|
||||
in a commercial product offering. The obligations in this section do not
|
||||
apply to any claims or Losses relating to any actual or alleged
|
||||
intellectual property infringement. In order to qualify, an Indemnified
|
||||
Contributor must: a) promptly notify the Commercial Contributor in
|
||||
writing of such claim, and b) allow the Commercial Contributor to control,
|
||||
and cooperate with the Commercial Contributor in, the defense and any
|
||||
related settlement negotiations. The Indemnified Contributor may
|
||||
participate in any such claim at its own expense.
|
||||
|
||||
For example, a Contributor might include the Program in a commercial
|
||||
product offering, Product X. That Contributor is then a Commercial
|
||||
Contributor. If that Commercial Contributor then makes performance
|
||||
claims, or offers warranties related to Product X, those performance
|
||||
claims and warranties are such Commercial Contributor's responsibility
|
||||
alone. Under this section, the Commercial Contributor would have to
|
||||
defend claims against the other Contributors related to those performance
|
||||
claims and warranties, and if a court requires any other Contributor to
|
||||
pay any damages as a result, the Commercial Contributor must pay
|
||||
those damages.
|
||||
|
||||
5. NO WARRANTY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS"
|
||||
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
|
||||
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF
|
||||
TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR
|
||||
PURPOSE. Each Recipient is solely responsible for determining the
|
||||
appropriateness of using and distributing the Program and assumes all
|
||||
risks associated with its exercise of rights under this Agreement,
|
||||
including but not limited to the risks and costs of program errors,
|
||||
compliance with applicable laws, damage to or loss of data, programs
|
||||
or equipment, and unavailability or interruption of operations.
|
||||
|
||||
6. DISCLAIMER OF LIABILITY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
|
||||
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
|
||||
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
|
||||
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. GENERAL
|
||||
|
||||
If any provision of this Agreement is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this Agreement, and without further
|
||||
action by the parties hereto, such provision shall be reformed to the
|
||||
minimum extent necessary to make such provision valid and enforceable.
|
||||
|
||||
If Recipient institutes patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||
Program itself (excluding combinations of the Program with other software
|
||||
or hardware) infringes such Recipient's patent(s), then such Recipient's
|
||||
rights granted under Section 2(b) shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
All Recipient's rights under this Agreement shall terminate if it
|
||||
fails to comply with any of the material terms or conditions of this
|
||||
Agreement and does not cure such failure in a reasonable period of
|
||||
time after becoming aware of such noncompliance. If all Recipient's
|
||||
rights under this Agreement terminate, Recipient agrees to cease use
|
||||
and distribution of the Program as soon as reasonably practicable.
|
||||
However, Recipient's obligations under this Agreement and any licenses
|
||||
granted by Recipient relating to the Program shall continue and survive.
|
||||
|
||||
Everyone is permitted to copy and distribute copies of this Agreement,
|
||||
but in order to avoid inconsistency the Agreement is copyrighted and
|
||||
may only be modified in the following manner. The Agreement Steward
|
||||
reserves the right to publish new versions (including revisions) of
|
||||
this Agreement from time to time. No one other than the Agreement
|
||||
Steward has the right to modify this Agreement. The Eclipse Foundation
|
||||
is the initial Agreement Steward. The Eclipse Foundation may assign the
|
||||
responsibility to serve as the Agreement Steward to a suitable separate
|
||||
entity. Each new version of the Agreement will be given a distinguishing
|
||||
version number. The Program (including Contributions) may always be
|
||||
Distributed subject to the version of the Agreement under which it was
|
||||
received. In addition, after a new version of the Agreement is published,
|
||||
Contributor may elect to Distribute the Program (including its
|
||||
Contributions) under the new version.
|
||||
|
||||
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
|
||||
receives no rights or licenses to the intellectual property of any
|
||||
Contributor under this Agreement, whether expressly, by implication,
|
||||
estoppel or otherwise. All rights in the Program not expressly granted
|
||||
under this Agreement are reserved. Nothing in this Agreement is intended
|
||||
to be enforceable by any entity that is not a Contributor or Recipient.
|
||||
No third-party beneficiary rights are created under this Agreement.
|
||||
|
||||
Exhibit A - Form of Secondary Licenses Notice
|
||||
|
||||
"This Source Code may also be made available under the following
|
||||
Secondary Licenses when the conditions for such availability set forth
|
||||
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
|
||||
version(s), and exceptions or additional permissions here}."
|
||||
|
||||
Simply including a copy of this Agreement, including this Exhibit A
|
||||
is not sufficient to license the Source Code under Secondary Licenses.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to
|
||||
look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
|
@ -1,6 +0,0 @@
|
|||
Copyright 2019 Dominic Meiser
|
||||
|
||||
The Gotham-Restful project is licensed under your option of:
|
||||
- [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache)
|
||||
- [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL)
|
||||
|
342
README.md
342
README.md
|
@ -1,342 +1,4 @@
|
|||
<div align="center">
|
||||
<h1>gotham-restful</h1>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
||||
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
||||
</a>
|
||||
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
|
||||
<img alt="coverage report" src="https://gitlab.com/msrd0/gotham-restful/badges/master/coverage.svg"/>
|
||||
</a>
|
||||
<a href="https://crates.io/crates/gotham_restful">
|
||||
<img alt="crates.io" src="https://img.shields.io/crates/v/gotham_restful.svg"/>
|
||||
</a>
|
||||
<a href="https://docs.rs/crate/gotham_restful">
|
||||
<img alt="docs.rs" src="https://docs.rs/gotham_restful/badge.svg"/>
|
||||
</a>
|
||||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
||||
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||
</a>
|
||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
||||
</a>
|
||||
</div>
|
||||
<br/>
|
||||
# Moved to GitHub
|
||||
|
||||
**Note:** The `stable` branch contains some bugfixes against the last release. The `master`
|
||||
branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be
|
||||
compatible with the new future / async stuff.
|
||||
This project has moved to GitHub: https://github.com/msrd0/gotham_restful
|
||||
|
||||
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
|
||||
for requests.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically parse **JSON** request and produce response bodies
|
||||
- Allow using **raw** request and response bodies
|
||||
- Convenient **macros** to create responses that can be registered with gotham's router
|
||||
- Auto-Generate an **OpenAPI** specification for your API
|
||||
- Manage **CORS** headers so you don't have to
|
||||
- Manage **Authentication** with JWT
|
||||
- Integrate diesel connection pools for easy **database** integration
|
||||
|
||||
## Safety
|
||||
|
||||
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
||||
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
|
||||
|
||||
## Methods
|
||||
|
||||
Assuming you assign `/foobar` to your resource, you can implement the following methods:
|
||||
|
||||
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ----------- | ------------------ | --------- | ----------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
| create | body | POST | /foobar |
|
||||
| change_all | body | PUT | /foobar |
|
||||
| change | id, body | PUT | /foobar/:id |
|
||||
| remove_all | | DELETE | /foobar |
|
||||
| remove | id | DELETE | /foobar/:id |
|
||||
|
||||
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
|
||||
simple example could look like this:
|
||||
|
||||
```rust
|
||||
/// Our RESTful resource.
|
||||
#[derive(Resource)]
|
||||
#[resource(read)]
|
||||
struct FooResource;
|
||||
|
||||
/// The return type of the foo read method.
|
||||
#[derive(Serialize)]
|
||||
struct Foo {
|
||||
id: u64
|
||||
}
|
||||
|
||||
/// The foo read method handler.
|
||||
#[read(FooResource)]
|
||||
fn read(id: u64) -> Success<Foo> {
|
||||
Foo { id }.into()
|
||||
}
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
Some methods require arguments. Those should be
|
||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
||||
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
||||
type needs to implement [`QueryStringExtractor`].
|
||||
|
||||
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
|
||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
||||
`Sync` there is unfortunately no more convenient way.
|
||||
|
||||
## Uploads and Downloads
|
||||
|
||||
By default, every request body is parsed from json, and every respone is converted to json using
|
||||
[serde_json]. However, you may also use raw bodies. This is an example where the request body
|
||||
is simply returned as the response again, no json parsing involved:
|
||||
|
||||
```rust
|
||||
#[derive(Resource)]
|
||||
#[resource(create)]
|
||||
struct ImageResource;
|
||||
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
||||
struct RawImage {
|
||||
content: Vec<u8>,
|
||||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(ImageResource)]
|
||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
To make life easier for common use-cases, this create offers a few features that might be helpful
|
||||
when you implement your web server. The complete feature list is
|
||||
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||
- `chrono` openapi support for chrono types
|
||||
- [`cors`](#cors-feature) CORS handling for all method handlers
|
||||
- [`database`](#database-feature) diesel middleware support
|
||||
- `errorlog` log errors returned from method handlers
|
||||
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
|
||||
- `uuid` openapi support for uuid
|
||||
|
||||
### Authentication Feature
|
||||
|
||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
||||
register a middleware that can automatically check for the existence of an JWT authentication
|
||||
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
|
||||
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
||||
None of this is currently supported by gotham's own JWT middleware.
|
||||
|
||||
A simple example that uses only a single secret could look like this:
|
||||
|
||||
```rust
|
||||
#[derive(Resource)]
|
||||
#[resource(read)]
|
||||
struct SecretResource;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Secret {
|
||||
id: u64,
|
||||
intended_for: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct AuthData {
|
||||
sub: String,
|
||||
exp: u64
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||
let intended_for = auth.ok()?.sub;
|
||||
Ok(Secret { id, intended_for })
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let auth: AuthMiddleware<AuthData, _> = AuthMiddleware::new(
|
||||
AuthSource::AuthorizationHeader,
|
||||
AuthValidation::default(),
|
||||
StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc")
|
||||
);
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build());
|
||||
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||
route.resource::<SecretResource>("secret");
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### CORS Feature
|
||||
|
||||
The cors feature allows an easy usage of this web server from other origins. By default, only
|
||||
the `Access-Control-Allow-Methods` header is touched. To change the behaviour, add your desired
|
||||
configuration as a middleware.
|
||||
|
||||
A simple example that allows authentication from every origin (note that `*` always disallows
|
||||
authentication), and every content type, could look like this:
|
||||
|
||||
```rust
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all() {
|
||||
// your handler
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cors = CorsConfig {
|
||||
origin: Origin::Copy,
|
||||
headers: vec![CONTENT_TYPE],
|
||||
max_age: 0,
|
||||
credentials: true
|
||||
};
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build());
|
||||
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||
route.resource::<FooResource>("foo");
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`]
|
||||
for an example.
|
||||
|
||||
### Database Feature
|
||||
|
||||
The database feature allows an easy integration of [diesel] into your handler functions. Please
|
||||
note however that due to the way gotham's diesel middleware implementation, it is not possible
|
||||
to run async code while holding a database connection. If you need to combine async and database,
|
||||
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
|
||||
|
||||
A simple non-async example could look like this:
|
||||
|
||||
```rust
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[derive(Queryable, Serialize)]
|
||||
struct Foo {
|
||||
id: i64,
|
||||
value: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||
foo::table.load(conn)
|
||||
}
|
||||
|
||||
type Repo = gotham_middleware_diesel::Repo<PgConnection>;
|
||||
|
||||
fn main() {
|
||||
let repo = Repo::new(&env::var("DATABASE_URL").unwrap());
|
||||
let diesel = DieselMiddleware::new(repo);
|
||||
|
||||
let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build());
|
||||
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||
route.resource::<FooResource>("foo");
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI Feature
|
||||
|
||||
The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section
|
||||
carefully both as a binary as well as a library author to avoid unwanted suprises.
|
||||
|
||||
In order to automatically create an openapi specification, gotham-restful needs knowledge over
|
||||
all routes and the types returned. `serde` does a great job at serialization but doesn't give
|
||||
enough type information, so all types used in the router need to implement `OpenapiType`. This
|
||||
can be derived for almoust any type and there should be no need to implement it manually. A simple
|
||||
example could look like this:
|
||||
|
||||
```rust
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[derive(OpenapiType, Serialize)]
|
||||
struct Foo {
|
||||
bar: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
fn read_all() -> Success<Foo> {
|
||||
Foo { bar: "Hello World".to_owned() }.into()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
let info = OpenapiInfo {
|
||||
title: "My Foo API".to_owned(),
|
||||
version: "0.1.0".to_owned(),
|
||||
urls: vec!["https://example.org/foo/api/v1".to_owned()]
|
||||
};
|
||||
route.with_openapi(info, |mut route| {
|
||||
route.resource::<FooResource>("foo");
|
||||
route.get_openapi("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
|
||||
in different languages without worying to exactly replicate your api in each of those languages.
|
||||
|
||||
However, as of right now there is one caveat. If you wrote code before enabling the openapi feature,
|
||||
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
|
||||
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
|
||||
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
|
||||
In other words, put
|
||||
|
||||
```toml
|
||||
[features]
|
||||
openapi = ["gotham-restful/openapi"]
|
||||
```
|
||||
|
||||
into your libraries `Cargo.toml` and use the following for all types used with handlers:
|
||||
|
||||
```rust
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
struct Foo;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under your option of:
|
||||
- [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache)
|
||||
- [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL)
|
||||
|
||||
|
||||
[diesel]: https://diesel.rs/
|
||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||
[gotham]: https://gotham.rs/
|
||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
||||
[`CorsRoute`]: trait.CorsRoute.html
|
||||
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
|
||||
[`RequestBody`]: trait.RequestBody.html
|
||||
[`State`]: ../gotham/state/struct.State.html
|
||||
|
|
58
README.tpl
58
README.tpl
|
@ -1,24 +1,16 @@
|
|||
<div align="center">
|
||||
<h1>gotham-restful</h1>
|
||||
</div>
|
||||
<div align="center">
|
||||
<br/>
|
||||
<div>
|
||||
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
||||
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
||||
</a>
|
||||
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
|
||||
<img alt="coverage report" src="https://gitlab.com/msrd0/gotham-restful/badges/master/coverage.svg"/>
|
||||
</a>
|
||||
<a href="https://crates.io/crates/gotham_restful">
|
||||
<img alt="crates.io" src="https://img.shields.io/crates/v/gotham_restful.svg"/>
|
||||
</a>
|
||||
<a href="https://docs.rs/crate/gotham_restful">
|
||||
<img alt="docs.rs" src="https://docs.rs/gotham_restful/badge.svg"/>
|
||||
</a>
|
||||
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
||||
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||
</a>
|
||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
|
||||
<a href="https://blog.rust-lang.org/2020/12/31/Rust-1.49.0.html">
|
||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.49+-orange.svg"/>
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
||||
|
@ -26,8 +18,44 @@
|
|||
</div>
|
||||
<br/>
|
||||
|
||||
**Note:** The `stable` branch contains some bugfixes against the last release. The `master`
|
||||
branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be
|
||||
compatible with the new future / async stuff.
|
||||
This repository contains the following crates:
|
||||
|
||||
- **gotham_restful**
|
||||
[](https://crates.io/crates/gotham_restful)
|
||||
[](https://docs.rs/gotham_restful)
|
||||
- **gotham_restful_derive**
|
||||
[](https://crates.io/crates/gotham_restful_derive)
|
||||
[](https://docs.rs/gotham_restful_derive)
|
||||
- **openapi_type**
|
||||
[](https://crates.io/crates/openapi_type)
|
||||
[](https://docs.rs/crate/openapi_type)
|
||||
- **openapi_type_derive**
|
||||
[](https://crates.io/crates/openapi_type_derive)
|
||||
[](https://docs.rs/crate/openapi_type_derive)
|
||||
|
||||
# gotham-restful
|
||||
|
||||
{{readme}}
|
||||
|
||||
## Versioning
|
||||
|
||||
Like all rust crates, this crate will follow semantic versioning guidelines. However, changing
|
||||
the MSRV (minimum supported rust version) is not considered a breaking change.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2020-2021 Dominic Meiser and [contributors](https://gitlab.com/msrd0/gotham-restful/-/graphs/master).
|
||||
|
||||
```
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
[package]
|
||||
name = "gotham_restful_derive"
|
||||
version = "0.1.1"
|
||||
version = "0.3.0-dev"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
edition = "2018"
|
||||
description = "RESTful additions for the gotham web framework - Derive"
|
||||
description = "Derive macros for gotham_restful"
|
||||
keywords = ["gotham", "rest", "restful", "web", "http"]
|
||||
license = "EPL-2.0 OR Apache-2.0"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||
workspace = ".."
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
@ -17,9 +18,11 @@ proc-macro = true
|
|||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||
|
||||
[dependencies]
|
||||
heck = "0.3.1"
|
||||
once_cell = "1.5"
|
||||
paste = "1.0"
|
||||
proc-macro2 = "1.0.13"
|
||||
quote = "1.0.6"
|
||||
regex = "1.4"
|
||||
syn = { version = "1.0.22", features = ["full"] }
|
||||
|
||||
[features]
|
||||
|
|
1
derive/LICENSE
Symbolic link
1
derive/LICENSE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../LICENSE
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-Apache
|
|
@ -1 +0,0 @@
|
|||
../LICENSE-EPL
|
|
@ -1 +0,0 @@
|
|||
../LICENSE.md
|
590
derive/src/endpoint.rs
Normal file
590
derive/src/endpoint.rs
Normal file
|
@ -0,0 +1,590 @@
|
|||
use crate::util::{CollectToResult, ExpectLit, PathEndsWith};
|
||||
use once_cell::sync::Lazy;
|
||||
use paste::paste;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
||||
use regex::Regex;
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
parse::Parse, spanned::Spanned, Attribute, AttributeArgs, Error, Expr, FnArg, ItemFn, LitBool, LitStr, Meta, NestedMeta,
|
||||
PatType, Result, ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum EndpointType {
|
||||
ReadAll,
|
||||
Read,
|
||||
Search,
|
||||
Create,
|
||||
UpdateAll,
|
||||
Update,
|
||||
DeleteAll,
|
||||
Delete,
|
||||
Custom {
|
||||
method: Option<Expr>,
|
||||
uri: Option<LitStr>,
|
||||
params: Option<LitBool>,
|
||||
body: Option<LitBool>
|
||||
}
|
||||
}
|
||||
|
||||
impl EndpointType {
|
||||
pub fn custom() -> Self {
|
||||
Self::Custom {
|
||||
method: None,
|
||||
uri: None,
|
||||
params: None,
|
||||
body: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! endpoint_type_setter {
|
||||
($name:ident : $ty:ty) => {
|
||||
impl EndpointType {
|
||||
paste! {
|
||||
fn [<set_ $name>](&mut self, span: Span, [<new_ $name>]: $ty) -> Result<()> {
|
||||
match self {
|
||||
Self::Custom { $name, .. } if $name.is_some() => {
|
||||
Err(Error::new(span, concat!("`", concat!(stringify!($name), "` must not appear more than once"))))
|
||||
},
|
||||
Self::Custom { $name, .. } => {
|
||||
*$name = Some([<new_ $name>]);
|
||||
Ok(())
|
||||
},
|
||||
_ => Err(Error::new(span, concat!("`", concat!(stringify!($name), "` can only be used on custom endpoints"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
endpoint_type_setter!(method: Expr);
|
||||
endpoint_type_setter!(uri: LitStr);
|
||||
endpoint_type_setter!(params: LitBool);
|
||||
endpoint_type_setter!(body: LitBool);
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static URI_PLACEHOLDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r#"(^|/):(?P<name>[^/]+)(/|$)"#).unwrap());
|
||||
|
||||
impl EndpointType {
|
||||
fn http_method(&self) -> Option<TokenStream> {
|
||||
let hyper_method = quote!(::gotham_restful::gotham::hyper::Method);
|
||||
match self {
|
||||
Self::ReadAll | Self::Read | Self::Search => Some(quote!(#hyper_method::GET)),
|
||||
Self::Create => Some(quote!(#hyper_method::POST)),
|
||||
Self::UpdateAll | Self::Update => Some(quote!(#hyper_method::PUT)),
|
||||
Self::DeleteAll | Self::Delete => Some(quote!(#hyper_method::DELETE)),
|
||||
Self::Custom { method, .. } => method.as_ref().map(ToTokens::to_token_stream)
|
||||
}
|
||||
}
|
||||
|
||||
fn uri(&self) -> Option<TokenStream> {
|
||||
match self {
|
||||
Self::ReadAll | Self::Create | Self::UpdateAll | Self::DeleteAll => Some(quote!("")),
|
||||
Self::Read | Self::Update | Self::Delete => Some(quote!(":id")),
|
||||
Self::Search => Some(quote!("search")),
|
||||
Self::Custom { uri, .. } => uri.as_ref().map(ToTokens::to_token_stream)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
Self::Custom { uri, .. } => LitBool {
|
||||
value: uri
|
||||
.as_ref()
|
||||
.map(|uri| URI_PLACEHOLDER_REGEX.is_match(&uri.value()))
|
||||
.unwrap_or(false),
|
||||
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::NoopExtractor)
|
||||
},
|
||||
Self::Read | Self::Update | Self::Delete => quote!(::gotham_restful::private::IdPlaceholder::<#arg_ty>),
|
||||
Self::Custom { .. } => {
|
||||
if self.has_placeholders().value {
|
||||
arg_ty.to_token_stream()
|
||||
} else {
|
||||
quote!(::gotham_restful::NoopExtractor)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
Self::Custom { params, .. } => params.clone().unwrap_or_else(|| LitBool {
|
||||
value: false,
|
||||
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::NoopExtractor)
|
||||
},
|
||||
Self::Search => quote!(#arg_ty),
|
||||
Self::Custom { .. } => {
|
||||
if self.needs_params().value {
|
||||
arg_ty.to_token_stream()
|
||||
} else {
|
||||
quote!(::gotham_restful::NoopExtractor)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
Self::Custom { body, .. } => body.clone().unwrap_or_else(|| LitBool {
|
||||
value: false,
|
||||
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),
|
||||
Self::Custom { .. } => {
|
||||
if self.needs_body().value {
|
||||
arg_ty.to_token_stream()
|
||||
} else {
|
||||
quote!(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<LitStr>) -> 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<LitStr>) -> Option<TokenStream> {
|
||||
None
|
||||
}
|
||||
|
||||
fn expand_wants_auth(wants_auth: Option<LitBool>, default: bool) -> TokenStream {
|
||||
let wants_auth = wants_auth.unwrap_or_else(|| 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)
|
||||
}
|
||||
|
||||
// clippy doesn't realize that vectors can be used in closures
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_collect))]
|
||||
fn expand_endpoint_type(mut 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 debug: bool = false;
|
||||
let mut operation_id: Option<LitStr> = None;
|
||||
let mut wants_auth: Option<LitBool> = None;
|
||||
for meta in attrs {
|
||||
match meta {
|
||||
NestedMeta::Meta(Meta::NameValue(kv)) => {
|
||||
if kv.path.ends_with("debug") {
|
||||
debug = kv.lit.expect_bool()?.value;
|
||||
} else if kv.path.ends_with("operation_id") {
|
||||
operation_id = Some(kv.lit.expect_str()?);
|
||||
} else if kv.path.ends_with("wants_auth") {
|
||||
wants_auth = Some(kv.lit.expect_bool()?);
|
||||
} else if kv.path.ends_with("method") {
|
||||
ty.set_method(kv.path.span(), kv.lit.expect_str()?.parse_with(Expr::parse)?)?;
|
||||
} else if kv.path.ends_with("uri") {
|
||||
ty.set_uri(kv.path.span(), kv.lit.expect_str()?)?;
|
||||
} else if kv.path.ends_with("params") {
|
||||
ty.set_params(kv.path.span(), kv.lit.expect_bool()?)?;
|
||||
} else if kv.path.ends_with("body") {
|
||||
ty.set_body(kv.path.span(), kv.lit.expect_bool()?)?;
|
||||
} 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 output_typedef = quote_spanned!(output_ty.span() => type Output = #output_ty;);
|
||||
|
||||
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().ok_or_else(|| {
|
||||
Error::new(
|
||||
Span::call_site(),
|
||||
"Missing `method` attribute (e.g. `#[endpoint(method = \"gotham_restful::gotham::hyper::Method::GET\")]`)"
|
||||
)
|
||||
})?;
|
||||
let uri = ty.uri().ok_or_else(|| {
|
||||
Error::new(
|
||||
Span::call_site(),
|
||||
"Missing `uri` attribute (e.g. `#[endpoint(uri = \"custom_endpoint\")]`)"
|
||||
)
|
||||
})?;
|
||||
let has_placeholders = ty.has_placeholders();
|
||||
let placeholder_ty = ty.placeholders_ty(next_arg_ty(!has_placeholders.value)?);
|
||||
let placeholder_typedef = quote_spanned!(placeholder_ty.span() => type Placeholders = #placeholder_ty;);
|
||||
let needs_params = ty.needs_params();
|
||||
let params_ty = ty.params_ty(next_arg_ty(!needs_params.value)?);
|
||||
let params_typedef = quote_spanned!(params_ty.span() => type Params = #params_ty;);
|
||||
let needs_body = ty.needs_body();
|
||||
let body_ty = ty.body_ty(next_arg_ty(!needs_body.value)?);
|
||||
let body_typedef = quote_spanned!(body_ty.span() => type Body = #body_ty;);
|
||||
|
||||
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 {
|
||||
if matches!(ty, EndpointType::Custom { .. }) {
|
||||
handle_args.push(quote!(placeholders));
|
||||
} else {
|
||||
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 as ::std::default::Default>::default())
|
||||
}
|
||||
|
||||
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::private::Repo<#conn_ty>>::borrow_from(state).clone();
|
||||
};
|
||||
handle_content = quote! {
|
||||
repo.run::<_, _, ()>(move |conn| {
|
||||
Ok({ #handle_content })
|
||||
}).await.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
Ok(quote! {
|
||||
use ::gotham_restful::private::FutureExt as _;
|
||||
use ::gotham_restful::gotham::state::FromState 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()));
|
||||
let code = 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()
|
||||
}
|
||||
|
||||
#output_typedef
|
||||
|
||||
fn has_placeholders() -> bool {
|
||||
#has_placeholders
|
||||
}
|
||||
#placeholder_typedef
|
||||
|
||||
fn needs_params() -> bool {
|
||||
#needs_params
|
||||
}
|
||||
#params_typedef
|
||||
|
||||
fn needs_body() -> bool {
|
||||
#needs_body
|
||||
}
|
||||
#body_typedef
|
||||
|
||||
fn handle<'a>(
|
||||
state: &'a mut ::gotham_restful::gotham::state::State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: ::std::option::Option<Self::Body>
|
||||
) -> ::gotham_restful::private::BoxFuture<'a, Self::Output> {
|
||||
#handle_content
|
||||
}
|
||||
|
||||
#operation_id
|
||||
#wants_auth
|
||||
}
|
||||
};
|
||||
};
|
||||
if debug {
|
||||
eprintln!("{}", code);
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||
#![deny(broken_intra_doc_links)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
|
@ -5,20 +9,23 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re
|
|||
|
||||
mod util;
|
||||
|
||||
mod endpoint;
|
||||
use endpoint::{expand_endpoint, EndpointType};
|
||||
|
||||
mod from_body;
|
||||
use from_body::expand_from_body;
|
||||
mod method;
|
||||
use method::{expand_method, Method};
|
||||
|
||||
mod request_body;
|
||||
use request_body::expand_request_body;
|
||||
|
||||
mod resource;
|
||||
use resource::expand_resource;
|
||||
|
||||
mod resource_error;
|
||||
use resource_error::expand_resource_error;
|
||||
#[cfg(feature = "openapi")]
|
||||
mod openapi_type;
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::expand_openapi_type;
|
||||
|
||||
mod private_openapi_trait;
|
||||
use private_openapi_trait::expand_private_openapi_trait;
|
||||
|
||||
#[inline]
|
||||
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
||||
|
@ -54,12 +61,6 @@ pub fn derive_from_body(input: TokenStream) -> TokenStream {
|
|||
expand_derive(input, expand_from_body)
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
#[proc_macro_derive(OpenapiType, attributes(openapi))]
|
||||
pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
|
||||
expand_derive(input, expand_openapi_type)
|
||||
}
|
||||
|
||||
#[proc_macro_derive(RequestBody, attributes(supported_types))]
|
||||
pub fn derive_request_body(input: TokenStream) -> TokenStream {
|
||||
expand_derive(input, expand_request_body)
|
||||
|
@ -75,42 +76,54 @@ pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
|||
expand_derive(input, expand_resource_error)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::custom(), attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item))
|
||||
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Delete, attr, item))
|
||||
}
|
||||
|
||||
/// PRIVATE MACRO - DO NOT USE
|
||||
#[doc(hidden)]
|
||||
#[proc_macro_attribute]
|
||||
pub fn _private_openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_macro(attr, item, expand_private_openapi_trait)
|
||||
}
|
||||
|
|
|
@ -1,466 +0,0 @@
|
|||
use crate::util::CollectToResult;
|
||||
use heck::{CamelCase, SnakeCase};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use std::str::FromStr;
|
||||
use syn::{
|
||||
spanned::Spanned, Attribute, AttributeArgs, Error, FnArg, ItemFn, Lit, LitBool, Meta, NestedMeta, PatType, Path, Result,
|
||||
ReturnType, Type
|
||||
};
|
||||
|
||||
pub enum Method {
|
||||
ReadAll,
|
||||
Read,
|
||||
Search,
|
||||
Create,
|
||||
ChangeAll,
|
||||
Change,
|
||||
RemoveAll,
|
||||
Remove
|
||||
}
|
||||
|
||||
impl FromStr for Method {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(str: &str) -> Result<Self> {
|
||||
match str {
|
||||
"ReadAll" | "read_all" => Ok(Self::ReadAll),
|
||||
"Read" | "read" => Ok(Self::Read),
|
||||
"Search" | "search" => Ok(Self::Search),
|
||||
"Create" | "create" => Ok(Self::Create),
|
||||
"ChangeAll" | "change_all" => Ok(Self::ChangeAll),
|
||||
"Change" | "change" => Ok(Self::Change),
|
||||
"RemoveAll" | "remove_all" => Ok(Self::RemoveAll),
|
||||
"Remove" | "remove" => Ok(Self::Remove),
|
||||
_ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn type_names(&self) -> Vec<&'static str> {
|
||||
use Method::*;
|
||||
|
||||
match self {
|
||||
ReadAll | RemoveAll => vec![],
|
||||
Read | Remove => vec!["ID"],
|
||||
Search => vec!["Query"],
|
||||
Create | ChangeAll => vec!["Body"],
|
||||
Change => vec!["ID", "Body"]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trait_ident(&self) -> Ident {
|
||||
use Method::*;
|
||||
|
||||
let name = match self {
|
||||
ReadAll => "ReadAll",
|
||||
Read => "Read",
|
||||
Search => "Search",
|
||||
Create => "Create",
|
||||
ChangeAll => "ChangeAll",
|
||||
Change => "Change",
|
||||
RemoveAll => "RemoveAll",
|
||||
Remove => "Remove"
|
||||
};
|
||||
format_ident!("Resource{}", name)
|
||||
}
|
||||
|
||||
pub fn fn_ident(&self) -> Ident {
|
||||
use Method::*;
|
||||
|
||||
let name = match self {
|
||||
ReadAll => "read_all",
|
||||
Read => "read",
|
||||
Search => "search",
|
||||
Create => "create",
|
||||
ChangeAll => "change_all",
|
||||
Change => "change",
|
||||
RemoveAll => "remove_all",
|
||||
Remove => "remove"
|
||||
};
|
||||
format_ident!("{}", name)
|
||||
}
|
||||
|
||||
pub fn handler_struct_ident(&self, resource: &str) -> Ident {
|
||||
format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident())
|
||||
}
|
||||
|
||||
pub fn setup_ident(&self, resource: &str) -> Ident {
|
||||
format_ident!("_gotham_restful_{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MethodArgumentType {
|
||||
StateRef,
|
||||
StateMutRef,
|
||||
MethodArg(Type),
|
||||
DatabaseConnection(Type),
|
||||
AuthStatus(Type),
|
||||
AuthStatusRef(Type)
|
||||
}
|
||||
|
||||
impl MethodArgumentType {
|
||||
fn is_method_arg(&self) -> bool {
|
||||
matches!(self, Self::MethodArg(_))
|
||||
}
|
||||
|
||||
fn is_database_conn(&self) -> bool {
|
||||
matches!(self, Self::DatabaseConnection(_))
|
||||
}
|
||||
|
||||
fn is_auth_status(&self) -> bool {
|
||||
matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_))
|
||||
}
|
||||
|
||||
fn ty(&self) -> Option<&Type> {
|
||||
match self {
|
||||
Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_ty(&self) -> Option<TokenStream> {
|
||||
self.ty().map(|ty| quote!(#ty))
|
||||
}
|
||||
}
|
||||
|
||||
struct MethodArgument {
|
||||
ident: Ident,
|
||||
ident_span: Span,
|
||||
ty: MethodArgumentType
|
||||
}
|
||||
|
||||
impl Spanned for MethodArgument {
|
||||
fn span(&self) -> Span {
|
||||
self.ident_span
|
||||
}
|
||||
}
|
||||
|
||||
fn interpret_arg_ty(attrs: &[Attribute], name: &str, ty: Type) -> Result<MethodArgumentType> {
|
||||
let attr = attrs
|
||||
.iter()
|
||||
.find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg"))
|
||||
.map(|arg| arg.tokens.to_string());
|
||||
|
||||
// TODO issue a warning for _state usage once diagnostics become stable
|
||||
if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) {
|
||||
return match ty {
|
||||
Type::Reference(ty) => Ok(if ty.mutability.is_none() {
|
||||
MethodArgumentType::StateRef
|
||||
} else {
|
||||
MethodArgumentType::StateMutRef
|
||||
}),
|
||||
_ => Err(Error::new(
|
||||
ty.span(),
|
||||
"The state parameter has to be a (mutable) reference to gotham_restful::State"
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) {
|
||||
return Ok(match ty {
|
||||
Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem),
|
||||
ty => MethodArgumentType::AuthStatus(ty)
|
||||
});
|
||||
}
|
||||
|
||||
if cfg!(feature = "database")
|
||||
&& (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn"))
|
||||
{
|
||||
return Ok(MethodArgumentType::DatabaseConnection(match ty {
|
||||
Type::Reference(ty) => *ty.elem,
|
||||
ty => ty
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(MethodArgumentType::MethodArg(ty))
|
||||
}
|
||||
|
||||
fn interpret_arg(index: usize, arg: &PatType) -> Result<MethodArgument> {
|
||||
let pat = &arg.pat;
|
||||
let ident = format_ident!("arg{}", index);
|
||||
let orig_name = quote!(#pat);
|
||||
let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?;
|
||||
|
||||
Ok(MethodArgument {
|
||||
ident,
|
||||
ident_span: arg.pat.span(),
|
||||
ty
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn expand_operation_id(attrs: &[NestedMeta]) -> TokenStream {
|
||||
let mut operation_id: Option<&Lit> = None;
|
||||
for meta in attrs {
|
||||
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
|
||||
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) {
|
||||
operation_id = Some(&kv.lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match operation_id {
|
||||
Some(operation_id) => quote! {
|
||||
fn operation_id() -> Option<String>
|
||||
{
|
||||
Some(#operation_id.to_string())
|
||||
}
|
||||
},
|
||||
None => quote!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
fn expand_operation_id(_: &[NestedMeta]) -> TokenStream {
|
||||
quote!()
|
||||
}
|
||||
|
||||
fn expand_wants_auth(attrs: &[NestedMeta], default: bool) -> TokenStream {
|
||||
let default_lit = Lit::Bool(LitBool {
|
||||
value: default,
|
||||
span: Span::call_site()
|
||||
});
|
||||
let mut wants_auth = &default_lit;
|
||||
for meta in attrs {
|
||||
if let NestedMeta::Meta(Meta::NameValue(kv)) = meta {
|
||||
if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) {
|
||||
wants_auth = &kv.lit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
fn wants_auth() -> bool
|
||||
{
|
||||
#wants_auth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::comparison_chain)]
|
||||
fn setup_body(
|
||||
method: &Method,
|
||||
fun: &ItemFn,
|
||||
attrs: &[NestedMeta],
|
||||
resource_name: &str,
|
||||
resource_path: &Path
|
||||
) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
|
||||
let fun_ident = &fun.sig.ident;
|
||||
let fun_is_async = fun.sig.asyncness.is_some();
|
||||
|
||||
if let Some(unsafety) = fun.sig.unsafety {
|
||||
return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe"));
|
||||
}
|
||||
|
||||
let trait_ident = method.trait_ident();
|
||||
let method_ident = method.fn_ident();
|
||||
let handler_ident = method.handler_struct_ident(resource_name);
|
||||
|
||||
let (ret, is_no_content) = match &fun.sig.output {
|
||||
ReturnType::Default => (quote!(#krate::NoContent), true),
|
||||
ReturnType::Type(_, ty) => (quote!(#ty), false)
|
||||
};
|
||||
|
||||
// some default idents we'll need
|
||||
let state_ident = format_ident!("state");
|
||||
let repo_ident = format_ident!("repo");
|
||||
let conn_ident = format_ident!("conn");
|
||||
let auth_ident = format_ident!("auth");
|
||||
let res_ident = format_ident!("res");
|
||||
|
||||
// extract arguments into pattern, ident and type
|
||||
let args = fun
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, arg)| match arg {
|
||||
FnArg::Typed(arg) => interpret_arg(i, arg),
|
||||
FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter"))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
// extract the generic parameters to use
|
||||
let ty_names = method.type_names();
|
||||
let ty_len = ty_names.len();
|
||||
let generics_args: Vec<&MethodArgument> = args.iter().filter(|arg| (*arg).ty.is_method_arg()).collect();
|
||||
if generics_args.len() > ty_len {
|
||||
return Err(Error::new(generics_args[ty_len].span(), "Too many arguments"));
|
||||
} else if generics_args.len() < ty_len {
|
||||
return Err(Error::new(fun_ident.span(), "Too few arguments"));
|
||||
}
|
||||
let generics: Vec<TokenStream> = generics_args
|
||||
.iter()
|
||||
.map(|arg| arg.ty.quote_ty().unwrap())
|
||||
.zip(ty_names)
|
||||
.map(|(arg, name)| {
|
||||
let ident = format_ident!("{}", name);
|
||||
quote!(type #ident = #arg;)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// extract the definition of our method
|
||||
let mut args_def: Vec<TokenStream> = args
|
||||
.iter()
|
||||
.filter(|arg| (*arg).ty.is_method_arg())
|
||||
.map(|arg| {
|
||||
let ident = &arg.ident;
|
||||
let ty = arg.ty.quote_ty();
|
||||
quote!(#ident : #ty)
|
||||
})
|
||||
.collect();
|
||||
args_def.insert(0, quote!(mut #state_ident : #krate::State));
|
||||
|
||||
// extract the arguments to pass over to the supplied method
|
||||
let args_pass: Vec<TokenStream> = args
|
||||
.iter()
|
||||
.map(|arg| match (&arg.ty, &arg.ident) {
|
||||
(MethodArgumentType::StateRef, _) => quote!(&#state_ident),
|
||||
(MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident),
|
||||
(MethodArgumentType::MethodArg(_), ident) => quote!(#ident),
|
||||
(MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident),
|
||||
(MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident),
|
||||
(MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// prepare the method block
|
||||
let mut block = quote!(#fun_ident(#(#args_pass),*));
|
||||
let mut state_block = quote!();
|
||||
if fun_is_async {
|
||||
if let Some(arg) = args.iter().find(|arg| matches!((*arg).ty, MethodArgumentType::StateRef)) {
|
||||
return Err(Error::new(
|
||||
arg.span(),
|
||||
"async fn must not take &State as an argument as State is not Sync, consider taking &mut State"
|
||||
));
|
||||
}
|
||||
block = quote!(#block.await);
|
||||
}
|
||||
if is_no_content {
|
||||
block = quote!(#block; Default::default())
|
||||
}
|
||||
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) {
|
||||
if fun_is_async {
|
||||
return Err(Error::new(
|
||||
arg.span(),
|
||||
"async fn is not supported when database support is required, consider boxing"
|
||||
));
|
||||
}
|
||||
let conn_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone();
|
||||
};
|
||||
block = quote! {
|
||||
{
|
||||
let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| {
|
||||
let #res_ident = { #block };
|
||||
Ok((#state_ident, #res_ident))
|
||||
}).await.unwrap();
|
||||
#state_ident = #res_ident.0;
|
||||
#res_ident.1
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
state_block = quote! {
|
||||
#state_block
|
||||
let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone();
|
||||
};
|
||||
}
|
||||
|
||||
// prepare the where clause
|
||||
let mut where_clause = quote!(#resource_path : #krate::Resource,);
|
||||
for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) {
|
||||
let auth_ty = arg.ty.quote_ty();
|
||||
where_clause = quote!(#where_clause #auth_ty : Clone,);
|
||||
}
|
||||
|
||||
// attribute generated code
|
||||
let operation_id = expand_operation_id(attrs);
|
||||
let wants_auth = expand_wants_auth(attrs, args.iter().any(|arg| (*arg).ty.is_auth_status()));
|
||||
|
||||
// put everything together
|
||||
let mut dummy = format_ident!("_IMPL_RESOURCEMETHOD_FOR_{}", fun_ident);
|
||||
dummy.set_span(Span::call_site());
|
||||
Ok(quote! {
|
||||
struct #handler_ident;
|
||||
|
||||
impl #krate::ResourceMethod for #handler_ident {
|
||||
type Res = #ret;
|
||||
|
||||
#operation_id
|
||||
#wants_auth
|
||||
}
|
||||
|
||||
impl #krate::#trait_ident for #handler_ident
|
||||
where #where_clause
|
||||
{
|
||||
#(#generics)*
|
||||
|
||||
fn #method_ident(#(#args_def),*) -> std::pin::Pin<Box<dyn std::future::Future<Output = (#krate::State, #ret)> + Send>> {
|
||||
#[allow(unused_imports)]
|
||||
use #krate::{export::FutureExt, FromState};
|
||||
|
||||
#state_block
|
||||
|
||||
async move {
|
||||
let #res_ident = { #block };
|
||||
(#state_ident, #res_ident)
|
||||
}.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
route.#method_ident::<#handler_ident>();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn expand_method(method: Method, mut attrs: AttributeArgs, fun: ItemFn) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
|
||||
// parse attributes
|
||||
if attrs.len() < 1 {
|
||||
return Err(Error::new(
|
||||
Span::call_site(),
|
||||
"Missing Resource struct. Example: #[read_all(MyResource)]"
|
||||
));
|
||||
}
|
||||
let resource_path = match attrs.remove(0) {
|
||||
NestedMeta::Meta(Meta::Path(path)) => path,
|
||||
p => {
|
||||
return Err(Error::new(
|
||||
p.span(),
|
||||
"Expected name of the Resource struct this method belongs to"
|
||||
))
|
||||
},
|
||||
};
|
||||
let resource_name = resource_path
|
||||
.segments
|
||||
.last()
|
||||
.map(|s| s.ident.to_string())
|
||||
.ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?;
|
||||
|
||||
let fun_vis = &fun.vis;
|
||||
let setup_ident = method.setup_ident(&resource_name);
|
||||
let setup_body = match setup_body(&method, &fun, &attrs, &resource_name, &resource_path) {
|
||||
Ok(body) => body,
|
||||
Err(err) => err.to_compile_error()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
#fun
|
||||
|
||||
#[deny(dead_code)]
|
||||
#[doc(hidden)]
|
||||
/// `gotham_restful` implementation detail.
|
||||
#fun_vis fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D) {
|
||||
#setup_body
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
use crate::util::{remove_parens, CollectToResult};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
parse_macro_input, spanned::Spanned, Attribute, AttributeArgs, Data, DataEnum, DataStruct, DeriveInput, Error, Field,
|
||||
Fields, GenericParam, Generics, Lit, LitStr, Meta, NestedMeta, Result, Variant
|
||||
};
|
||||
|
||||
pub fn expand_openapi_type(input: DeriveInput) -> Result<TokenStream> {
|
||||
match (input.ident, input.generics, input.attrs, input.data) {
|
||||
(ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum),
|
||||
(ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt),
|
||||
(_, _, _, Data::Union(uni)) => Err(Error::new(
|
||||
uni.union_token.span(),
|
||||
"#[derive(OpenapiType)] only works for structs and enums"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_where(generics: &Generics) -> TokenStream {
|
||||
if generics.params.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
|
||||
let krate = super::krate();
|
||||
let idents = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|param| match param {
|
||||
GenericParam::Type(ty) => Some(ty.ident.clone()),
|
||||
_ => None
|
||||
})
|
||||
.filter(|param| param.is_some())
|
||||
.map(|param| param.unwrap());
|
||||
|
||||
quote! {
|
||||
where #(#idents : #krate::OpenapiType),*
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Attrs {
|
||||
nullable: bool,
|
||||
rename: Option<String>
|
||||
}
|
||||
|
||||
fn to_string(lit: &Lit) -> Result<String> {
|
||||
match lit {
|
||||
Lit::Str(str) => Ok(str.value()),
|
||||
_ => Err(Error::new(lit.span(), "Expected string literal"))
|
||||
}
|
||||
}
|
||||
|
||||
fn to_bool(lit: &Lit) -> Result<bool> {
|
||||
match lit {
|
||||
Lit::Bool(bool) => Ok(bool.value),
|
||||
_ => Err(Error::new(lit.span(), "Expected bool"))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_attributes(input: &[Attribute]) -> Result<Attrs> {
|
||||
let mut parsed = Attrs::default();
|
||||
for attr in input {
|
||||
if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) {
|
||||
let tokens = remove_parens(attr.tokens.clone());
|
||||
// TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs
|
||||
let nested = parse_macro_input::parse::<AttributeArgs>(tokens.into())?;
|
||||
for meta in nested {
|
||||
match &meta {
|
||||
NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) {
|
||||
Some(key) => match key.as_ref() {
|
||||
"nullable" => parsed.nullable = to_bool(&kv.lit)?,
|
||||
"rename" => parsed.rename = Some(to_string(&kv.lit)?),
|
||||
_ => return Err(Error::new(kv.path.span(), "Unknown key"))
|
||||
},
|
||||
_ => return Err(Error::new(meta.span(), "Unexpected token"))
|
||||
},
|
||||
_ => return Err(Error::new(meta.span(), "Unexpected token"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn expand_variant(variant: &Variant) -> Result<TokenStream> {
|
||||
if !matches!(variant.fields, Fields::Unit) {
|
||||
return Err(Error::new(
|
||||
variant.span(),
|
||||
"#[derive(OpenapiType)] does not support enum variants with fields"
|
||||
));
|
||||
}
|
||||
|
||||
let ident = &variant.ident;
|
||||
|
||||
let attrs = parse_attributes(&variant.attrs)?;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
enumeration.push(#name.to_string());
|
||||
})
|
||||
}
|
||||
|
||||
fn expand_enum(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataEnum) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let where_clause = expand_where(&generics);
|
||||
|
||||
let attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
let variants = input.variants.iter().map(expand_variant).collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
{
|
||||
use #krate::{export::openapi::*, OpenapiSchema};
|
||||
|
||||
let mut enumeration : Vec<String> = Vec::new();
|
||||
|
||||
#(#variants)*
|
||||
|
||||
let schema = SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Empty,
|
||||
enumeration,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
OpenapiSchema {
|
||||
name: Some(#name.to_string()),
|
||||
nullable: #nullable,
|
||||
schema,
|
||||
dependencies: Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn expand_field(field: &Field) -> Result<TokenStream> {
|
||||
let ident = match &field.ident {
|
||||
Some(ident) => ident,
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
field.span(),
|
||||
"#[derive(OpenapiType)] does not support fields without an ident"
|
||||
))
|
||||
},
|
||||
};
|
||||
let ident_str = LitStr::new(&ident.to_string(), ident.span());
|
||||
let ty = &field.ty;
|
||||
|
||||
let attrs = parse_attributes(&field.attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
Ok(quote! {{
|
||||
let mut schema = <#ty>::schema();
|
||||
|
||||
if schema.nullable
|
||||
{
|
||||
schema.nullable = false;
|
||||
}
|
||||
else if !#nullable
|
||||
{
|
||||
required.push(#ident_str.to_string());
|
||||
}
|
||||
|
||||
let keys : Vec<String> = schema.dependencies.keys().map(|k| k.to_string()).collect();
|
||||
for dep in keys
|
||||
{
|
||||
let dep_schema = schema.dependencies.swap_remove(&dep);
|
||||
if let Some(dep_schema) = dep_schema
|
||||
{
|
||||
dependencies.insert(dep, dep_schema);
|
||||
}
|
||||
}
|
||||
|
||||
match schema.name.clone() {
|
||||
Some(schema_name) => {
|
||||
properties.insert(
|
||||
#name.to_string(),
|
||||
ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) }
|
||||
);
|
||||
dependencies.insert(schema_name, schema);
|
||||
},
|
||||
None => {
|
||||
properties.insert(
|
||||
#name.to_string(),
|
||||
ReferenceOr::Item(Box::new(schema.into_schema()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}})
|
||||
}
|
||||
|
||||
fn expand_struct(ident: Ident, generics: Generics, attrs: Vec<Attribute>, input: DataStruct) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let where_clause = expand_where(&generics);
|
||||
|
||||
let attrs = parse_attributes(&attrs)?;
|
||||
let nullable = attrs.nullable;
|
||||
let name = match attrs.rename {
|
||||
Some(rename) => rename,
|
||||
None => ident.to_string()
|
||||
};
|
||||
|
||||
let fields: Vec<TokenStream> = match input.fields {
|
||||
Fields::Named(named_fields) => named_fields.named.iter().map(expand_field).collect_to_result()?,
|
||||
Fields::Unnamed(fields) => {
|
||||
return Err(Error::new(
|
||||
fields.span(),
|
||||
"#[derive(OpenapiType)] does not support unnamed fields"
|
||||
))
|
||||
},
|
||||
Fields::Unit => Vec::new()
|
||||
};
|
||||
|
||||
Ok(quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
#where_clause
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
{
|
||||
use #krate::{export::{openapi::*, IndexMap}, OpenapiSchema};
|
||||
|
||||
let mut properties : IndexMap<String, ReferenceOr<Box<Schema>>> = IndexMap::new();
|
||||
let mut required : Vec<String> = Vec::new();
|
||||
let mut dependencies : IndexMap<String, OpenapiSchema> = IndexMap::new();
|
||||
|
||||
#(#fields)*
|
||||
|
||||
let schema = SchemaKind::Type(Type::Object(ObjectType {
|
||||
properties,
|
||||
required,
|
||||
additional_properties: None,
|
||||
min_properties: None,
|
||||
max_properties: None
|
||||
}));
|
||||
|
||||
OpenapiSchema {
|
||||
name: Some(#name.to_string()),
|
||||
nullable: #nullable,
|
||||
schema,
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
171
derive/src/private_openapi_trait.rs
Normal file
171
derive/src/private_openapi_trait.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
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 {
|
||||
// we compare two incompatible types using their `Display` implementation
|
||||
// this triggers a false positive in clippy
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::cmp_owned))]
|
||||
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 {
|
||||
// we compare two incompatible types using their `Display` implementation
|
||||
// this triggers a false positive in clippy
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::cmp_owned))]
|
||||
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
|
||||
})
|
||||
}
|
|
@ -12,7 +12,7 @@ use syn::{
|
|||
struct MimeList(Punctuated<Path, Token![,]>);
|
||||
|
||||
impl Parse for MimeList {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||
let list = Punctuated::parse_separated_nonempty(&input)?;
|
||||
Ok(Self(list))
|
||||
}
|
||||
|
@ -26,18 +26,25 @@ fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream {
|
|||
#[cfg(feature = "openapi")]
|
||||
fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
|
||||
let krate = super::krate();
|
||||
let openapi = quote!(#krate::private::openapi);
|
||||
|
||||
quote! {
|
||||
impl #generics #krate::OpenapiType for #ident #generics
|
||||
impl #generics #krate::private::OpenapiType for #ident #generics
|
||||
{
|
||||
fn schema() -> #krate::OpenapiSchema
|
||||
fn schema() -> #krate::private::OpenapiSchema
|
||||
{
|
||||
use #krate::{export::openapi::*, OpenapiSchema};
|
||||
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||
..Default::default()
|
||||
})))
|
||||
#krate::private::OpenapiSchema::new(
|
||||
#openapi::SchemaKind::Type(
|
||||
#openapi::Type::String(
|
||||
#openapi::StringType {
|
||||
format: #openapi::VariantOrUnknownOrEmpty::Item(
|
||||
#openapi::StringFormat::Binary
|
||||
),
|
||||
.. ::std::default::Default::default()
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
use crate::{method::Method, util::CollectToResult};
|
||||
use crate::{
|
||||
endpoint::endpoint_ident,
|
||||
util::{CollectToResult, PathEndsWith}
|
||||
};
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use std::{iter, str::FromStr};
|
||||
use std::iter;
|
||||
use syn::{
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
DeriveInput, Error, Result, Token
|
||||
DeriveInput, Result, Token
|
||||
};
|
||||
|
||||
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||
|
||||
impl Parse for MethodList {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||
let content;
|
||||
let _paren = parenthesized!(content in input);
|
||||
let list = Punctuated::parse_separated_nonempty(&content)?;
|
||||
|
@ -23,29 +26,22 @@ impl Parse for MethodList {
|
|||
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||
let krate = super::krate();
|
||||
let ident = input.ident;
|
||||
let name = ident.to_string();
|
||||
|
||||
let methods =
|
||||
input
|
||||
let methods = input
|
||||
.attrs
|
||||
.into_iter()
|
||||
.filter(
|
||||
|attr| {
|
||||
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string())
|
||||
} // TODO wtf
|
||||
)
|
||||
.filter(|attr| attr.path.ends_with("resource"))
|
||||
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||
.flat_map(|list| match list {
|
||||
Ok(iter) => Box::new(iter.map(|method| {
|
||||
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
|
||||
let ident = method.setup_ident(&name);
|
||||
Ok(quote!(#ident(&mut route);))
|
||||
let ident = endpoint_ident(&method);
|
||||
Ok(quote!(route.endpoint::<#ident>();))
|
||||
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||
Err(err) => Box::new(iter::once(Err(err)))
|
||||
})
|
||||
.collect_to_result()?;
|
||||
|
||||
Ok(quote! {
|
||||
let non_openapi_impl = quote! {
|
||||
impl #krate::Resource for #ident
|
||||
{
|
||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||
|
@ -53,5 +49,22 @@ pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
|||
#(#methods)*
|
||||
}
|
||||
}
|
||||
};
|
||||
let openapi_impl = if !cfg!(feature = "openapi") {
|
||||
None
|
||||
} else {
|
||||
Some(quote! {
|
||||
impl #krate::ResourceWithSchema for #ident
|
||||
{
|
||||
fn setup<D : #krate::DrawResourceRoutesWithSchema>(mut route : D)
|
||||
{
|
||||
#(#methods)*
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
Ok(quote! {
|
||||
#non_openapi_impl
|
||||
#openapi_impl
|
||||
})
|
||||
}
|
||||
|
|
|
@ -196,11 +196,11 @@ impl ErrorVariant {
|
|||
quote!(#from_field.into_response_error())
|
||||
},
|
||||
(Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")),
|
||||
(None, Some(status)) => quote!(Ok(#krate::Response {
|
||||
status: { #status }.into(),
|
||||
body: #krate::gotham::hyper::Body::empty(),
|
||||
mime: None
|
||||
})),
|
||||
(None, Some(status)) => quote!(Ok(#krate::Response::new(
|
||||
{ #status }.into(),
|
||||
#krate::gotham::hyper::Body::empty(),
|
||||
None
|
||||
))),
|
||||
(None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant"))
|
||||
};
|
||||
|
||||
|
@ -316,7 +316,7 @@ pub fn expand_resource_error(input: DeriveInput) -> Result<TokenStream> {
|
|||
impl #generics #krate::IntoResponseError for #ident #generics
|
||||
where #( #were ),*
|
||||
{
|
||||
type Err = #krate::export::serde_json::Error;
|
||||
type Err = #krate::private::serde_json::Error;
|
||||
|
||||
fn into_response_error(self) -> Result<#krate::Response, Self::Err>
|
||||
{
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||
use std::iter;
|
||||
use syn::Error;
|
||||
use syn::{Error, Lit, LitBool, LitStr, Path, Result};
|
||||
|
||||
pub trait CollectToResult {
|
||||
pub(crate) trait CollectToResult {
|
||||
type Item;
|
||||
|
||||
fn collect_to_result(self) -> Result<Vec<Self::Item>, Error>;
|
||||
fn collect_to_result(self) -> Result<Vec<Self::Item>>;
|
||||
}
|
||||
|
||||
impl<Item, I> CollectToResult for I
|
||||
where
|
||||
I: Iterator<Item = Result<Item, Error>>
|
||||
I: Iterator<Item = Result<Item>>
|
||||
{
|
||||
type Item = Item;
|
||||
|
||||
fn collect_to_result(self) -> Result<Vec<Item>, Error> {
|
||||
self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| match (code, res) {
|
||||
fn collect_to_result(self) -> Result<Vec<Item>> {
|
||||
self.fold(Ok(Vec::new()), |res, code| match (code, res) {
|
||||
(Ok(code), Ok(mut codes)) => {
|
||||
codes.push(code);
|
||||
Ok(codes)
|
||||
|
@ -30,7 +30,38 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn remove_parens(input: TokenStream) -> TokenStream {
|
||||
pub(crate) trait ExpectLit {
|
||||
fn expect_bool(self) -> Result<LitBool>;
|
||||
fn expect_str(self) -> Result<LitStr>;
|
||||
}
|
||||
|
||||
impl ExpectLit for Lit {
|
||||
fn expect_bool(self) -> Result<LitBool> {
|
||||
match self {
|
||||
Self::Bool(bool) => Ok(bool),
|
||||
_ => Err(Error::new(self.span(), "Expected boolean literal"))
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_str(self) -> Result<LitStr> {
|
||||
match self {
|
||||
Self::Str(str) => Ok(str),
|
||||
_ => Err(Error::new(self.span(), "Expected string literal"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(crate) fn remove_parens(input: TokenStream) -> TokenStream {
|
||||
let iter = input.into_iter().flat_map(|tt| {
|
||||
if let TokenTree::Group(group) = &tt {
|
||||
if group.delimiter() == Delimiter::Parenthesis {
|
||||
|
|
|
@ -2,22 +2,23 @@
|
|||
|
||||
[package]
|
||||
name = "example"
|
||||
version = "0.0.1"
|
||||
version = "0.0.0"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
edition = "2018"
|
||||
license = "Unlicense"
|
||||
readme = "README.md"
|
||||
include = ["src/**/*", "Cargo.toml", "LICENSE"]
|
||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||
publish = false
|
||||
workspace = ".."
|
||||
|
||||
[badges]
|
||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||
|
||||
[dependencies]
|
||||
fake = "2.2.2"
|
||||
gotham = { version = "0.5.0-rc.1", default-features = false }
|
||||
gotham_derive = "0.5.0-rc.1"
|
||||
gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] }
|
||||
gotham = { version = "0.5.0", default-features = false }
|
||||
gotham_derive = "0.5.0"
|
||||
gotham_restful = { version = "0.2.0", features = ["auth", "cors", "openapi"], default-features = false }
|
||||
log = "0.4.8"
|
||||
pretty_env_logger = "0.4"
|
||||
serde = "1.0.110"
|
||||
|
|
|
@ -11,15 +11,15 @@ use gotham::{
|
|||
router::builder::*,
|
||||
state::State
|
||||
};
|
||||
use gotham_restful::*;
|
||||
use gotham_restful::{cors::*, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all, read, search, create, change_all, change, remove, remove_all)]
|
||||
#[resource(read_all, read, search, create, update_all, update, remove, remove_all)]
|
||||
struct Users {}
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(ReadAll)]
|
||||
#[resource(auth_read_all)]
|
||||
struct Auth {}
|
||||
|
||||
#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)]
|
||||
|
@ -27,7 +27,7 @@ struct User {
|
|||
username: String
|
||||
}
|
||||
|
||||
#[read_all(Users)]
|
||||
#[read_all]
|
||||
fn read_all() -> Success<Vec<Option<User>>> {
|
||||
vec![Username().fake(), Username().fake()]
|
||||
.into_iter()
|
||||
|
@ -36,7 +36,7 @@ fn read_all() -> Success<Vec<Option<User>>> {
|
|||
.into()
|
||||
}
|
||||
|
||||
#[read(Users)]
|
||||
#[read]
|
||||
fn read(id: u64) -> Success<User> {
|
||||
let username: String = Username().fake();
|
||||
User {
|
||||
|
@ -45,17 +45,17 @@ fn read(id: u64) -> Success<User> {
|
|||
.into()
|
||||
}
|
||||
|
||||
#[search(Users)]
|
||||
#[search]
|
||||
fn search(query: User) -> Success<User> {
|
||||
query.into()
|
||||
}
|
||||
|
||||
#[create(Users)]
|
||||
#[create]
|
||||
fn create(body: User) {
|
||||
info!("Created User: {}", body.username);
|
||||
}
|
||||
|
||||
#[change_all(Users)]
|
||||
#[change_all]
|
||||
fn update_all(body: Vec<User>) {
|
||||
info!(
|
||||
"Changing all Users to {:?}",
|
||||
|
@ -63,22 +63,22 @@ fn update_all(body: Vec<User>) {
|
|||
);
|
||||
}
|
||||
|
||||
#[change(Users)]
|
||||
#[change]
|
||||
fn update(id: u64, body: User) {
|
||||
info!("Change User {} to {}", id, body.username);
|
||||
}
|
||||
|
||||
#[remove_all(Users)]
|
||||
#[remove_all]
|
||||
fn remove_all() {
|
||||
info!("Delete all Users");
|
||||
}
|
||||
|
||||
#[remove(Users)]
|
||||
#[remove]
|
||||
fn remove(id: u64) {
|
||||
info!("Delete User {}", id);
|
||||
}
|
||||
|
||||
#[read_all(Auth)]
|
||||
#[read_all]
|
||||
fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess<String> {
|
||||
match auth {
|
||||
AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)),
|
||||
|
@ -101,7 +101,7 @@ fn main() {
|
|||
|
||||
let cors = CorsConfig {
|
||||
origin: Origin::Copy,
|
||||
headers: vec![CONTENT_TYPE],
|
||||
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||
credentials: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -122,6 +122,7 @@ fn main() {
|
|||
route.resource::<Users>("users");
|
||||
route.resource::<Auth>("auth");
|
||||
route.get_openapi("openapi");
|
||||
route.swagger_ui("");
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
27
openapi_type/Cargo.toml
Normal file
27
openapi_type/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- eval: (cargo-minor-mode 1) -*-
|
||||
|
||||
[package]
|
||||
workspace = ".."
|
||||
name = "openapi_type"
|
||||
version = "0.1.0-dev"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
edition = "2018"
|
||||
description = "OpenAPI type information for Rust structs and enums"
|
||||
keywords = ["openapi", "type"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type"
|
||||
|
||||
[dependencies]
|
||||
indexmap = "1.6"
|
||||
openapi_type_derive = "0.1.0-dev"
|
||||
openapiv3 = "=0.3.2"
|
||||
serde_json = "1.0"
|
||||
|
||||
# optional dependencies / features
|
||||
chrono = { version = "0.4.19", optional = true }
|
||||
uuid = { version = "0.8.2" , optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
paste = "1.0"
|
||||
serde = "1.0"
|
||||
trybuild = "1.0"
|
224
openapi_type/src/impls.rs
Normal file
224
openapi_type/src/impls.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
use crate::{OpenapiSchema, OpenapiType};
|
||||
#[cfg(feature = "chrono")]
|
||||
use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime};
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use openapiv3::{
|
||||
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind,
|
||||
StringFormat, StringType, Type, VariantOrUnknownOrEmpty
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
hash::BuildHasher,
|
||||
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
|
||||
};
|
||||
#[cfg(feature = "uuid")]
|
||||
use uuid::Uuid;
|
||||
|
||||
macro_rules! impl_openapi_type {
|
||||
($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => {
|
||||
$(
|
||||
impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* {
|
||||
fn schema() -> OpenapiSchema {
|
||||
$schema
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
type Unit = ();
|
||||
impl_openapi_type!(Unit => {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
|
||||
additional_properties: Some(AdditionalProperties::Any(false)),
|
||||
..Default::default()
|
||||
})))
|
||||
});
|
||||
|
||||
impl_openapi_type!(Value => {
|
||||
OpenapiSchema {
|
||||
nullable: true,
|
||||
name: None,
|
||||
schema: SchemaKind::Any(Default::default()),
|
||||
dependencies: Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
impl_openapi_type!(bool => OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})));
|
||||
|
||||
#[inline]
|
||||
fn int_schema(minimum: Option<i64>, bits: Option<i64>) -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
minimum,
|
||||
format: bits
|
||||
.map(|bits| VariantOrUnknownOrEmpty::Unknown(format!("int{}", bits)))
|
||||
.unwrap_or(VariantOrUnknownOrEmpty::Empty),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
|
||||
impl_openapi_type!(isize => int_schema(None, None));
|
||||
impl_openapi_type!(i8 => int_schema(None, Some(8)));
|
||||
impl_openapi_type!(i16 => int_schema(None, Some(16)));
|
||||
impl_openapi_type!(i32 => int_schema(None, Some(32)));
|
||||
impl_openapi_type!(i64 => int_schema(None, Some(64)));
|
||||
impl_openapi_type!(i128 => int_schema(None, Some(128)));
|
||||
|
||||
impl_openapi_type!(usize => int_schema(Some(0), None));
|
||||
impl_openapi_type!(u8 => int_schema(Some(0), Some(8)));
|
||||
impl_openapi_type!(u16 => int_schema(Some(0), Some(16)));
|
||||
impl_openapi_type!(u32 => int_schema(Some(0), Some(32)));
|
||||
impl_openapi_type!(u64 => int_schema(Some(0), Some(64)));
|
||||
impl_openapi_type!(u128 => int_schema(Some(0), Some(128)));
|
||||
|
||||
impl_openapi_type!(NonZeroUsize => int_schema(Some(1), None));
|
||||
impl_openapi_type!(NonZeroU8 => int_schema(Some(1), Some(8)));
|
||||
impl_openapi_type!(NonZeroU16 => int_schema(Some(1), Some(16)));
|
||||
impl_openapi_type!(NonZeroU32 => int_schema(Some(1), Some(32)));
|
||||
impl_openapi_type!(NonZeroU64 => int_schema(Some(1), Some(64)));
|
||||
impl_openapi_type!(NonZeroU128 => int_schema(Some(1), Some(128)));
|
||||
|
||||
#[inline]
|
||||
fn float_schema(format: NumberFormat) -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType {
|
||||
format: VariantOrUnknownOrEmpty::Item(format),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
|
||||
impl_openapi_type!(f32 => float_schema(NumberFormat::Float));
|
||||
impl_openapi_type!(f64 => float_schema(NumberFormat::Double));
|
||||
|
||||
#[inline]
|
||||
fn str_schema(format: VariantOrUnknownOrEmpty<StringFormat>) -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format,
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
|
||||
impl_openapi_type!(String, str => str_schema(VariantOrUnknownOrEmpty::Empty));
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
impl_openapi_type!(Date<T: TimeZone>, NaiveDate => {
|
||||
str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date))
|
||||
});
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
impl_openapi_type!(DateTime<T: TimeZone>, NaiveDateTime => {
|
||||
str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime))
|
||||
});
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
impl_openapi_type!(Uuid => {
|
||||
str_schema(VariantOrUnknownOrEmpty::Unknown("uuid".to_owned()))
|
||||
});
|
||||
|
||||
impl_openapi_type!(Option<T: OpenapiType> => {
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
let schema = match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = ReferenceOr::Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, schema);
|
||||
SchemaKind::AllOf { all_of: vec![reference] }
|
||||
},
|
||||
None => schema.schema
|
||||
};
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: true,
|
||||
name: None,
|
||||
schema,
|
||||
dependencies
|
||||
}
|
||||
});
|
||||
|
||||
#[inline]
|
||||
fn array_schema<T: OpenapiType>(unique_items: bool) -> OpenapiSchema {
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
|
||||
let items = match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = ReferenceOr::Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, schema);
|
||||
reference
|
||||
},
|
||||
None => ReferenceOr::Item(Box::new(schema.into_schema()))
|
||||
};
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: false,
|
||||
name: None,
|
||||
schema: SchemaKind::Type(Type::Array(ArrayType {
|
||||
items,
|
||||
min_items: None,
|
||||
max_items: None,
|
||||
unique_items
|
||||
})),
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
|
||||
impl_openapi_type!(Vec<T: OpenapiType> => array_schema::<T>(false));
|
||||
impl_openapi_type!(BTreeSet<T: OpenapiType>, IndexSet<T: OpenapiType>, HashSet<T: OpenapiType, S: BuildHasher> => {
|
||||
array_schema::<T>(true)
|
||||
});
|
||||
|
||||
#[inline]
|
||||
fn map_schema<K: OpenapiType, T: OpenapiType>() -> OpenapiSchema {
|
||||
let key_schema = K::schema();
|
||||
let mut dependencies = key_schema.dependencies.clone();
|
||||
|
||||
let keys = match key_schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = ReferenceOr::Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, key_schema);
|
||||
reference
|
||||
},
|
||||
None => ReferenceOr::Item(Box::new(key_schema.into_schema()))
|
||||
};
|
||||
|
||||
let schema = T::schema();
|
||||
dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
|
||||
let items = Box::new(match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = ReferenceOr::Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, schema);
|
||||
reference
|
||||
},
|
||||
None => ReferenceOr::Item(schema.into_schema())
|
||||
});
|
||||
|
||||
let mut properties = IndexMap::new();
|
||||
properties.insert("default".to_owned(), keys);
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: false,
|
||||
name: None,
|
||||
schema: SchemaKind::Type(Type::Object(ObjectType {
|
||||
properties,
|
||||
required: vec!["default".to_owned()],
|
||||
additional_properties: Some(AdditionalProperties::Schema(items)),
|
||||
..Default::default()
|
||||
})),
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
|
||||
impl_openapi_type!(
|
||||
BTreeMap<K: OpenapiType, T: OpenapiType>,
|
||||
IndexMap<K: OpenapiType, T: OpenapiType>,
|
||||
HashMap<K: OpenapiType, T: OpenapiType, S: BuildHasher>
|
||||
=> map_schema::<K, T>()
|
||||
);
|
86
openapi_type/src/lib.rs
Normal file
86
openapi_type/src/lib.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))]
|
||||
/*!
|
||||
TODO
|
||||
*/
|
||||
|
||||
pub use indexmap;
|
||||
pub use openapi_type_derive::OpenapiType;
|
||||
pub use openapiv3 as openapi;
|
||||
|
||||
mod impls;
|
||||
#[doc(hidden)]
|
||||
pub mod private;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use openapi::{Schema, SchemaData, SchemaKind};
|
||||
|
||||
// TODO update the documentation
|
||||
/**
|
||||
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
|
||||
already implemented for primitive types, String, Vec, Option and the like. To have it available
|
||||
for your type, simply derive from [OpenapiType].
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct OpenapiSchema {
|
||||
/// The name of this schema. If it is None, the schema will be inlined.
|
||||
pub name: Option<String>,
|
||||
/// Whether this particular schema is nullable. Note that there is no guarantee that this will
|
||||
/// make it into the final specification, it might just be interpreted as a hint to make it
|
||||
/// an optional parameter.
|
||||
pub nullable: bool,
|
||||
/// The actual OpenAPI schema.
|
||||
pub schema: SchemaKind,
|
||||
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
|
||||
/// along with this schema.
|
||||
pub dependencies: IndexMap<String, OpenapiSchema>
|
||||
}
|
||||
|
||||
impl OpenapiSchema {
|
||||
/// Create a new schema that has no name.
|
||||
pub fn new(schema: SchemaKind) -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
nullable: false,
|
||||
schema,
|
||||
dependencies: IndexMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this schema to a [Schema] that can be serialized to the OpenAPI Spec.
|
||||
pub fn into_schema(self) -> Schema {
|
||||
Schema {
|
||||
schema_data: SchemaData {
|
||||
nullable: self.nullable,
|
||||
title: self.name,
|
||||
..Default::default()
|
||||
},
|
||||
schema_kind: self.schema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives
|
||||
access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the
|
||||
like. For use on your own types, there is a derive macro:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate openapi_type_derive;
|
||||
#
|
||||
#[derive(OpenapiType)]
|
||||
struct MyResponse {
|
||||
message: String
|
||||
}
|
||||
```
|
||||
*/
|
||||
pub trait OpenapiType {
|
||||
fn schema() -> OpenapiSchema;
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized + OpenapiType> OpenapiType for &'a T {
|
||||
fn schema() -> OpenapiSchema {
|
||||
T::schema()
|
||||
}
|
||||
}
|
12
openapi_type/src/private.rs
Normal file
12
openapi_type/src/private.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use crate::OpenapiSchema;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
pub type Dependencies = IndexMap<String, OpenapiSchema>;
|
||||
|
||||
pub fn add_dependencies(dependencies: &mut Dependencies, other: &mut Dependencies) {
|
||||
while let Some((dep_name, dep_schema)) = other.pop() {
|
||||
if !dependencies.contains_key(&dep_name) {
|
||||
dependencies.insert(dep_name, dep_schema);
|
||||
}
|
||||
}
|
||||
}
|
249
openapi_type/tests/custom_types.rs
Normal file
249
openapi_type/tests/custom_types.rs
Normal file
|
@ -0,0 +1,249 @@
|
|||
#![allow(dead_code)]
|
||||
use openapi_type::OpenapiType;
|
||||
|
||||
macro_rules! test_type {
|
||||
($ty:ty = $json:tt) => {
|
||||
paste::paste! {
|
||||
#[test]
|
||||
fn [< $ty:lower >]() {
|
||||
let schema = <$ty as OpenapiType>::schema();
|
||||
let schema = openapi_type::OpenapiSchema::into_schema(schema);
|
||||
let schema_json = serde_json::to_value(&schema).unwrap();
|
||||
let expected = serde_json::json!($json);
|
||||
assert_eq!(schema_json, expected);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct UnitStruct;
|
||||
test_type!(UnitStruct = {
|
||||
"type": "object",
|
||||
"title": "UnitStruct",
|
||||
"additionalProperties": false
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct SimpleStruct {
|
||||
foo: String,
|
||||
bar: isize
|
||||
}
|
||||
test_type!(SimpleStruct = {
|
||||
"type": "object",
|
||||
"title": "SimpleStruct",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string"
|
||||
},
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["foo", "bar"]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
#[openapi(rename = "FooBar")]
|
||||
struct StructRename;
|
||||
test_type!(StructRename = {
|
||||
"type": "object",
|
||||
"title": "FooBar",
|
||||
"additionalProperties": false
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum EnumWithoutFields {
|
||||
Success,
|
||||
Error
|
||||
}
|
||||
test_type!(EnumWithoutFields = {
|
||||
"type": "string",
|
||||
"title": "EnumWithoutFields",
|
||||
"enum": [
|
||||
"Success",
|
||||
"Error"
|
||||
]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum EnumWithOneField {
|
||||
Success { value: isize }
|
||||
}
|
||||
test_type!(EnumWithOneField = {
|
||||
"type": "object",
|
||||
"title": "EnumWithOneField",
|
||||
"properties": {
|
||||
"Success": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
},
|
||||
"required": ["Success"]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum EnumWithFields {
|
||||
Success { value: isize },
|
||||
Error { msg: String }
|
||||
}
|
||||
test_type!(EnumWithFields = {
|
||||
"title": "EnumWithFields",
|
||||
"oneOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Success": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
},
|
||||
"required": ["Success"]
|
||||
}, {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["msg"]
|
||||
}
|
||||
},
|
||||
"required": ["Error"]
|
||||
}]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum EnumExternallyTagged {
|
||||
Success { value: isize },
|
||||
Empty,
|
||||
Error
|
||||
}
|
||||
test_type!(EnumExternallyTagged = {
|
||||
"title": "EnumExternallyTagged",
|
||||
"oneOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Success": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
},
|
||||
"required": ["Success"]
|
||||
}, {
|
||||
"type": "string",
|
||||
"enum": ["Empty", "Error"]
|
||||
}]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
#[openapi(tag = "ty")]
|
||||
enum EnumInternallyTagged {
|
||||
Success { value: isize },
|
||||
Empty,
|
||||
Error
|
||||
}
|
||||
test_type!(EnumInternallyTagged = {
|
||||
"title": "EnumInternallyTagged",
|
||||
"oneOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ty": {
|
||||
"type": "string",
|
||||
"enum": ["Success"]
|
||||
}
|
||||
},
|
||||
"required": ["value", "ty"]
|
||||
}, {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ty": {
|
||||
"type": "string",
|
||||
"enum": ["Empty", "Error"]
|
||||
}
|
||||
},
|
||||
"required": ["ty"]
|
||||
}]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
#[openapi(tag = "ty", content = "ct")]
|
||||
enum EnumAdjacentlyTagged {
|
||||
Success { value: isize },
|
||||
Empty,
|
||||
Error
|
||||
}
|
||||
test_type!(EnumAdjacentlyTagged = {
|
||||
"title": "EnumAdjacentlyTagged",
|
||||
"oneOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ty": {
|
||||
"type": "string",
|
||||
"enum": ["Success"]
|
||||
},
|
||||
"ct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}
|
||||
},
|
||||
"required": ["ty", "ct"]
|
||||
}, {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ty": {
|
||||
"type": "string",
|
||||
"enum": ["Empty", "Error"]
|
||||
}
|
||||
},
|
||||
"required": ["ty"]
|
||||
}]
|
||||
});
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
#[openapi(untagged)]
|
||||
enum EnumUntagged {
|
||||
Success { value: isize },
|
||||
Empty,
|
||||
Error
|
||||
}
|
||||
test_type!(EnumUntagged = {
|
||||
"title": "EnumUntagged",
|
||||
"oneOf": [{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["value"]
|
||||
}, {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}]
|
||||
});
|
6
openapi_type/tests/fail/enum_with_no_variants.rs
Normal file
6
openapi_type/tests/fail/enum_with_no_variants.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Foo {}
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/enum_with_no_variants.stderr
Normal file
5
openapi_type/tests/fail/enum_with_no_variants.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] does not support enums with no variants
|
||||
--> $DIR/enum_with_no_variants.rs:4:10
|
||||
|
|
||||
4 | enum Foo {}
|
||||
| ^^
|
12
openapi_type/tests/fail/not_openapitype.rs
Normal file
12
openapi_type/tests/fail/not_openapitype.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo {
|
||||
bar: Bar
|
||||
}
|
||||
|
||||
struct Bar;
|
||||
|
||||
fn main() {
|
||||
Foo::schema();
|
||||
}
|
8
openapi_type/tests/fail/not_openapitype.stderr
Normal file
8
openapi_type/tests/fail/not_openapitype.stderr
Normal file
|
@ -0,0 +1,8 @@
|
|||
error[E0277]: the trait bound `Bar: OpenapiType` is not satisfied
|
||||
--> $DIR/not_openapitype.rs:3:10
|
||||
|
|
||||
3 | #[derive(OpenapiType)]
|
||||
| ^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `Bar`
|
||||
|
|
||||
= note: required by `schema`
|
||||
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
12
openapi_type/tests/fail/not_openapitype_generics.rs
Normal file
12
openapi_type/tests/fail/not_openapitype_generics.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo<T> {
|
||||
bar: T
|
||||
}
|
||||
|
||||
struct Bar;
|
||||
|
||||
fn main() {
|
||||
<Foo<Bar>>::schema();
|
||||
}
|
23
openapi_type/tests/fail/not_openapitype_generics.stderr
Normal file
23
openapi_type/tests/fail/not_openapitype_generics.stderr
Normal file
|
@ -0,0 +1,23 @@
|
|||
error[E0599]: no function or associated item named `schema` found for struct `Foo<Bar>` in the current scope
|
||||
--> $DIR/not_openapitype_generics.rs:11:14
|
||||
|
|
||||
4 | struct Foo<T> {
|
||||
| -------------
|
||||
| |
|
||||
| function or associated item `schema` not found for this
|
||||
| doesn't satisfy `Foo<Bar>: OpenapiType`
|
||||
...
|
||||
8 | struct Bar;
|
||||
| ----------- doesn't satisfy `Bar: OpenapiType`
|
||||
...
|
||||
11 | <Foo<Bar>>::schema();
|
||||
| ^^^^^^ function or associated item not found in `Foo<Bar>`
|
||||
|
|
||||
= note: the method `schema` exists but the following trait bounds were not satisfied:
|
||||
`Bar: OpenapiType`
|
||||
which is required by `Foo<Bar>: OpenapiType`
|
||||
`Foo<Bar>: OpenapiType`
|
||||
which is required by `&Foo<Bar>: OpenapiType`
|
||||
= help: items from traits can only be used if the trait is implemented and in scope
|
||||
= note: the following trait defines an item `schema`, perhaps you need to implement it:
|
||||
candidate #1: `OpenapiType`
|
21
openapi_type/tests/fail/rustfmt.sh
Executable file
21
openapi_type/tests/fail/rustfmt.sh
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/busybox ash
|
||||
set -euo pipefail
|
||||
|
||||
rustfmt=${RUSTFMT:-rustfmt}
|
||||
version="$($rustfmt -V)"
|
||||
case "$version" in
|
||||
*nightly*)
|
||||
# all good, no additional flags required
|
||||
;;
|
||||
*)
|
||||
# assume we're using some sort of rustup setup
|
||||
rustfmt="$rustfmt +nightly"
|
||||
;;
|
||||
esac
|
||||
|
||||
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
|
6
openapi_type/tests/fail/tuple_struct.rs
Normal file
6
openapi_type/tests/fail/tuple_struct.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Foo(i64, i64);
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/tuple_struct.stderr
Normal file
5
openapi_type/tests/fail/tuple_struct.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] does not support tuple structs
|
||||
--> $DIR/tuple_struct.rs:4:11
|
||||
|
|
||||
4 | struct Foo(i64, i64);
|
||||
| ^^^^^^^^^^
|
8
openapi_type/tests/fail/tuple_variant.rs
Normal file
8
openapi_type/tests/fail/tuple_variant.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
enum Foo {
|
||||
Pair(i64, i64)
|
||||
}
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/tuple_variant.stderr
Normal file
5
openapi_type/tests/fail/tuple_variant.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] does not support tuple variants
|
||||
--> $DIR/tuple_variant.rs:5:6
|
||||
|
|
||||
5 | Pair(i64, i64)
|
||||
| ^^^^^^^^^^
|
9
openapi_type/tests/fail/union.rs
Normal file
9
openapi_type/tests/fail/union.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
union Foo {
|
||||
signed: i64,
|
||||
unsigned: u64
|
||||
}
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/union.stderr
Normal file
5
openapi_type/tests/fail/union.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: #[derive(OpenapiType)] cannot be used on unions
|
||||
--> $DIR/union.rs:4:1
|
||||
|
|
||||
4 | union Foo {
|
||||
| ^^^^^
|
7
openapi_type/tests/fail/unknown_attribute.rs
Normal file
7
openapi_type/tests/fail/unknown_attribute.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
#[openapi(pizza)]
|
||||
struct Foo;
|
||||
|
||||
fn main() {}
|
5
openapi_type/tests/fail/unknown_attribute.stderr
Normal file
5
openapi_type/tests/fail/unknown_attribute.stderr
Normal file
|
@ -0,0 +1,5 @@
|
|||
error: Unexpected token
|
||||
--> $DIR/unknown_attribute.rs:4:11
|
||||
|
|
||||
4 | #[openapi(pizza)]
|
||||
| ^^^^^
|
216
openapi_type/tests/std_types.rs
Normal file
216
openapi_type/tests/std_types.rs
Normal file
|
@ -0,0 +1,216 @@
|
|||
#[cfg(feature = "chrono")]
|
||||
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use openapi_type::OpenapiType;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
|
||||
};
|
||||
#[cfg(feature = "uuid")]
|
||||
use uuid::Uuid;
|
||||
|
||||
macro_rules! test_type {
|
||||
($($ty:ident $(<$($generic:ident),+>)*),* = $json:tt) => {
|
||||
paste::paste! { $(
|
||||
#[test]
|
||||
fn [< $ty:lower $($(_ $generic:lower)+)* >]() {
|
||||
let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema();
|
||||
let schema = openapi_type::OpenapiSchema::into_schema(schema);
|
||||
let schema_json = serde_json::to_value(&schema).unwrap();
|
||||
let expected = serde_json::json!($json);
|
||||
assert_eq!(schema_json, expected);
|
||||
}
|
||||
)* }
|
||||
};
|
||||
}
|
||||
|
||||
type Unit = ();
|
||||
test_type!(Unit = {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
});
|
||||
|
||||
test_type!(Value = {
|
||||
"nullable": true
|
||||
});
|
||||
|
||||
test_type!(bool = {
|
||||
"type": "boolean"
|
||||
});
|
||||
|
||||
// ### integer types
|
||||
|
||||
test_type!(isize = {
|
||||
"type": "integer"
|
||||
});
|
||||
|
||||
test_type!(usize = {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
});
|
||||
|
||||
test_type!(i8 = {
|
||||
"type": "integer",
|
||||
"format": "int8"
|
||||
});
|
||||
|
||||
test_type!(u8 = {
|
||||
"type": "integer",
|
||||
"format": "int8",
|
||||
"minimum": 0
|
||||
});
|
||||
|
||||
test_type!(i16 = {
|
||||
"type": "integer",
|
||||
"format": "int16"
|
||||
});
|
||||
|
||||
test_type!(u16 = {
|
||||
"type": "integer",
|
||||
"format": "int16",
|
||||
"minimum": 0
|
||||
});
|
||||
|
||||
test_type!(i32 = {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
});
|
||||
|
||||
test_type!(u32 = {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"minimum": 0
|
||||
});
|
||||
|
||||
test_type!(i64 = {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
});
|
||||
|
||||
test_type!(u64 = {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 0
|
||||
});
|
||||
|
||||
test_type!(i128 = {
|
||||
"type": "integer",
|
||||
"format": "int128"
|
||||
});
|
||||
|
||||
test_type!(u128 = {
|
||||
"type": "integer",
|
||||
"format": "int128",
|
||||
"minimum": 0
|
||||
});
|
||||
|
||||
// ### non-zero integer types
|
||||
|
||||
test_type!(NonZeroUsize = {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
});
|
||||
|
||||
test_type!(NonZeroU8 = {
|
||||
"type": "integer",
|
||||
"format": "int8",
|
||||
"minimum": 1
|
||||
});
|
||||
|
||||
test_type!(NonZeroU16 = {
|
||||
"type": "integer",
|
||||
"format": "int16",
|
||||
"minimum": 1
|
||||
});
|
||||
|
||||
test_type!(NonZeroU32 = {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"minimum": 1
|
||||
});
|
||||
|
||||
test_type!(NonZeroU64 = {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 1
|
||||
});
|
||||
|
||||
test_type!(NonZeroU128 = {
|
||||
"type": "integer",
|
||||
"format": "int128",
|
||||
"minimum": 1
|
||||
});
|
||||
|
||||
// ### floats
|
||||
|
||||
test_type!(f32 = {
|
||||
"type": "number",
|
||||
"format": "float"
|
||||
});
|
||||
|
||||
test_type!(f64 = {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
});
|
||||
|
||||
// ### string
|
||||
|
||||
test_type!(String = {
|
||||
"type": "string"
|
||||
});
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
test_type!(Uuid = {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
});
|
||||
|
||||
// ### date/time
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
test_type!(Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate = {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
});
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
test_type!(DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime = {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
});
|
||||
|
||||
// ### some std types
|
||||
|
||||
test_type!(Option<String> = {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
});
|
||||
|
||||
test_type!(Vec<String> = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
});
|
||||
|
||||
test_type!(BTreeSet<String>, IndexSet<String>, HashSet<String> = {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": true
|
||||
});
|
||||
|
||||
test_type!(BTreeMap<isize, String>, IndexMap<isize, String>, HashMap<isize, String> = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": ["default"],
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
});
|
7
openapi_type/tests/trybuild.rs
Normal file
7
openapi_type/tests/trybuild.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use trybuild::TestCases;
|
||||
|
||||
#[test]
|
||||
fn trybuild() {
|
||||
let t = TestCases::new();
|
||||
t.compile_fail("tests/fail/*.rs");
|
||||
}
|
19
openapi_type_derive/Cargo.toml
Normal file
19
openapi_type_derive/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
# -*- eval: (cargo-minor-mode 1) -*-
|
||||
|
||||
[package]
|
||||
workspace = ".."
|
||||
name = "openapi_type_derive"
|
||||
version = "0.1.0-dev"
|
||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||
edition = "2018"
|
||||
description = "Implementation detail of the openapi_type crate"
|
||||
license = "Apache-2.0"
|
||||
repository = "https://gitlab.com/msrd0/gotham-restful/-/tree/master/openapi_type_derive"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = "1.0"
|
143
openapi_type_derive/src/codegen.rs
Normal file
143
openapi_type_derive/src/codegen.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use crate::parser::{ParseData, ParseDataType};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::LitStr;
|
||||
|
||||
impl ParseData {
|
||||
pub(super) fn gen_schema(&self) -> TokenStream {
|
||||
match self {
|
||||
Self::Struct(fields) => gen_struct(fields),
|
||||
Self::Enum(variants) => gen_enum(variants),
|
||||
Self::Alternatives(alt) => gen_alt(alt),
|
||||
Self::Unit => gen_unit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_struct(fields: &[(LitStr, ParseDataType)]) -> TokenStream {
|
||||
let field_name = fields.iter().map(|(name, _)| name);
|
||||
let field_schema = fields.iter().map(|(_, ty)| match ty {
|
||||
ParseDataType::Type(ty) => {
|
||||
quote!(<#ty as ::openapi_type::OpenapiType>::schema())
|
||||
},
|
||||
ParseDataType::Inline(data) => {
|
||||
let code = data.gen_schema();
|
||||
quote!(::openapi_type::OpenapiSchema::new(#code))
|
||||
}
|
||||
});
|
||||
|
||||
let openapi = path!(::openapi_type::openapi);
|
||||
quote! {
|
||||
{
|
||||
let mut properties = <::openapi_type::indexmap::IndexMap<
|
||||
::std::string::String,
|
||||
#openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>>
|
||||
>>::new();
|
||||
let mut required = <::std::vec::Vec<::std::string::String>>::new();
|
||||
|
||||
#({
|
||||
const FIELD_NAME: &::core::primitive::str = #field_name;
|
||||
let mut field_schema = #field_schema;
|
||||
::openapi_type::private::add_dependencies(
|
||||
&mut dependencies,
|
||||
&mut field_schema.dependencies
|
||||
);
|
||||
|
||||
// fields in OpenAPI are nullable by default
|
||||
match field_schema.nullable {
|
||||
true => field_schema.nullable = false,
|
||||
false => required.push(::std::string::String::from(FIELD_NAME))
|
||||
};
|
||||
|
||||
match field_schema.name.as_ref() {
|
||||
// include the field schema as reference
|
||||
::std::option::Option::Some(schema_name) => {
|
||||
let mut reference = ::std::string::String::from("#/components/schemas/");
|
||||
reference.push_str(schema_name);
|
||||
properties.insert(
|
||||
::std::string::String::from(FIELD_NAME),
|
||||
#openapi::ReferenceOr::Reference { reference }
|
||||
);
|
||||
dependencies.insert(
|
||||
::std::string::String::from(schema_name),
|
||||
field_schema
|
||||
);
|
||||
},
|
||||
// inline the field schema
|
||||
::std::option::Option::None => {
|
||||
properties.insert(
|
||||
::std::string::String::from(FIELD_NAME),
|
||||
#openapi::ReferenceOr::Item(
|
||||
::std::boxed::Box::new(
|
||||
field_schema.into_schema()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
})*
|
||||
|
||||
#openapi::SchemaKind::Type(
|
||||
#openapi::Type::Object(
|
||||
#openapi::ObjectType {
|
||||
properties,
|
||||
required,
|
||||
.. ::std::default::Default::default()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_enum(variants: &[LitStr]) -> TokenStream {
|
||||
let openapi = path!(::openapi_type::openapi);
|
||||
quote! {
|
||||
{
|
||||
let mut enumeration = <::std::vec::Vec<::std::string::String>>::new();
|
||||
#(enumeration.push(::std::string::String::from(#variants));)*
|
||||
#openapi::SchemaKind::Type(
|
||||
#openapi::Type::String(
|
||||
#openapi::StringType {
|
||||
enumeration,
|
||||
.. ::std::default::Default::default()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_alt(alt: &[ParseData]) -> TokenStream {
|
||||
let openapi = path!(::openapi_type::openapi);
|
||||
let schema = alt.iter().map(|data| data.gen_schema());
|
||||
quote! {
|
||||
{
|
||||
let mut alternatives = <::std::vec::Vec<
|
||||
#openapi::ReferenceOr<#openapi::Schema>
|
||||
>>::new();
|
||||
#(alternatives.push(#openapi::ReferenceOr::Item(
|
||||
::openapi_type::OpenapiSchema::new(#schema).into_schema()
|
||||
));)*
|
||||
#openapi::SchemaKind::OneOf {
|
||||
one_of: alternatives
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_unit() -> TokenStream {
|
||||
let openapi = path!(::openapi_type::openapi);
|
||||
quote! {
|
||||
#openapi::SchemaKind::Type(
|
||||
#openapi::Type::Object(
|
||||
#openapi::ObjectType {
|
||||
additional_properties: ::std::option::Option::Some(
|
||||
#openapi::AdditionalProperties::Any(false)
|
||||
),
|
||||
.. ::std::default::Default::default()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
95
openapi_type_derive/src/lib.rs
Normal file
95
openapi_type_derive/src/lib.rs
Normal file
|
@ -0,0 +1,95 @@
|
|||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||
#![deny(broken_intra_doc_links)]
|
||||
#![forbid(unsafe_code)]
|
||||
//! This crate defines the macros for `#[derive(OpenapiType)]`.
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, LitStr, TraitBound, TraitBoundModifier, TypeParamBound};
|
||||
|
||||
#[macro_use]
|
||||
mod util;
|
||||
//use util::*;
|
||||
|
||||
mod codegen;
|
||||
mod parser;
|
||||
use parser::*;
|
||||
|
||||
/// The derive macro for [OpenapiType](https://docs.rs/openapi_type/*/openapi_type/trait.OpenapiType.html).
|
||||
#[proc_macro_derive(OpenapiType, attributes(openapi))]
|
||||
pub fn derive_openapi_type(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input);
|
||||
expand_openapi_type(input).unwrap_or_else(|err| err.to_compile_error()).into()
|
||||
}
|
||||
|
||||
fn expand_openapi_type(mut input: DeriveInput) -> syn::Result<TokenStream2> {
|
||||
// parse #[serde] and #[openapi] attributes
|
||||
let mut attrs = ContainerAttributes::default();
|
||||
for attr in &input.attrs {
|
||||
if attr.path.is_ident("serde") {
|
||||
parse_container_attrs(attr, &mut attrs, false)?;
|
||||
}
|
||||
}
|
||||
for attr in &input.attrs {
|
||||
if attr.path.is_ident("openapi") {
|
||||
parse_container_attrs(attr, &mut attrs, true)?;
|
||||
}
|
||||
}
|
||||
|
||||
// prepare impl block for codegen
|
||||
let ident = &input.ident;
|
||||
let name = ident.to_string();
|
||||
let mut name = LitStr::new(&name, ident.span());
|
||||
if let Some(rename) = &attrs.rename {
|
||||
name = rename.clone();
|
||||
}
|
||||
|
||||
// prepare the generics - all impl generics will get `OpenapiType` requirement
|
||||
let (impl_generics, ty_generics, where_clause) = {
|
||||
let generics = &mut input.generics;
|
||||
generics.type_params_mut().for_each(|param| {
|
||||
param.colon_token.get_or_insert_with(Default::default);
|
||||
param.bounds.push(TypeParamBound::Trait(TraitBound {
|
||||
paren_token: None,
|
||||
modifier: TraitBoundModifier::None,
|
||||
lifetimes: None,
|
||||
path: path!(::openapi_type::OpenapiType)
|
||||
}));
|
||||
});
|
||||
generics.split_for_impl()
|
||||
};
|
||||
|
||||
// parse the input data
|
||||
let parsed = match &input.data {
|
||||
Data::Struct(strukt) => parse_struct(strukt)?,
|
||||
Data::Enum(inum) => parse_enum(inum, &attrs)?,
|
||||
Data::Union(union) => parse_union(union)?
|
||||
};
|
||||
|
||||
// run the codegen
|
||||
let schema_code = parsed.gen_schema();
|
||||
|
||||
// put the code together
|
||||
Ok(quote! {
|
||||
#[allow(unused_mut)]
|
||||
impl #impl_generics ::openapi_type::OpenapiType for #ident #ty_generics #where_clause {
|
||||
fn schema() -> ::openapi_type::OpenapiSchema {
|
||||
// prepare the dependencies
|
||||
let mut dependencies = ::openapi_type::private::Dependencies::new();
|
||||
|
||||
// create the schema
|
||||
let schema = #schema_code;
|
||||
|
||||
// return everything
|
||||
const NAME: &::core::primitive::str = #name;
|
||||
::openapi_type::OpenapiSchema {
|
||||
name: ::std::option::Option::Some(::std::string::String::from(NAME)),
|
||||
nullable: false,
|
||||
schema,
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
198
openapi_type_derive/src/parser.rs
Normal file
198
openapi_type_derive/src/parser.rs
Normal file
|
@ -0,0 +1,198 @@
|
|||
use crate::util::{ExpectLit, ToLitStr};
|
||||
use proc_macro2::Span;
|
||||
use syn::{
|
||||
punctuated::Punctuated, spanned::Spanned as _, Attribute, DataEnum, DataStruct, DataUnion, Fields, FieldsNamed, LitStr,
|
||||
Meta, Token, Type
|
||||
};
|
||||
|
||||
pub(super) enum ParseDataType {
|
||||
Type(Type),
|
||||
Inline(ParseData)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(super) enum ParseData {
|
||||
Struct(Vec<(LitStr, ParseDataType)>),
|
||||
Enum(Vec<LitStr>),
|
||||
Alternatives(Vec<ParseData>),
|
||||
Unit
|
||||
}
|
||||
|
||||
fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result<ParseData> {
|
||||
let mut fields: Vec<(LitStr, ParseDataType)> = Vec::new();
|
||||
for f in &named_fields.named {
|
||||
let ident = f
|
||||
.ident
|
||||
.as_ref()
|
||||
.ok_or_else(|| syn::Error::new(f.span(), "#[derive(OpenapiType)] does not support fields without an ident"))?;
|
||||
let name = ident.to_lit_str();
|
||||
let ty = f.ty.to_owned();
|
||||
fields.push((name, ParseDataType::Type(ty)));
|
||||
}
|
||||
Ok(ParseData::Struct(fields))
|
||||
}
|
||||
|
||||
pub(super) fn parse_struct(strukt: &DataStruct) -> syn::Result<ParseData> {
|
||||
match &strukt.fields {
|
||||
Fields::Named(named_fields) => parse_named_fields(named_fields),
|
||||
Fields::Unnamed(unnamed_fields) => {
|
||||
return Err(syn::Error::new(
|
||||
unnamed_fields.span(),
|
||||
"#[derive(OpenapiType)] does not support tuple structs"
|
||||
))
|
||||
},
|
||||
Fields::Unit => Ok(ParseData::Unit)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_enum(inum: &DataEnum, attrs: &ContainerAttributes) -> syn::Result<ParseData> {
|
||||
let mut strings: Vec<LitStr> = Vec::new();
|
||||
let mut types: Vec<(LitStr, ParseData)> = Vec::new();
|
||||
|
||||
for v in &inum.variants {
|
||||
let name = v.ident.to_lit_str();
|
||||
match &v.fields {
|
||||
Fields::Named(named_fields) => {
|
||||
types.push((name, parse_named_fields(named_fields)?));
|
||||
},
|
||||
Fields::Unnamed(unnamed_fields) => {
|
||||
return Err(syn::Error::new(
|
||||
unnamed_fields.span(),
|
||||
"#[derive(OpenapiType)] does not support tuple variants"
|
||||
))
|
||||
},
|
||||
Fields::Unit => strings.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
let data_strings = if strings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match (&attrs.tag, &attrs.content, attrs.untagged) {
|
||||
// externally tagged (default)
|
||||
(None, None, false) => Some(ParseData::Enum(strings)),
|
||||
// internally tagged or adjacently tagged
|
||||
(Some(tag), _, false) => Some(ParseData::Struct(vec![(
|
||||
tag.clone(),
|
||||
ParseDataType::Inline(ParseData::Enum(strings))
|
||||
)])),
|
||||
// untagged
|
||||
(None, None, true) => Some(ParseData::Unit),
|
||||
// unknown
|
||||
_ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation"))
|
||||
}
|
||||
};
|
||||
|
||||
let data_types =
|
||||
if types.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ParseData::Alternatives(
|
||||
types
|
||||
.into_iter()
|
||||
.map(|(name, mut data)| {
|
||||
Ok(match (&attrs.tag, &attrs.content, attrs.untagged) {
|
||||
// externally tagged (default)
|
||||
(None, None, false) => ParseData::Struct(vec![(name, ParseDataType::Inline(data))]),
|
||||
// internally tagged
|
||||
(Some(tag), None, false) => {
|
||||
match &mut data {
|
||||
ParseData::Struct(fields) => {
|
||||
fields.push((tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name]))))
|
||||
},
|
||||
_ => return Err(syn::Error::new(
|
||||
tag.span(),
|
||||
"#[derive(OpenapiType)] does not support tuple variants on internally tagged enums"
|
||||
))
|
||||
};
|
||||
data
|
||||
},
|
||||
// adjacently tagged
|
||||
(Some(tag), Some(content), false) => ParseData::Struct(vec![
|
||||
(tag.clone(), ParseDataType::Inline(ParseData::Enum(vec![name]))),
|
||||
(content.clone(), ParseDataType::Inline(data)),
|
||||
]),
|
||||
// untagged
|
||||
(None, None, true) => data,
|
||||
// unknown
|
||||
_ => return Err(syn::Error::new(Span::call_site(), "Unknown enum representation"))
|
||||
})
|
||||
})
|
||||
.collect::<syn::Result<Vec<_>>>()?
|
||||
))
|
||||
};
|
||||
|
||||
match (data_strings, data_types) {
|
||||
// only variants without fields
|
||||
(Some(data), None) => Ok(data),
|
||||
// only one variant with fields
|
||||
(None, Some(ParseData::Alternatives(mut alt))) if alt.len() == 1 => Ok(alt.remove(0)),
|
||||
// only variants with fields
|
||||
(None, Some(data)) => Ok(data),
|
||||
// variants with and without fields
|
||||
(Some(data), Some(ParseData::Alternatives(mut alt))) => {
|
||||
alt.push(data);
|
||||
Ok(ParseData::Alternatives(alt))
|
||||
},
|
||||
// no variants
|
||||
(None, None) => Err(syn::Error::new(
|
||||
inum.brace_token.span,
|
||||
"#[derive(OpenapiType)] does not support enums with no variants"
|
||||
)),
|
||||
// data_types always produces Alternatives
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse_union(union: &DataUnion) -> syn::Result<ParseData> {
|
||||
Err(syn::Error::new(
|
||||
union.union_token.span(),
|
||||
"#[derive(OpenapiType)] cannot be used on unions"
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ContainerAttributes {
|
||||
pub(super) rename: Option<LitStr>,
|
||||
pub(super) rename_all: Option<LitStr>,
|
||||
pub(super) tag: Option<LitStr>,
|
||||
pub(super) content: Option<LitStr>,
|
||||
pub(super) untagged: bool
|
||||
}
|
||||
|
||||
pub(super) fn parse_container_attrs(
|
||||
input: &Attribute,
|
||||
attrs: &mut ContainerAttributes,
|
||||
error_on_unknown: bool
|
||||
) -> syn::Result<()> {
|
||||
let tokens: Punctuated<Meta, Token![,]> = input.parse_args_with(Punctuated::parse_terminated)?;
|
||||
for token in tokens {
|
||||
match token {
|
||||
Meta::NameValue(kv) if kv.path.is_ident("rename") => {
|
||||
attrs.rename = Some(kv.lit.expect_str()?);
|
||||
},
|
||||
|
||||
Meta::NameValue(kv) if kv.path.is_ident("rename_all") => {
|
||||
attrs.rename_all = Some(kv.lit.expect_str()?);
|
||||
},
|
||||
|
||||
Meta::NameValue(kv) if kv.path.is_ident("tag") => {
|
||||
attrs.tag = Some(kv.lit.expect_str()?);
|
||||
},
|
||||
|
||||
Meta::NameValue(kv) if kv.path.is_ident("content") => {
|
||||
attrs.content = Some(kv.lit.expect_str()?);
|
||||
},
|
||||
|
||||
Meta::Path(path) if path.is_ident("untagged") => {
|
||||
attrs.untagged = true;
|
||||
},
|
||||
|
||||
Meta::Path(path) if error_on_unknown => return Err(syn::Error::new(path.span(), "Unexpected token")),
|
||||
Meta::List(list) if error_on_unknown => return Err(syn::Error::new(list.span(), "Unexpected token")),
|
||||
Meta::NameValue(kv) if error_on_unknown => return Err(syn::Error::new(kv.path.span(), "Unexpected token")),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
52
openapi_type_derive/src/util.rs
Normal file
52
openapi_type_derive/src/util.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use proc_macro2::Ident;
|
||||
use syn::{Lit, LitStr};
|
||||
|
||||
/// Convert any literal path into a [syn::Path].
|
||||
macro_rules! path {
|
||||
(:: $($segment:ident)::*) => {
|
||||
path!(@private Some(Default::default()), $($segment),*)
|
||||
};
|
||||
($($segment:ident)::*) => {
|
||||
path!(@private None, $($segment),*)
|
||||
};
|
||||
(@private $leading_colon:expr, $($segment:ident),*) => {
|
||||
{
|
||||
#[allow(unused_mut)]
|
||||
let mut segments: ::syn::punctuated::Punctuated<::syn::PathSegment, _> = Default::default();
|
||||
$(
|
||||
segments.push(::syn::PathSegment {
|
||||
ident: ::proc_macro2::Ident::new(stringify!($segment), ::proc_macro2::Span::call_site()),
|
||||
arguments: Default::default()
|
||||
});
|
||||
)*
|
||||
::syn::Path {
|
||||
leading_colon: $leading_colon,
|
||||
segments
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert any [Ident] into a [LitStr]. Basically `stringify!`.
|
||||
pub(super) trait ToLitStr {
|
||||
fn to_lit_str(&self) -> LitStr;
|
||||
}
|
||||
impl ToLitStr for Ident {
|
||||
fn to_lit_str(&self) -> LitStr {
|
||||
LitStr::new(&self.to_string(), self.span())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [Lit] to one specific literal type.
|
||||
pub(crate) trait ExpectLit {
|
||||
fn expect_str(self) -> syn::Result<LitStr>;
|
||||
}
|
||||
|
||||
impl ExpectLit for Lit {
|
||||
fn expect_str(self) -> syn::Result<LitStr> {
|
||||
match self {
|
||||
Self::Str(str) => Ok(str),
|
||||
_ => Err(syn::Error::new(self.span(), "Expected string literal"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ trailing_comma = "Never"
|
|||
|
||||
# misc
|
||||
format_code_in_doc_comments = true
|
||||
merge_imports = true
|
||||
imports_granularity = "Crate"
|
||||
overflow_delimited_expr = true
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
||||
|
|
10
src/auth.rs
10
src/auth.rs
|
@ -1,4 +1,5 @@
|
|||
use crate::{AuthError, Forbidden, HeaderName};
|
||||
use crate::{AuthError, Forbidden};
|
||||
|
||||
use cookie::CookieJar;
|
||||
use futures_util::{
|
||||
future,
|
||||
|
@ -7,7 +8,7 @@ use futures_util::{
|
|||
use gotham::{
|
||||
anyhow,
|
||||
handler::HandlerFuture,
|
||||
hyper::header::{HeaderMap, AUTHORIZATION},
|
||||
hyper::header::{HeaderMap, HeaderName, AUTHORIZATION},
|
||||
middleware::{cookie::CookieParser, Middleware, NewMiddleware},
|
||||
state::{FromState, State}
|
||||
};
|
||||
|
@ -15,6 +16,7 @@ use jsonwebtoken::{errors::ErrorKind, DecodingKey};
|
|||
use serde::de::DeserializeOwned;
|
||||
use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin};
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use jsonwebtoken::Validation as AuthValidation;
|
||||
|
||||
/// The authentication status returned by the auth middleware for each request.
|
||||
|
@ -77,7 +79,7 @@ This trait will help the auth middleware to determine the validity of an authent
|
|||
A very basic implementation could look like this:
|
||||
|
||||
```
|
||||
# use gotham_restful::{AuthHandler, State};
|
||||
# use gotham_restful::{AuthHandler, gotham::state::State};
|
||||
#
|
||||
const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc";
|
||||
|
||||
|
@ -136,7 +138,7 @@ struct AuthData {
|
|||
exp: u64
|
||||
}
|
||||
|
||||
#[read_all(AuthResource)]
|
||||
#[read_all]
|
||||
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
||||
format!("{:?}", auth).into()
|
||||
}
|
||||
|
|
95
src/cors.rs
95
src/cors.rs
|
@ -5,16 +5,18 @@ use gotham::{
|
|||
header::{
|
||||
HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE,
|
||||
ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY
|
||||
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY
|
||||
},
|
||||
Body, Method, Response, StatusCode
|
||||
},
|
||||
middleware::Middleware,
|
||||
pipeline::chain::PipelineHandleChain,
|
||||
router::{builder::*, route::matcher::AccessControlRequestMethodMatcher},
|
||||
router::{
|
||||
builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher},
|
||||
route::matcher::AccessControlRequestMethodMatcher
|
||||
},
|
||||
state::{FromState, State}
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use std::{panic::RefUnwindSafe, pin::Pin};
|
||||
|
||||
/**
|
||||
|
@ -53,6 +55,52 @@ impl Origin {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the `Vary` header has to include `Origin`.
|
||||
fn varies(&self) -> bool {
|
||||
matches!(self, Self::Copy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Specify the allowed headers of the request. It is up to the browser to check that only the allowed
|
||||
headers are sent with the request.
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Headers {
|
||||
/// Do not send any `Access-Control-Allow-Headers` headers.
|
||||
None,
|
||||
/// Set the `Access-Control-Allow-Headers` header to the following header list. If empty, this
|
||||
/// is treated as if it was [None].
|
||||
List(Vec<HeaderName>),
|
||||
/// Copy the `Access-Control-Request-Headers` header into the `Access-Control-Allow-Header`
|
||||
/// header.
|
||||
Copy
|
||||
}
|
||||
|
||||
impl Default for Headers {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
/// Get the header value for the `Access-Control-Allow-Headers` header.
|
||||
fn header_value(&self, state: &State) -> Option<HeaderValue> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::List(list) => Some(list.join(",").parse().unwrap()),
|
||||
Self::Copy => {
|
||||
let headers = HeaderMap::borrow_from(state);
|
||||
headers.get(ACCESS_CONTROL_REQUEST_HEADERS).map(Clone::clone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the `Vary` header has to include `Origin`.
|
||||
fn varies(&self) -> bool {
|
||||
matches!(self, Self::Copy)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,7 +111,8 @@ To change settings, you need to put this type into gotham's [State]:
|
|||
|
||||
```rust,no_run
|
||||
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use gotham_restful::{*, cors::Origin};
|
||||
# #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_doctest_main))]
|
||||
fn main() {
|
||||
let cors = CorsConfig {
|
||||
origin: Origin::Star,
|
||||
|
@ -81,7 +130,7 @@ configurations for different scopes, you need to register the middleware inside
|
|||
|
||||
```rust,no_run
|
||||
# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use gotham_restful::{*, cors::Origin};
|
||||
let pipelines = new_pipeline_set();
|
||||
|
||||
// The first cors configuration
|
||||
|
@ -119,7 +168,7 @@ pub struct CorsConfig {
|
|||
/// The allowed origins.
|
||||
pub origin: Origin,
|
||||
/// The allowed headers.
|
||||
pub headers: Vec<HeaderName>,
|
||||
pub headers: Headers,
|
||||
/// The amount of seconds that the preflight request can be cached.
|
||||
pub max_age: u64,
|
||||
/// Whether or not the request may be made with supplying credentials.
|
||||
|
@ -149,22 +198,24 @@ For further information on CORS, read
|
|||
*/
|
||||
pub fn handle_cors(state: &State, res: &mut Response<Body>) {
|
||||
let config = CorsConfig::try_borrow_from(state);
|
||||
if let Some(cfg) = config {
|
||||
let headers = res.headers_mut();
|
||||
|
||||
// non-preflight requests require the Access-Control-Allow-Origin header
|
||||
if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) {
|
||||
if let Some(header) = cfg.origin.header_value(state) {
|
||||
headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header);
|
||||
}
|
||||
|
||||
// if the origin is copied over, we should tell the browser by specifying the Vary header
|
||||
if matches!(config.map(|cfg| &cfg.origin), Some(Origin::Copy)) {
|
||||
let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap()));
|
||||
headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap());
|
||||
if cfg.origin.varies() {
|
||||
let vary = headers.get(VARY).map(|vary| format!("{},origin", vary.to_str().unwrap()));
|
||||
headers.insert(VARY, vary.as_deref().unwrap_or("origin").parse().unwrap());
|
||||
}
|
||||
|
||||
// if we allow credentials, tell the browser
|
||||
if config.map(|cfg| cfg.credentials).unwrap_or(false) {
|
||||
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap());
|
||||
if cfg.credentials {
|
||||
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,12 +247,13 @@ where
|
|||
fn cors(&mut self, path: &str, method: Method);
|
||||
}
|
||||
|
||||
fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
||||
pub(crate) fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
||||
let config = CorsConfig::try_borrow_from(&state);
|
||||
|
||||
// prepare the response
|
||||
let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
|
||||
let headers = res.headers_mut();
|
||||
let mut vary: Vec<HeaderName> = Vec::new();
|
||||
|
||||
// copy the request method over to the response
|
||||
let method = HeaderMap::borrow_from(&state)
|
||||
|
@ -209,22 +261,27 @@ fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
|||
.unwrap()
|
||||
.clone();
|
||||
headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method);
|
||||
vary.push(ACCESS_CONTROL_REQUEST_METHOD);
|
||||
|
||||
// if we allow any headers, put them in
|
||||
if let Some(hdrs) = config.map(|cfg| &cfg.headers) {
|
||||
if hdrs.len() > 0 {
|
||||
// TODO do we want to return all headers or just those asked by the browser?
|
||||
headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, hdrs.iter().join(",").parse().unwrap());
|
||||
if let Some(cfg) = config {
|
||||
// if we allow any headers, copy them over
|
||||
if let Some(header) = cfg.headers.header_value(&state) {
|
||||
headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, header);
|
||||
}
|
||||
|
||||
// if the headers are copied over, we should tell the browser by specifying the Vary header
|
||||
if cfg.headers.varies() {
|
||||
vary.push(ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
}
|
||||
|
||||
// set the max age for the preflight cache
|
||||
if let Some(age) = config.map(|cfg| cfg.max_age) {
|
||||
headers.insert(ACCESS_CONTROL_MAX_AGE, age.into());
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the browser knows that this request was based on the method
|
||||
headers.insert(VARY, "Access-Control-Request-Method".parse().unwrap());
|
||||
headers.insert(VARY, vary.join(",").parse().unwrap());
|
||||
|
||||
handle_cors(&state, &mut res);
|
||||
(state, res)
|
||||
|
|
136
src/endpoint.rs
Normal file
136
src/endpoint.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
use crate::{IntoResponse, RequestBody};
|
||||
use futures_util::future::BoxFuture;
|
||||
use gotham::{
|
||||
extractor::{PathExtractor, QueryStringExtractor},
|
||||
hyper::{Body, Method, Response},
|
||||
router::response::extender::StaticResponseExtender,
|
||||
state::{State, StateData}
|
||||
};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::{OpenapiSchema, OpenapiType};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// A no-op extractor that can be used as a default type for [Endpoint::Placeholders] and
|
||||
/// [Endpoint::Params].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NoopExtractor;
|
||||
|
||||
impl<'de> Deserialize<'de> for NoopExtractor {
|
||||
fn deserialize<D: Deserializer<'de>>(_: D) -> Result<Self, D::Error> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl OpenapiType for NoopExtractor {
|
||||
fn schema() -> OpenapiSchema {
|
||||
warn!("You're asking for the OpenAPI Schema for gotham_restful::NoopExtractor. This is probably not what you want.");
|
||||
<() as OpenapiType>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl StateData for NoopExtractor {}
|
||||
|
||||
impl StaticResponseExtender for NoopExtractor {
|
||||
type ResBody = Body;
|
||||
fn extend(_: &mut State, _: &mut Response<Body>) {}
|
||||
}
|
||||
|
||||
// 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.
|
||||
#[openapi_bound("Output: crate::ResponseSchema")]
|
||||
type Output: IntoResponse + Send;
|
||||
|
||||
/// Returns `true` _iff_ the URI contains placeholders. `false` by default.
|
||||
fn has_placeholders() -> bool {
|
||||
false
|
||||
}
|
||||
/// The type that parses the URI placeholders. Use [NoopExtractor] if `has_placeholders()`
|
||||
/// returns `false`.
|
||||
#[openapi_bound("Placeholders: OpenapiType")]
|
||||
type Placeholders: PathExtractor<Body> + Clone + 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 [NoopExtractor] if `needs_params()`
|
||||
/// returns `false`.
|
||||
#[openapi_bound("Params: OpenapiType")]
|
||||
type Params: QueryStringExtractor<Body> + Clone + 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<'a>(
|
||||
state: &'a mut State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: Option<Self::Body>
|
||||
) -> BoxFuture<'a, 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<'a>(
|
||||
state: &'a mut State,
|
||||
placeholders: Self::Placeholders,
|
||||
params: Self::Params,
|
||||
body: Option<Self::Body>
|
||||
) -> BoxFuture<'a, Self::Output> {
|
||||
E::handle(state, placeholders, params, body)
|
||||
}
|
||||
}
|
269
src/lib.rs
269
src/lib.rs
|
@ -1,10 +1,13 @@
|
|||
#![allow(clippy::tabs_in_doc_comments)]
|
||||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||
#![deny(broken_intra_doc_links)]
|
||||
#![forbid(unsafe_code)]
|
||||
// can we have a lint for spaces in doc comments please?
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))]
|
||||
// intra-doc links only fully work when OpenAPI is enabled
|
||||
#![cfg_attr(feature = "openapi", deny(broken_intra_doc_links))]
|
||||
#![cfg_attr(not(feature = "openapi"), allow(broken_intra_doc_links))]
|
||||
/*!
|
||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
||||
create resources with assigned methods that aim to be a more convenient way of creating handlers
|
||||
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
|
||||
for requests.
|
||||
|
||||
# Features
|
||||
|
@ -22,12 +25,17 @@ for requests.
|
|||
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
||||
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
|
||||
|
||||
# Methods
|
||||
# Endpoints
|
||||
|
||||
Assuming you assign `/foobar` to your resource, you can implement the following methods:
|
||||
There are a set of pre-defined endpoints that should cover the majority of REST APIs. However,
|
||||
it is also possible to define your own endpoints.
|
||||
|
||||
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ----------- | ------------------ | --------- | ----------- |
|
||||
## Pre-defined Endpoints
|
||||
|
||||
Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist:
|
||||
|
||||
| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path |
|
||||
| ------------- | ------------------ | --------- | -------------- |
|
||||
| read_all | | GET | /foobar |
|
||||
| read | id | GET | /foobar/:id |
|
||||
| search | query | GET | /foobar/search |
|
||||
|
@ -37,8 +45,8 @@ Assuming you assign `/foobar` to your resource, you can implement the following
|
|||
| remove_all | | DELETE | /foobar |
|
||||
| remove | id | DELETE | /foobar/:id |
|
||||
|
||||
Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A
|
||||
simple example could look like this:
|
||||
Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A
|
||||
simple example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -50,15 +58,15 @@ simple example could look like this:
|
|||
#[resource(read)]
|
||||
struct FooResource;
|
||||
|
||||
/// The return type of the foo read method.
|
||||
/// The return type of the foo read endpoint.
|
||||
#[derive(Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Foo {
|
||||
id: u64
|
||||
}
|
||||
|
||||
/// The foo read method handler.
|
||||
#[read(FooResource)]
|
||||
/// The foo read endpoint.
|
||||
#[read]
|
||||
fn read(id: u64) -> Success<Foo> {
|
||||
Foo { id }.into()
|
||||
}
|
||||
|
@ -69,19 +77,53 @@ fn read(id: u64) -> Success<Foo> {
|
|||
# }
|
||||
```
|
||||
|
||||
## Custom Endpoints
|
||||
|
||||
Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that
|
||||
of the pre-defined endpoints, but you need to give it more context:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_derive;
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::router::builder::*;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
use gotham_restful::gotham::hyper::Method;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(custom_endpoint)]
|
||||
struct CustomResource;
|
||||
|
||||
/// This type is used to parse path parameters.
|
||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct CustomPath {
|
||||
name: String
|
||||
}
|
||||
|
||||
#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)]
|
||||
fn custom_endpoint(path: CustomPath) -> Success<String> {
|
||||
path.name.into()
|
||||
}
|
||||
# fn main() {
|
||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
# route.resource::<CustomResource>("custom");
|
||||
# }));
|
||||
# }
|
||||
```
|
||||
|
||||
# Arguments
|
||||
|
||||
Some methods require arguments. Those should be
|
||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
||||
Some endpoints require arguments. Those should be
|
||||
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
|
||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
||||
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
||||
type needs to implement [`QueryStringExtractor`].
|
||||
type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor).
|
||||
|
||||
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
|
||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
||||
`Sync` there is unfortunately no more convenient way.
|
||||
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
|
||||
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
|
||||
improve.
|
||||
|
||||
# Uploads and Downloads
|
||||
|
||||
|
@ -105,7 +147,7 @@ struct RawImage {
|
|||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(ImageResource)]
|
||||
#[create]
|
||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
|
@ -116,27 +158,60 @@ fn create(body : RawImage) -> Raw<Vec<u8>> {
|
|||
# }
|
||||
```
|
||||
|
||||
# Custom HTTP Headers
|
||||
|
||||
You can read request headers from the state as you would in any other gotham handler, and specify
|
||||
custom response headers using [Response::header].
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# use gotham::hyper::header::{ACCEPT, HeaderMap, VARY};
|
||||
# use gotham::{router::builder::*, state::State};
|
||||
# use gotham_restful::*;
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all]
|
||||
async fn read_all(state: &mut State) -> NoContent {
|
||||
let headers: &HeaderMap = state.borrow();
|
||||
let accept = &headers[ACCEPT];
|
||||
# drop(accept);
|
||||
|
||||
let mut res = NoContent::default();
|
||||
res.header(VARY, "accept".parse().unwrap());
|
||||
res
|
||||
}
|
||||
# fn main() {
|
||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||
# route.resource::<FooResource>("foo");
|
||||
# }));
|
||||
# }
|
||||
```
|
||||
|
||||
# Features
|
||||
|
||||
To make life easier for common use-cases, this create offers a few features that might be helpful
|
||||
when you implement your web server. The complete feature list is
|
||||
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||
- `chrono` openapi support for chrono types
|
||||
- [`cors`](#cors-feature) CORS handling for all method handlers
|
||||
- `full` enables all features except `without-openapi`
|
||||
- [`cors`](#cors-feature) CORS handling for all endpoint handlers
|
||||
- [`database`](#database-feature) diesel middleware support
|
||||
- `errorlog` log errors returned from method handlers
|
||||
- `errorlog` log errors returned from endpoint handlers
|
||||
- [`openapi`](#openapi-feature) router additions to generate an openapi spec
|
||||
- `uuid` openapi support for uuid
|
||||
- `without-openapi` (**default**) disables `openapi` support.
|
||||
|
||||
## Authentication Feature
|
||||
|
||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
||||
register a middleware that can automatically check for the existence of an JWT authentication
|
||||
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
|
||||
token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret
|
||||
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
||||
None of this is currently supported by gotham's own JWT middleware.
|
||||
|
||||
A simple example that uses only a single secret could look like this:
|
||||
A simple example that uses only a single secret looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -150,7 +225,7 @@ A simple example that uses only a single secret could look like this:
|
|||
struct SecretResource;
|
||||
|
||||
#[derive(Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Secret {
|
||||
id: u64,
|
||||
intended_for: String
|
||||
|
@ -162,7 +237,7 @@ struct AuthData {
|
|||
exp: u64
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
#[read]
|
||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||
let intended_for = auth.ok()?.sub;
|
||||
Ok(Secret { id, intended_for })
|
||||
|
@ -189,20 +264,20 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a
|
|||
configuration as a middleware.
|
||||
|
||||
A simple example that allows authentication from every origin (note that `*` always disallows
|
||||
authentication), and every content type, could look like this:
|
||||
authentication), and every content type, looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# #[cfg(feature = "cors")]
|
||||
# mod cors_feature_enabled {
|
||||
# use gotham::{hyper::header::*, router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use gotham_restful::{*, cors::*};
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() {
|
||||
// your handler
|
||||
}
|
||||
|
@ -210,7 +285,7 @@ fn read_all() {
|
|||
fn main() {
|
||||
let cors = CorsConfig {
|
||||
origin: Origin::Copy,
|
||||
headers: vec![CONTENT_TYPE],
|
||||
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||
max_age: 0,
|
||||
credentials: true
|
||||
};
|
||||
|
@ -232,7 +307,7 @@ note however that due to the way gotham's diesel middleware implementation, it i
|
|||
to run async code while holding a database connection. If you need to combine async and database,
|
||||
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
|
||||
|
||||
A simple non-async example could look like this:
|
||||
A simple non-async example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate diesel;
|
||||
|
@ -256,13 +331,13 @@ A simple non-async example could look like this:
|
|||
struct FooResource;
|
||||
|
||||
#[derive(Queryable, Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Foo {
|
||||
id: i64,
|
||||
value: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||
foo::table.load(conn)
|
||||
}
|
||||
|
@ -288,9 +363,9 @@ carefully both as a binary as well as a library author to avoid unwanted suprise
|
|||
|
||||
In order to automatically create an openapi specification, gotham-restful needs knowledge over
|
||||
all routes and the types returned. `serde` does a great job at serialization but doesn't give
|
||||
enough type information, so all types used in the router need to implement `OpenapiType`. This
|
||||
can be derived for almoust any type and there should be no need to implement it manually. A simple
|
||||
example could look like this:
|
||||
enough type information, so all types used in the router need to implement
|
||||
`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there
|
||||
should be no need to implement it manually. A simple example looks like this:
|
||||
|
||||
```rust,no_run
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
|
@ -298,6 +373,7 @@ example could look like this:
|
|||
# mod openapi_feature_enabled {
|
||||
# use gotham::{router::builder::*, state::State};
|
||||
# use gotham_restful::*;
|
||||
# use openapi_type::OpenapiType;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
|
@ -308,7 +384,7 @@ struct Foo {
|
|||
bar: String
|
||||
}
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Success<Foo> {
|
||||
Foo { bar: "Hello World".to_owned() }.into()
|
||||
}
|
||||
|
@ -329,62 +405,54 @@ fn main() {
|
|||
# }
|
||||
```
|
||||
|
||||
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`
|
||||
that will return the generated openapi specification. This allows you to easily write clients
|
||||
in different languages without worying to exactly replicate your api in each of those languages.
|
||||
Above example adds the resource as before, but adds another endpoint that we specified as `/openapi`.
|
||||
It will return the generated openapi specification in JSON format. This allows you to easily write
|
||||
clients in different languages without worying to exactly replicate your api in each of those
|
||||
languages.
|
||||
|
||||
However, as of right now there is one caveat. If you wrote code before enabling the openapi feature,
|
||||
it is likely to break. This is because of the new requirement of `OpenapiType` for all types used
|
||||
with resources, even outside of the `with_openapi` scope. This issue will eventually be resolved.
|
||||
If you are writing a library that uses gotham-restful, make sure that you expose an openapi feature.
|
||||
In other words, put
|
||||
However, please note that by default, the `without-openapi` feature of this crate is enabled.
|
||||
Disabling it in favour of the `openapi` feature will add an additional type bound,
|
||||
[`OpenapiType`][openapi_type::OpenapiType], on some of the types in [`Endpoint`] and related
|
||||
traits. This means that some code might only compile on either feature, but not on both. If you
|
||||
are writing a library that uses gotham-restful, it is strongly recommended to pass both features
|
||||
through and conditionally enable the openapi code, like this:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
openapi = ["gotham-restful/openapi"]
|
||||
```
|
||||
|
||||
into your libraries `Cargo.toml` and use the following for all types used with handlers:
|
||||
|
||||
```
|
||||
# #[cfg(feature = "openapi")]
|
||||
# mod openapi_feature_enabled {
|
||||
# use gotham_restful::OpenapiType;
|
||||
```rust
|
||||
# #[macro_use] extern crate gotham_restful;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Foo;
|
||||
# }
|
||||
```
|
||||
|
||||
# Examples
|
||||
|
||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
||||
|
||||
# License
|
||||
|
||||
Licensed under your option of:
|
||||
- [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache)
|
||||
- [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL)
|
||||
This readme and the crate documentation contain some of example. In addition to that, there is
|
||||
a collection of code in the [example] directory that might help you. Any help writing more
|
||||
examples is highly appreciated.
|
||||
|
||||
|
||||
[diesel]: https://diesel.rs/
|
||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||
[gotham]: https://gotham.rs/
|
||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
||||
[`CorsRoute`]: trait.CorsRoute.html
|
||||
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
|
||||
[`RequestBody`]: trait.RequestBody.html
|
||||
[`State`]: ../gotham/state/struct.State.html
|
||||
[`State`]: gotham::state::State
|
||||
*/
|
||||
|
||||
#[cfg(all(feature = "openapi", feature = "without-openapi"))]
|
||||
compile_error!("The 'openapi' and 'without-openapi' features cannot be combined");
|
||||
|
||||
#[cfg(all(not(feature = "openapi"), not(feature = "without-openapi")))]
|
||||
compile_error!("Either the 'openapi' or 'without-openapi' feature needs to be enabled");
|
||||
|
||||
// weird proc macro issue
|
||||
extern crate self as gotham_restful;
|
||||
|
||||
#[macro_use]
|
||||
extern crate gotham_derive;
|
||||
#[macro_use]
|
||||
extern crate gotham_restful_derive;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
@ -392,19 +460,16 @@ extern crate serde;
|
|||
#[doc(no_inline)]
|
||||
pub use gotham;
|
||||
#[doc(no_inline)]
|
||||
pub use gotham::{
|
||||
hyper::{header::HeaderName, StatusCode},
|
||||
state::{FromState, State}
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use mime::Mime;
|
||||
|
||||
pub use gotham_restful_derive::*;
|
||||
|
||||
/// Not public API
|
||||
#[doc(hidden)]
|
||||
pub mod export {
|
||||
pub use futures_util::future::FutureExt;
|
||||
pub mod private {
|
||||
pub use crate::routing::PathExtractor as IdPlaceholder;
|
||||
|
||||
pub use futures_util::future::{BoxFuture, FutureExt};
|
||||
|
||||
pub use serde_json;
|
||||
|
||||
|
@ -414,6 +479,8 @@ pub mod export {
|
|||
#[cfg(feature = "openapi")]
|
||||
pub use indexmap::IndexMap;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapi_type::{OpenapiSchema, OpenapiType};
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapiv3 as openapi;
|
||||
}
|
||||
|
||||
|
@ -423,38 +490,44 @@ mod auth;
|
|||
pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler};
|
||||
|
||||
#[cfg(feature = "cors")]
|
||||
mod cors;
|
||||
pub mod cors;
|
||||
#[cfg(feature = "cors")]
|
||||
pub use cors::{handle_cors, CorsConfig, CorsRoute, Origin};
|
||||
pub use cors::{handle_cors, CorsConfig, CorsRoute};
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
mod openapi;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use openapi::{
|
||||
builder::OpenapiInfo,
|
||||
router::GetOpenapi,
|
||||
types::{OpenapiSchema, OpenapiType}
|
||||
};
|
||||
pub use openapi::{builder::OpenapiInfo, router::GetOpenapi};
|
||||
|
||||
mod resource;
|
||||
pub use resource::{
|
||||
Resource, ResourceChange, ResourceChangeAll, ResourceCreate, ResourceMethod, ResourceRead, ResourceReadAll,
|
||||
ResourceRemove, ResourceRemoveAll, ResourceSearch
|
||||
};
|
||||
mod endpoint;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use endpoint::EndpointWithSchema;
|
||||
pub use endpoint::{Endpoint, NoopExtractor};
|
||||
|
||||
mod response;
|
||||
pub use response::Response;
|
||||
|
||||
mod result;
|
||||
pub use result::{
|
||||
AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponseError, NoContent, Raw,
|
||||
ResourceResult, Success
|
||||
pub use response::{
|
||||
AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent,
|
||||
Raw, Redirect, Response, Success
|
||||
};
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use response::{IntoResponseWithSchema, ResponseSchema};
|
||||
|
||||
mod routing;
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use routing::WithOpenapi;
|
||||
pub use routing::{DrawResourceRoutes, DrawResources};
|
||||
#[cfg(feature = "openapi")]
|
||||
pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi};
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
/// This trait must be implemented for every resource. It allows you to register the different
|
||||
/// endpoints that can be handled by this resource to be registered with the underlying router.
|
||||
///
|
||||
/// It is not recommended to implement this yourself, 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 crate::{OpenapiSchema, OpenapiType};
|
||||
use indexmap::IndexMap;
|
||||
use openapi_type::OpenapiSchema;
|
||||
use openapiv3::{
|
||||
Components, OpenAPI, PathItem, ReferenceOr,
|
||||
ReferenceOr::{Item, Reference},
|
||||
|
@ -83,8 +83,7 @@ impl OpenapiBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_schema<T: OpenapiType>(&mut self) -> ReferenceOr<Schema> {
|
||||
let mut schema = T::schema();
|
||||
pub fn add_schema(&mut self, mut schema: OpenapiSchema) -> ReferenceOr<Schema> {
|
||||
match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = Reference {
|
||||
|
@ -105,6 +104,7 @@ impl OpenapiBuilder {
|
|||
#[allow(dead_code)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use openapi_type::OpenapiType;
|
||||
|
||||
#[derive(OpenapiType)]
|
||||
struct Message {
|
||||
|
@ -142,7 +142,7 @@ mod test {
|
|||
#[test]
|
||||
fn add_schema() {
|
||||
let mut builder = OpenapiBuilder::new(info());
|
||||
builder.add_schema::<Option<Messages>>();
|
||||
builder.add_schema(<Option<Messages>>::schema());
|
||||
let openapi = openapi(builder);
|
||||
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,38 +1,29 @@
|
|||
#![cfg_attr(not(feature = "auth"), allow(unused_imports))]
|
||||
use super::SECURITY_NAME;
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::{
|
||||
anyhow,
|
||||
handler::{Handler, HandlerFuture, NewHandler},
|
||||
helpers::http::response::create_response,
|
||||
helpers::http::response::{create_empty_response, create_response},
|
||||
hyper::{
|
||||
header::{
|
||||
HeaderMap, HeaderValue, CACHE_CONTROL, CONTENT_SECURITY_POLICY, ETAG, IF_NONE_MATCH, REFERRER_POLICY,
|
||||
X_CONTENT_TYPE_OPTIONS
|
||||
},
|
||||
Body, Response, StatusCode, Uri
|
||||
},
|
||||
state::State
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||
use mime::{APPLICATION_JSON, TEXT_HTML, TEXT_PLAIN};
|
||||
use once_cell::sync::Lazy;
|
||||
use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock}
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenapiHandler {
|
||||
openapi: Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl OpenapiHandler {
|
||||
pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for OpenapiHandler {
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> anyhow::Result<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
fn get_security(state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
|
||||
use crate::AuthSource;
|
||||
|
@ -65,37 +56,206 @@ fn get_security(state: &mut State) -> IndexMap<String, ReferenceOr<SecuritySchem
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "auth"))]
|
||||
fn get_security(state: &mut State) -> (Vec<SecurityRequirement>, IndexMap<String, ReferenceOr<SecurityScheme>>) {
|
||||
fn get_security(_state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
impl Handler for OpenapiHandler {
|
||||
fn handle(self, mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let openapi = match self.openapi.read() {
|
||||
fn create_openapi_response(state: &mut State, openapi: &Arc<RwLock<OpenAPI>>) -> Response<Body> {
|
||||
let openapi = match openapi.read() {
|
||||
Ok(openapi) => openapi,
|
||||
Err(e) => {
|
||||
error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
|
||||
let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
return future::ok((state, res)).boxed();
|
||||
return create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
}
|
||||
};
|
||||
|
||||
let mut openapi = openapi.clone();
|
||||
let security_schemes = get_security(&mut state);
|
||||
let security_schemes = get_security(state);
|
||||
let mut components = openapi.components.unwrap_or_default();
|
||||
components.security_schemes = security_schemes;
|
||||
openapi.components = Some(components);
|
||||
|
||||
match serde_json::to_string(&openapi) {
|
||||
Ok(body) => {
|
||||
let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body);
|
||||
future::ok((state, res)).boxed()
|
||||
let mut res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body);
|
||||
let headers = res.headers_mut();
|
||||
headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
|
||||
res
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Unable to handle OpenAPI request due to error: {}", e);
|
||||
let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenapiHandler {
|
||||
openapi: Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl OpenapiHandler {
|
||||
pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for OpenapiHandler {
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> anyhow::Result<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for OpenapiHandler {
|
||||
fn handle(self, mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let res = create_openapi_response(&mut state, &self.openapi);
|
||||
future::ok((state, res)).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SwaggerUiHandler {
|
||||
openapi: Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl SwaggerUiHandler {
|
||||
pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for SwaggerUiHandler {
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> anyhow::Result<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for SwaggerUiHandler {
|
||||
fn handle(self, mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let uri: &Uri = state.borrow();
|
||||
let query = uri.query();
|
||||
match query {
|
||||
// TODO this is hacky
|
||||
Some(q) if q.contains("spec") => {
|
||||
let res = create_openapi_response(&mut state, &self.openapi);
|
||||
future::ok((state, res)).boxed()
|
||||
},
|
||||
_ => {
|
||||
{
|
||||
let headers: &HeaderMap = state.borrow();
|
||||
if headers
|
||||
.get(IF_NONE_MATCH)
|
||||
.map_or(false, |etag| etag.as_bytes() == SWAGGER_UI_HTML_ETAG.as_bytes())
|
||||
{
|
||||
let res = create_empty_response(&state, StatusCode::NOT_MODIFIED);
|
||||
return future::ok((state, res)).boxed();
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = create_response(&state, StatusCode::OK, TEXT_HTML, SWAGGER_UI_HTML.as_bytes());
|
||||
let headers = res.headers_mut();
|
||||
headers.insert(CACHE_CONTROL, HeaderValue::from_static("public,max-age=2592000"));
|
||||
headers.insert(CONTENT_SECURITY_POLICY, format!("default-src 'none'; script-src 'unsafe-inline' 'sha256-{}' 'strict-dynamic'; style-src 'unsafe-inline' https://cdnjs.cloudflare.com; connect-src 'self'; img-src data:;", SWAGGER_UI_SCRIPT_HASH.as_str()).parse().unwrap());
|
||||
headers.insert(ETAG, SWAGGER_UI_HTML_ETAG.parse().unwrap());
|
||||
headers.insert(REFERRER_POLICY, HeaderValue::from_static("strict-origin-when-cross-origin"));
|
||||
headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
|
||||
future::ok((state, res)).boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inspired by https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html
|
||||
const SWAGGER_UI_HTML: Lazy<&'static String> = Lazy::new(|| {
|
||||
let template = indoc::indoc! {
|
||||
r#"
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui.css" integrity="sha512-sphGjcjFvN5sAW6S28Ge+F9SCzRuc9IVkLinDHu7B1wOUHHFAY5sSQ2Axff+qs7/0GTm0Ifg4i0lQKgM8vdV2w==" crossorigin="anonymous"/>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script>{{script}}</script>
|
||||
</body>
|
||||
</html>
|
||||
"#
|
||||
};
|
||||
Box::leak(Box::new(template.replace("{{script}}", SWAGGER_UI_SCRIPT)))
|
||||
});
|
||||
static SWAGGER_UI_HTML_ETAG: Lazy<String> = Lazy::new(|| {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(SWAGGER_UI_HTML.as_bytes());
|
||||
let hash = hash.finalize();
|
||||
let hash = base64::encode(hash);
|
||||
format!("\"{}\"", hash)
|
||||
});
|
||||
const SWAGGER_UI_SCRIPT: &str = r#"
|
||||
let s0rdy = false;
|
||||
let s1rdy = false;
|
||||
|
||||
window.onload = function() {
|
||||
const cb = function() {
|
||||
if (!s0rdy || !s1rdy)
|
||||
return;
|
||||
const ui = SwaggerUIBundle({
|
||||
url: window.location.origin + window.location.pathname + '?spec',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: 'StandaloneLayout'
|
||||
});
|
||||
window.ui = ui;
|
||||
};
|
||||
|
||||
const s0 = document.createElement('script');
|
||||
s0.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-bundle.js');
|
||||
s0.setAttribute('integrity', 'sha512-EfK//grBlevo9MrtEDyNvf4SkBA0avHZoVLEuSR2Yl6ymnjcIwClgZ7FXdr/42yGqnhEHxb+Sv/bJeUp26YPRw==');
|
||||
s0.setAttribute('crossorigin', 'anonymous');
|
||||
s0.onload = function() {
|
||||
s0rdy = true;
|
||||
cb();
|
||||
};
|
||||
document.head.appendChild(s0);
|
||||
|
||||
const s1 = document.createElement('script');
|
||||
s1.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-standalone-preset.js');
|
||||
s1.setAttribute('integrity', 'sha512-Hbx9NyAhG+P6YNBU9mp6hc6ntRjGYHqo/qae4OHhzPA69xbNmF8n2aJxpzUwdbXbYICO6eor4IhgSfiSQm9OYg==');
|
||||
s1.setAttribute('crossorigin', 'anonymous');
|
||||
s1.onload = function() {
|
||||
s1rdy = true;
|
||||
cb();
|
||||
};
|
||||
document.head.appendChild(s1);
|
||||
};
|
||||
"#;
|
||||
static SWAGGER_UI_SCRIPT_HASH: Lazy<String> = Lazy::new(|| {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(SWAGGER_UI_SCRIPT);
|
||||
let hash = hash.finalize();
|
||||
base64::encode(hash)
|
||||
});
|
||||
|
|
|
@ -4,4 +4,3 @@ pub mod builder;
|
|||
pub mod handler;
|
||||
pub mod operation;
|
||||
pub mod router;
|
||||
pub mod types;
|
||||
|
|
|
@ -1,38 +1,48 @@
|
|||
use super::SECURITY_NAME;
|
||||
use crate::{resource::*, result::*, OpenapiSchema, RequestBody};
|
||||
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema};
|
||||
use indexmap::IndexMap;
|
||||
use mime::Mime;
|
||||
use openapi_type::OpenapiSchema;
|
||||
use openapiv3::{
|
||||
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
|
||||
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct OperationParams<'a> {
|
||||
path_params: Vec<(&'a str, ReferenceOr<Schema>)>,
|
||||
struct OperationParams {
|
||||
path_params: Option<OpenapiSchema>,
|
||||
query_params: Option<OpenapiSchema>
|
||||
}
|
||||
|
||||
impl<'a> OperationParams<'a> {
|
||||
fn add_path_params(&self, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
for param in &self.path_params {
|
||||
impl OperationParams {
|
||||
fn add_path_params(path_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
let path_params = match path_params {
|
||||
Some(pp) => pp.schema,
|
||||
None => return
|
||||
};
|
||||
let path_params = match path_params {
|
||||
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||
_ => panic!("Path Parameters needs to be a plain struct")
|
||||
};
|
||||
for (name, schema) in path_params.properties {
|
||||
let required = path_params.required.contains(&name);
|
||||
params.push(Item(Parameter::Path {
|
||||
parameter_data: ParameterData {
|
||||
name: (*param).0.to_string(),
|
||||
name,
|
||||
description: None,
|
||||
required: true,
|
||||
required,
|
||||
deprecated: None,
|
||||
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
|
||||
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||
example: None,
|
||||
examples: IndexMap::new()
|
||||
},
|
||||
style: Default::default()
|
||||
}));
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn add_query_params(self, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
let query_params = match self.query_params {
|
||||
fn add_query_params(query_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||
let query_params = match query_params {
|
||||
Some(qp) => qp.schema,
|
||||
None => return
|
||||
};
|
||||
|
@ -61,51 +71,48 @@ impl<'a> OperationParams<'a> {
|
|||
|
||||
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
|
||||
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
|
||||
self.add_path_params(&mut params);
|
||||
self.add_query_params(&mut params);
|
||||
Self::add_path_params(self.path_params, &mut params);
|
||||
Self::add_query_params(self.query_params, &mut params);
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OperationDescription<'a> {
|
||||
pub struct OperationDescription {
|
||||
operation_id: Option<String>,
|
||||
default_status: crate::StatusCode,
|
||||
default_status: gotham::hyper::StatusCode,
|
||||
accepted_types: Option<Vec<Mime>>,
|
||||
schema: ReferenceOr<Schema>,
|
||||
params: OperationParams<'a>,
|
||||
params: OperationParams,
|
||||
body_schema: Option<ReferenceOr<Schema>>,
|
||||
supported_types: Option<Vec<Mime>>,
|
||||
requires_auth: bool
|
||||
}
|
||||
|
||||
impl<'a> OperationDescription<'a> {
|
||||
pub fn new<Handler: ResourceMethod>(schema: ReferenceOr<Schema>) -> Self {
|
||||
impl OperationDescription {
|
||||
pub fn new<E: EndpointWithSchema>(schema: ReferenceOr<Schema>) -> Self {
|
||||
Self {
|
||||
operation_id: Handler::operation_id(),
|
||||
default_status: Handler::Res::default_status(),
|
||||
accepted_types: Handler::Res::accepted_types(),
|
||||
operation_id: E::operation_id(),
|
||||
default_status: E::Output::default_status(),
|
||||
accepted_types: E::Output::accepted_types(),
|
||||
schema,
|
||||
params: Default::default(),
|
||||
body_schema: None,
|
||||
supported_types: None,
|
||||
requires_auth: Handler::wants_auth()
|
||||
requires_auth: E::wants_auth()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path_param(mut self, name: &'a str, schema: ReferenceOr<Schema>) -> Self {
|
||||
self.params.path_params.push((name, schema));
|
||||
self
|
||||
pub fn set_path_params(&mut self, params: OpenapiSchema) {
|
||||
self.params.path_params = Some(params);
|
||||
}
|
||||
|
||||
pub fn with_query_params(mut self, params: OpenapiSchema) -> Self {
|
||||
pub fn set_query_params(&mut self, params: OpenapiSchema) {
|
||||
self.params.query_params = Some(params);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_body<Body: RequestBody>(mut self, schema: ReferenceOr<Schema>) -> Self {
|
||||
pub fn set_body<Body: RequestBody>(&mut self, schema: ReferenceOr<Schema>) {
|
||||
self.body_schema = Some(schema);
|
||||
self.supported_types = Body::supported_types();
|
||||
self
|
||||
}
|
||||
|
||||
fn schema_to_content(types: Vec<Mime>, schema: ReferenceOr<Schema>) -> IndexMap<String, MediaType> {
|
||||
|
@ -178,12 +185,12 @@ impl<'a> OperationDescription<'a> {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{OpenapiType, ResourceResult};
|
||||
use crate::{NoContent, Raw, ResponseSchema};
|
||||
|
||||
#[test]
|
||||
fn no_content_schema_to_content() {
|
||||
let types = NoContent::accepted_types();
|
||||
let schema = <NoContent as OpenapiType>::schema();
|
||||
let schema = <NoContent as ResponseSchema>::schema();
|
||||
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||
assert!(content.is_empty());
|
||||
}
|
||||
|
@ -191,7 +198,7 @@ mod test {
|
|||
#[test]
|
||||
fn raw_schema_to_content() {
|
||||
let types = Raw::<&str>::accepted_types();
|
||||
let schema = <Raw<&str> as OpenapiType>::schema();
|
||||
let schema = <Raw<&str> as ResponseSchema>::schema();
|
||||
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||
assert_eq!(content.len(), 1);
|
||||
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
|
||||
use crate::{resource::*, routing::*, OpenapiType};
|
||||
use gotham::{pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||
use super::{
|
||||
builder::OpenapiBuilder,
|
||||
handler::{OpenapiHandler, SwaggerUiHandler},
|
||||
operation::OperationDescription
|
||||
};
|
||||
use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema};
|
||||
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||
use once_cell::sync::Lazy;
|
||||
use openapi_type::OpenapiType;
|
||||
use regex::{Captures, Regex};
|
||||
use std::panic::RefUnwindSafe;
|
||||
|
||||
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
|
||||
/// This trait adds the `get_openapi` and `swagger_ui` method to an OpenAPI-aware router.
|
||||
pub trait GetOpenapi {
|
||||
fn get_openapi(&mut self, path: &str);
|
||||
fn swagger_ui(&mut self, path: &str);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -49,152 +57,74 @@ macro_rules! implOpenapiRouter {
|
|||
.get(path)
|
||||
.to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone()));
|
||||
}
|
||||
|
||||
fn swagger_ui(&mut self, path: &str) {
|
||||
self.router
|
||||
.get(path)
|
||||
.to_new_handler(SwaggerUiHandler::new(self.openapi_builder.openapi.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
where
|
||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P: RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn resource<R: Resource>(&mut self, path: &str) {
|
||||
fn resource<R: ResourceWithSchema>(&mut self, path: &str) {
|
||||
R::setup((self, path));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||
impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||
where
|
||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P: RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn read_all<Handler: ResourceReadAll>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read_all::<Handler>()
|
||||
fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema(E::Output::schema());
|
||||
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::schema());
|
||||
descr.set_body::<E::Body>(body_schema);
|
||||
}
|
||||
|
||||
fn read<Handler: ResourceRead>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
static URI_PLACEHOLDER_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(?P<prefix>^|/):(?P<name>[^/]+)(?P<suffix>/|$)"#).unwrap());
|
||||
let uri: &str = &E::uri();
|
||||
let uri = URI_PLACEHOLDER_REGEX.replace_all(uri, |captures: &Captures<'_>| {
|
||||
format!(
|
||||
"{}{{{}}}{}",
|
||||
&captures["prefix"], &captures["name"], &captures["suffix"]
|
||||
)
|
||||
});
|
||||
let path = if uri.is_empty() {
|
||||
format!("{}/{}", self.0.scope.unwrap_or_default(), self.1)
|
||||
} else {
|
||||
format!("{}/{}/{}", self.0.scope.unwrap_or_default(), self.1, uri)
|
||||
};
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let op = descr.into_operation();
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.add_path_param("id", id_schema)
|
||||
.into_operation()
|
||||
);
|
||||
match E::http_method() {
|
||||
Method::GET => item.get = Some(op),
|
||||
Method::PUT => item.put = Some(op),
|
||||
Method::POST => item.post = Some(op),
|
||||
Method::DELETE => item.delete = Some(op),
|
||||
Method::OPTIONS => item.options = Some(op),
|
||||
Method::HEAD => item.head = Some(op),
|
||||
Method::PATCH => item.patch = Some(op),
|
||||
Method::TRACE => item.trace = Some(op),
|
||||
method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method)
|
||||
};
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).read::<Handler>()
|
||||
}
|
||||
|
||||
fn search<Handler: ResourceSearch>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.get = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.with_query_params(Handler::Query::schema())
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).search::<Handler>()
|
||||
}
|
||||
|
||||
fn create<Handler: ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.post = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.with_body::<Handler::Body>(body_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).create::<Handler>()
|
||||
}
|
||||
|
||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.put = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.with_body::<Handler::Body>(body_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).change_all::<Handler>()
|
||||
}
|
||||
|
||||
fn change<Handler: ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.put = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.add_path_param("id", id_schema)
|
||||
.with_body::<Handler::Body>(body_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).change::<Handler>()
|
||||
}
|
||||
|
||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
|
||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).remove_all::<Handler>()
|
||||
}
|
||||
|
||||
fn remove<Handler: ResourceRemove>(&mut self) {
|
||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
||||
|
||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||
item.delete = Some(
|
||||
OperationDescription::new::<Handler>(schema)
|
||||
.add_path_param("id", id_schema)
|
||||
.into_operation()
|
||||
);
|
||||
(self.0).openapi_builder.add_path(path, item);
|
||||
|
||||
(&mut *(self.0).router, self.1).remove::<Handler>()
|
||||
(&mut *(self.0).router, self.1).endpoint::<E>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,443 +0,0 @@
|
|||
#[cfg(feature = "chrono")]
|
||||
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
|
||||
use indexmap::IndexMap;
|
||||
use openapiv3::{
|
||||
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType,
|
||||
ReferenceOr::{Item, Reference},
|
||||
Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
|
||||
};
|
||||
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap, HashSet},
|
||||
hash::BuildHasher,
|
||||
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
|
||||
};
|
||||
#[cfg(feature = "uuid")]
|
||||
use uuid::Uuid;
|
||||
|
||||
/**
|
||||
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
|
||||
already implemented for primitive types, String, Vec, Option and the like. To have it available
|
||||
for your type, simply derive from [OpenapiType].
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct OpenapiSchema {
|
||||
/// The name of this schema. If it is None, the schema will be inlined.
|
||||
pub name: Option<String>,
|
||||
/// Whether this particular schema is nullable. Note that there is no guarantee that this will
|
||||
/// make it into the final specification, it might just be interpreted as a hint to make it
|
||||
/// an optional parameter.
|
||||
pub nullable: bool,
|
||||
/// The actual OpenAPI schema.
|
||||
pub schema: SchemaKind,
|
||||
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
|
||||
/// along with this schema.
|
||||
pub dependencies: IndexMap<String, OpenapiSchema>
|
||||
}
|
||||
|
||||
impl OpenapiSchema {
|
||||
/// Create a new schema that has no name.
|
||||
pub fn new(schema: SchemaKind) -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
nullable: false,
|
||||
schema,
|
||||
dependencies: IndexMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this schema to an [openapiv3::Schema] that can be serialized to the OpenAPI Spec.
|
||||
pub fn into_schema(self) -> Schema {
|
||||
Schema {
|
||||
schema_data: SchemaData {
|
||||
nullable: self.nullable,
|
||||
title: self.name,
|
||||
..Default::default()
|
||||
},
|
||||
schema_kind: self.schema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives
|
||||
access to the [OpenapiSchema] of this type. It is provided for primitive types, String and the
|
||||
like. For use on your own types, there is a derive macro:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
#
|
||||
#[derive(OpenapiType)]
|
||||
struct MyResponse {
|
||||
message: String
|
||||
}
|
||||
```
|
||||
*/
|
||||
pub trait OpenapiType {
|
||||
fn schema() -> OpenapiSchema;
|
||||
}
|
||||
|
||||
impl OpenapiType for () {
|
||||
fn schema() -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
|
||||
additional_properties: Some(AdditionalProperties::Any(false)),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for bool {
|
||||
fn schema() -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Boolean {}))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! int_types {
|
||||
($($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default())))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(unsigned $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
minimum: Some(0),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(gtzero $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
minimum: Some(1),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(bits = $bits:expr, $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(unsigned bits = $bits:expr, $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
|
||||
minimum: Some(0),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(gtzero bits = $bits:expr, $($int_ty:ty),*) => {$(
|
||||
impl OpenapiType for $int_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
|
||||
minimum: Some(1),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
int_types!(isize);
|
||||
int_types!(unsigned usize);
|
||||
int_types!(gtzero NonZeroUsize);
|
||||
int_types!(bits = 8, i8);
|
||||
int_types!(unsigned bits = 8, u8);
|
||||
int_types!(gtzero bits = 8, NonZeroU8);
|
||||
int_types!(bits = 16, i16);
|
||||
int_types!(unsigned bits = 16, u16);
|
||||
int_types!(gtzero bits = 16, NonZeroU16);
|
||||
int_types!(bits = 32, i32);
|
||||
int_types!(unsigned bits = 32, u32);
|
||||
int_types!(gtzero bits = 32, NonZeroU32);
|
||||
int_types!(bits = 64, i64);
|
||||
int_types!(unsigned bits = 64, u64);
|
||||
int_types!(gtzero bits = 64, NonZeroU64);
|
||||
int_types!(bits = 128, i128);
|
||||
int_types!(unsigned bits = 128, u128);
|
||||
int_types!(gtzero bits = 128, NonZeroU128);
|
||||
|
||||
macro_rules! num_types {
|
||||
($($num_ty:ty = $num_fmt:ident),*) => {$(
|
||||
impl OpenapiType for $num_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType {
|
||||
format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*}
|
||||
}
|
||||
|
||||
num_types!(f32 = Float, f64 = Double);
|
||||
|
||||
macro_rules! str_types {
|
||||
($($str_ty:ty),*) => {$(
|
||||
impl OpenapiType for $str_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default())))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(format = $format:ident, $($str_ty:ty),*) => {$(
|
||||
impl OpenapiType for $str_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
use openapiv3::StringFormat;
|
||||
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::$format),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
|
||||
(format_str = $format:expr, $($str_ty:ty),*) => {$(
|
||||
impl OpenapiType for $str_ty
|
||||
{
|
||||
fn schema() -> OpenapiSchema
|
||||
{
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Unknown($format.to_string()),
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
str_types!(String, &str);
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate);
|
||||
#[cfg(feature = "chrono")]
|
||||
str_types!(
|
||||
format = DateTime,
|
||||
DateTime<FixedOffset>,
|
||||
DateTime<Local>,
|
||||
DateTime<Utc>,
|
||||
NaiveDateTime
|
||||
);
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
str_types!(format_str = "uuid", Uuid);
|
||||
|
||||
impl<T: OpenapiType> OpenapiType for Option<T> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
let schema = match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, schema);
|
||||
SchemaKind::AllOf { all_of: vec![reference] }
|
||||
},
|
||||
None => schema.schema
|
||||
};
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: true,
|
||||
name: None,
|
||||
schema,
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: OpenapiType> OpenapiType for Vec<T> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
|
||||
let items = match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, schema);
|
||||
reference
|
||||
},
|
||||
None => Item(Box::new(schema.into_schema()))
|
||||
};
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: false,
|
||||
name: None,
|
||||
schema: SchemaKind::Type(Type::Array(ArrayType {
|
||||
items,
|
||||
min_items: None,
|
||||
max_items: None,
|
||||
unique_items: false
|
||||
})),
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: OpenapiType> OpenapiType for BTreeSet<T> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
<Vec<T> as OpenapiType>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: OpenapiType, S: BuildHasher> OpenapiType for HashSet<T, S> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
<Vec<T> as OpenapiType>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, T: OpenapiType, S: BuildHasher> OpenapiType for HashMap<K, T, S> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
let schema = T::schema();
|
||||
let mut dependencies = schema.dependencies.clone();
|
||||
|
||||
let items = Box::new(match schema.name.clone() {
|
||||
Some(name) => {
|
||||
let reference = Reference {
|
||||
reference: format!("#/components/schemas/{}", name)
|
||||
};
|
||||
dependencies.insert(name, schema);
|
||||
reference
|
||||
},
|
||||
None => Item(schema.into_schema())
|
||||
});
|
||||
|
||||
OpenapiSchema {
|
||||
nullable: false,
|
||||
name: None,
|
||||
schema: SchemaKind::Type(Type::Object(ObjectType {
|
||||
additional_properties: Some(AdditionalProperties::Schema(items)),
|
||||
..Default::default()
|
||||
})),
|
||||
dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenapiType for serde_json::Value {
|
||||
fn schema() -> OpenapiSchema {
|
||||
OpenapiSchema {
|
||||
nullable: true,
|
||||
name: None,
|
||||
schema: SchemaKind::Any(Default::default()),
|
||||
dependencies: Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json::Value;
|
||||
|
||||
type Unit = ();
|
||||
|
||||
macro_rules! assert_schema {
|
||||
($ty:ident $(<$($generic:ident),+>)* => $json:expr) => {
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<test_schema_ $ty:lower $($(_ $generic:lower)+)*>]()
|
||||
{
|
||||
let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema();
|
||||
let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty)));
|
||||
assert_eq!(schema_json, $json);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#);
|
||||
assert_schema!(bool => r#"{"type":"boolean"}"#);
|
||||
|
||||
assert_schema!(isize => r#"{"type":"integer"}"#);
|
||||
assert_schema!(usize => r#"{"type":"integer","minimum":0}"#);
|
||||
assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#);
|
||||
assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#);
|
||||
assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#);
|
||||
assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#);
|
||||
assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#);
|
||||
assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#);
|
||||
assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#);
|
||||
assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#);
|
||||
assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#);
|
||||
assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#);
|
||||
|
||||
assert_schema!(NonZeroUsize => r#"{"type":"integer","minimum":1}"#);
|
||||
assert_schema!(NonZeroU8 => r#"{"type":"integer","format":"int8","minimum":1}"#);
|
||||
assert_schema!(NonZeroU16 => r#"{"type":"integer","format":"int16","minimum":1}"#);
|
||||
assert_schema!(NonZeroU32 => r#"{"type":"integer","format":"int32","minimum":1}"#);
|
||||
assert_schema!(NonZeroU64 => r#"{"type":"integer","format":"int64","minimum":1}"#);
|
||||
assert_schema!(NonZeroU128 => r#"{"type":"integer","format":"int128","minimum":1}"#);
|
||||
|
||||
assert_schema!(f32 => r#"{"type":"number","format":"float"}"#);
|
||||
assert_schema!(f64 => r#"{"type":"number","format":"double"}"#);
|
||||
|
||||
assert_schema!(String => r#"{"type":"string"}"#);
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#);
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
mod chrono {
|
||||
use super::*;
|
||||
|
||||
assert_schema!(Date<FixedOffset> => r#"{"type":"string","format":"date"}"#);
|
||||
assert_schema!(Date<Local> => r#"{"type":"string","format":"date"}"#);
|
||||
assert_schema!(Date<Utc> => r#"{"type":"string","format":"date"}"#);
|
||||
assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#);
|
||||
assert_schema!(DateTime<FixedOffset> => r#"{"type":"string","format":"date-time"}"#);
|
||||
assert_schema!(DateTime<Local> => r#"{"type":"string","format":"date-time"}"#);
|
||||
assert_schema!(DateTime<Utc> => r#"{"type":"string","format":"date-time"}"#);
|
||||
assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#);
|
||||
}
|
||||
|
||||
assert_schema!(Option<String> => r#"{"nullable":true,"type":"string"}"#);
|
||||
assert_schema!(Vec<String> => r#"{"type":"array","items":{"type":"string"}}"#);
|
||||
assert_schema!(BTreeSet<String> => r#"{"type":"array","items":{"type":"string"}}"#);
|
||||
assert_schema!(HashSet<String> => r#"{"type":"array","items":{"type":"string"}}"#);
|
||||
assert_schema!(HashMap<String, String> => r#"{"type":"object","additionalProperties":{"type":"string"}}"#);
|
||||
assert_schema!(Value => r#"{"nullable":true}"#);
|
||||
}
|
|
@ -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>>;
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use gotham::hyper::{Body, StatusCode};
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
|
||||
/// A response, used to create the final gotham response from.
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub status: StatusCode,
|
||||
pub body: Body,
|
||||
pub mime: Option<Mime>
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Create a new [Response] from raw data.
|
||||
pub fn new<B: Into<Body>>(status: StatusCode, body: B, mime: Option<Mime>) -> Self {
|
||||
Self {
|
||||
status,
|
||||
body: body.into(),
|
||||
mime
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [Response] with mime type json from already serialized data.
|
||||
pub fn json<B: Into<Body>>(status: StatusCode, body: B) -> Self {
|
||||
Self {
|
||||
status,
|
||||
body: body.into(),
|
||||
mime: Some(APPLICATION_JSON)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a _204 No Content_ [Response].
|
||||
pub fn no_content() -> Self {
|
||||
Self {
|
||||
status: StatusCode::NO_CONTENT,
|
||||
body: Body::empty(),
|
||||
mime: None
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty _403 Forbidden_ [Response].
|
||||
pub fn forbidden() -> Self {
|
||||
Self {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: Body::empty(),
|
||||
mime: None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn full_body(mut self) -> Result<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::Error> {
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::body::to_bytes;
|
||||
|
||||
let bytes: &[u8] = &block_on(to_bytes(&mut self.body))?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
}
|
|
@ -12,9 +12,9 @@ pub enum AuthError {
|
|||
}
|
||||
|
||||
/**
|
||||
This return type can be used to map another [ResourceResult](crate::ResourceResult) that can
|
||||
only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response
|
||||
will be issued.
|
||||
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||
response will be issued.
|
||||
|
||||
Use can look something like this (assuming the `auth` feature is enabled):
|
||||
|
||||
|
@ -33,7 +33,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
|
|||
# #[derive(Clone, Deserialize)]
|
||||
# struct MyAuthData { exp : u64 }
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
#[read_all]
|
||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
||||
let auth_data = match auth {
|
||||
AuthStatus::Authenticated(data) => data,
|
||||
|
@ -69,10 +69,16 @@ impl<E> From<AuthError> for AuthErrorOrOther<E> {
|
|||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use gotham::handler::HandlerError;
|
||||
pub trait Sealed {}
|
||||
impl<E: Into<HandlerError>> Sealed for E {}
|
||||
}
|
||||
|
||||
impl<E, F> From<F> for AuthErrorOrOther<E>
|
||||
where
|
||||
// TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20
|
||||
F: std::error::Error + Into<E>
|
||||
F: private::Sealed + Into<E>
|
||||
{
|
||||
fn from(err: F) -> Self {
|
||||
Self::Other(err.into())
|
||||
|
@ -80,9 +86,9 @@ where
|
|||
}
|
||||
|
||||
/**
|
||||
This return type can be used to map another [ResourceResult](crate::ResourceResult) that can
|
||||
only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ response
|
||||
will be issued.
|
||||
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||
response will be issued.
|
||||
|
||||
Use can look something like this (assuming the `auth` feature is enabled):
|
||||
|
||||
|
@ -102,7 +108,7 @@ Use can look something like this (assuming the `auth` feature is enabled):
|
|||
# #[derive(Clone, Deserialize)]
|
||||
# struct MyAuthData { exp : u64 }
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
#[read_all]
|
||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
||||
let auth_data = match auth {
|
||||
AuthStatus::Authenticated(data) => data,
|
282
src/response/mod.rs
Normal file
282
src/response/mod.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
use futures_util::future::{self, BoxFuture, FutureExt};
|
||||
use gotham::{
|
||||
handler::HandlerError,
|
||||
hyper::{
|
||||
header::{HeaderMap, HeaderName, HeaderValue},
|
||||
Body, StatusCode
|
||||
}
|
||||
};
|
||||
use mime::{Mime, APPLICATION_JSON, STAR_STAR};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiSchema;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
mod auth_result;
|
||||
pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess};
|
||||
|
||||
mod no_content;
|
||||
pub use no_content::NoContent;
|
||||
|
||||
mod raw;
|
||||
pub use raw::Raw;
|
||||
|
||||
mod redirect;
|
||||
pub use redirect::Redirect;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod result;
|
||||
pub use result::IntoResponseError;
|
||||
|
||||
mod success;
|
||||
pub use success::Success;
|
||||
|
||||
pub(crate) trait OrAllTypes {
|
||||
fn or_all_types(self) -> Vec<Mime>;
|
||||
}
|
||||
|
||||
impl OrAllTypes for Option<Vec<Mime>> {
|
||||
fn or_all_types(self) -> Vec<Mime> {
|
||||
self.unwrap_or_else(|| vec![STAR_STAR])
|
||||
}
|
||||
}
|
||||
|
||||
/// A response, used to create the final gotham response from.
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub(crate) status: StatusCode,
|
||||
pub(crate) body: Body,
|
||||
pub(crate) mime: Option<Mime>,
|
||||
pub(crate) headers: HeaderMap
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Create a new [Response] from raw data.
|
||||
#[must_use = "Creating a response is pointless if you don't use it"]
|
||||
pub fn new<B: Into<Body>>(status: StatusCode, body: B, mime: Option<Mime>) -> Self {
|
||||
Self {
|
||||
status,
|
||||
body: body.into(),
|
||||
mime,
|
||||
headers: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [Response] with mime type json from already serialized data.
|
||||
#[must_use = "Creating a response is pointless if you don't use it"]
|
||||
pub fn json<B: Into<Body>>(status: StatusCode, body: B) -> Self {
|
||||
Self {
|
||||
status,
|
||||
body: body.into(),
|
||||
mime: Some(APPLICATION_JSON),
|
||||
headers: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a _204 No Content_ [Response].
|
||||
#[must_use = "Creating a response is pointless if you don't use it"]
|
||||
pub fn no_content() -> Self {
|
||||
Self {
|
||||
status: StatusCode::NO_CONTENT,
|
||||
body: Body::empty(),
|
||||
mime: None,
|
||||
headers: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty _403 Forbidden_ [Response].
|
||||
#[must_use = "Creating a response is pointless if you don't use it"]
|
||||
pub fn forbidden() -> Self {
|
||||
Self {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
body: Body::empty(),
|
||||
mime: None,
|
||||
headers: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the status code of this [Response].
|
||||
pub fn status(&self) -> StatusCode {
|
||||
self.status
|
||||
}
|
||||
|
||||
/// Return the mime type of this [Response].
|
||||
pub fn mime(&self) -> Option<&Mime> {
|
||||
self.mime.as_ref()
|
||||
}
|
||||
|
||||
/// Add an HTTP header to the [Response].
|
||||
pub fn header(&mut self, name: HeaderName, value: HeaderValue) {
|
||||
self.headers.insert(name, value);
|
||||
}
|
||||
|
||||
pub(crate) fn with_headers(mut self, headers: HeaderMap) -> Self {
|
||||
self.headers = headers;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn full_body(mut self) -> Result<Vec<u8>, <Body as gotham::hyper::body::HttpBody>::Error> {
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::body::to_bytes;
|
||||
|
||||
let bytes: &[u8] = &block_on(to_bytes(&mut self.body))?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Response {
|
||||
type Err = Infallible;
|
||||
|
||||
fn into_response(self) -> BoxFuture<'static, Result<Response, Self::Err>> {
|
||||
future::ok(self).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait needs to be implemented by every type returned from an endpoint to
|
||||
/// to provide the response.
|
||||
pub trait IntoResponse {
|
||||
type Err: Into<HandlerError> + Send + Sync + 'static;
|
||||
|
||||
/// Turn this into a response that can be returned to the browser. This api will likely
|
||||
/// change in the future.
|
||||
fn into_response(self) -> BoxFuture<'static, Result<Response, Self::Err>>;
|
||||
|
||||
/// Return a list of supported mime types.
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional details for [IntoResponse] to be used with an OpenAPI-aware router.
|
||||
#[cfg(feature = "openapi")]
|
||||
pub trait ResponseSchema {
|
||||
fn schema() -> OpenapiSchema;
|
||||
|
||||
fn default_status() -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
/// A trait provided to convert a resource's result to json, and provide an OpenAPI schema to the
|
||||
/// router. This trait is implemented for all types that implement [IntoResponse] and
|
||||
/// [ResponseSchema].
|
||||
#[cfg(feature = "openapi")]
|
||||
pub trait IntoResponseWithSchema: IntoResponse + ResponseSchema + private::Sealed {}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<R: IntoResponse + ResponseSchema> private::Sealed for R {}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<R: IntoResponse + ResponseSchema> IntoResponseWithSchema for R {}
|
||||
|
||||
/// The default json returned on an 500 Internal Server Error.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ResourceError {
|
||||
error: bool,
|
||||
message: String
|
||||
}
|
||||
|
||||
impl<T: ToString> From<T> for ResourceError {
|
||||
fn from(message: T) -> Self {
|
||||
Self {
|
||||
error: true,
|
||||
message: message.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "errorlog")]
|
||||
fn errorlog<E: Display>(e: E) {
|
||||
error!("The handler encountered an error: {}", e);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "errorlog"))]
|
||||
fn errorlog<E>(_e: E) {}
|
||||
|
||||
fn handle_error<E>(e: E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
|
||||
where
|
||||
E: Display + IntoResponseError
|
||||
{
|
||||
let msg = e.to_string();
|
||||
let res = e.into_response_error();
|
||||
match &res {
|
||||
Ok(res) if res.status.is_server_error() => errorlog(msg),
|
||||
Err(err) => {
|
||||
errorlog(msg);
|
||||
errorlog(&err);
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
future::ready(res).boxed()
|
||||
}
|
||||
|
||||
impl<Res> IntoResponse for Pin<Box<dyn Future<Output = Res> + Send>>
|
||||
where
|
||||
Res: IntoResponse + 'static
|
||||
{
|
||||
type Err = Res::Err;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
|
||||
self.then(IntoResponse::into_response).boxed()
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
Res::accepted_types()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<Res> ResponseSchema for Pin<Box<dyn Future<Output = Res> + Send>>
|
||||
where
|
||||
Res: ResponseSchema
|
||||
{
|
||||
fn schema() -> OpenapiSchema {
|
||||
Res::schema()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> StatusCode {
|
||||
Res::default_status()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Msg {
|
||||
msg: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
#[error("An Error")]
|
||||
struct MsgError;
|
||||
|
||||
#[test]
|
||||
fn result_from_future() {
|
||||
let nc = NoContent::default();
|
||||
let res = block_on(nc.into_response()).unwrap();
|
||||
|
||||
let fut_nc = async move { NoContent::default() }.boxed();
|
||||
let fut_res = block_on(fut_nc.into_response()).unwrap();
|
||||
|
||||
assert_eq!(res.status, fut_res.status);
|
||||
assert_eq!(res.mime, fut_res.mime);
|
||||
assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap());
|
||||
}
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
use super::{handle_error, ResourceResult};
|
||||
use crate::{IntoResponseError, Response};
|
||||
use super::{handle_error, IntoResponse};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::{OpenapiSchema, OpenapiType};
|
||||
use crate::ResponseSchema;
|
||||
use crate::{IntoResponseError, Response};
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::hyper::header::{HeaderMap, HeaderValue, IntoHeaderName};
|
||||
#[cfg(feature = "openapi")]
|
||||
use gotham::hyper::StatusCode;
|
||||
use mime::Mime;
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::{OpenapiSchema, OpenapiType};
|
||||
use std::{fmt::Display, future::Future, pin::Pin};
|
||||
|
||||
/**
|
||||
|
@ -21,49 +26,64 @@ the function attributes:
|
|||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(_state: &mut State) {
|
||||
#[read_all]
|
||||
fn read_all() {
|
||||
// do something
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct NoContent;
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NoContent {
|
||||
headers: HeaderMap
|
||||
}
|
||||
|
||||
impl From<()> for NoContent {
|
||||
fn from(_: ()) -> Self {
|
||||
Self {}
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceResult for NoContent {
|
||||
impl NoContent {
|
||||
/// Set a custom HTTP header. If a header with this name was set before, its value is being updated.
|
||||
pub fn header<K: IntoHeaderName>(&mut self, name: K, value: HeaderValue) {
|
||||
self.headers.insert(name, value);
|
||||
}
|
||||
|
||||
/// Allow manipulating HTTP headers.
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for NoContent {
|
||||
// TODO this shouldn't be a serde_json::Error
|
||||
type Err = serde_json::Error; // just for easier handling of `Result<NoContent, E>`
|
||||
|
||||
/// This will always be a _204 No Content_ together with an empty string.
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
|
||||
future::ok(Response::no_content()).boxed()
|
||||
future::ok(Response::no_content().with_headers(self.headers)).boxed()
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
Some(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the schema of the `()` type.
|
||||
#[cfg(feature = "openapi")]
|
||||
impl ResponseSchema for NoContent {
|
||||
/// Returns the schema of the `()` type.
|
||||
fn schema() -> OpenapiSchema {
|
||||
<()>::schema()
|
||||
}
|
||||
|
||||
/// This will always be a _204 No Content_
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode {
|
||||
crate::StatusCode::NO_CONTENT
|
||||
fn default_status() -> StatusCode {
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ResourceResult for Result<NoContent, E>
|
||||
impl<E> IntoResponse for Result<NoContent, E>
|
||||
where
|
||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||
{
|
||||
|
@ -79,14 +99,19 @@ where
|
|||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
NoContent::accepted_types()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema {
|
||||
<NoContent as ResourceResult>::schema()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode {
|
||||
impl<E> ResponseSchema for Result<NoContent, E>
|
||||
where
|
||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||
{
|
||||
fn schema() -> OpenapiSchema {
|
||||
<NoContent as ResponseSchema>::schema()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> StatusCode {
|
||||
NoContent::default_status()
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +120,7 @@ where
|
|||
mod test {
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::StatusCode;
|
||||
use gotham::hyper::{header::ACCESS_CONTROL_ALLOW_ORIGIN, StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
|
@ -109,6 +134,9 @@ mod test {
|
|||
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
||||
assert_eq!(res.mime, None);
|
||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
assert_eq!(NoContent::default_status(), StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -119,4 +147,13 @@ mod test {
|
|||
assert_eq!(res.mime, None);
|
||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_content_custom_headers() {
|
||||
let mut no_content = NoContent::default();
|
||||
no_content.header(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
||||
let cors = res.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
assert_eq!(cors.map(|value| value.to_str().unwrap()), Some("*"));
|
||||
}
|
||||
}
|
|
@ -1,10 +1,16 @@
|
|||
use super::{handle_error, IntoResponseError, ResourceResult};
|
||||
use super::{handle_error, IntoResponse, IntoResponseError};
|
||||
use crate::{FromBody, RequestBody, ResourceType, Response};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode};
|
||||
use crate::{IntoResponseWithSchema, ResponseSchema};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::{OpenapiSchema, OpenapiType};
|
||||
|
||||
use futures_core::future::Future;
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::hyper::body::{Body, Bytes};
|
||||
use gotham::hyper::{
|
||||
body::{Body, Bytes},
|
||||
StatusCode
|
||||
};
|
||||
use mime::Mime;
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
||||
|
@ -25,7 +31,7 @@ example that simply returns its body:
|
|||
#[resource(create)]
|
||||
struct ImageResource;
|
||||
|
||||
#[create(ImageResource)]
|
||||
#[create]
|
||||
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||
body
|
||||
}
|
||||
|
@ -85,17 +91,8 @@ impl<T: for<'a> From<&'a [u8]>> FromBody for Raw<T> {
|
|||
|
||||
impl<T> RequestBody for Raw<T> where Raw<T>: FromBody + ResourceType {}
|
||||
|
||||
impl<T: Into<Body>> ResourceResult for Raw<T>
|
||||
where
|
||||
Self: Send
|
||||
{
|
||||
type Err = SerdeJsonError; // just for easier handling of `Result<Raw<T>, E>`
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>> {
|
||||
future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<T> OpenapiType for Raw<T> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||
|
@ -104,10 +101,31 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<T, E> ResourceResult for Result<Raw<T>, E>
|
||||
impl<T: Into<Body>> IntoResponse for Raw<T>
|
||||
where
|
||||
Raw<T>: ResourceResult,
|
||||
E: Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
|
||||
Self: Send
|
||||
{
|
||||
type Err = SerdeJsonError; // just for easier handling of `Result<Raw<T>, E>`
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>> {
|
||||
future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime))).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<T: Into<Body>> ResponseSchema for Raw<T>
|
||||
where
|
||||
Self: Send
|
||||
{
|
||||
fn schema() -> OpenapiSchema {
|
||||
<Self as OpenapiType>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> IntoResponse for Result<Raw<T>, E>
|
||||
where
|
||||
Raw<T>: IntoResponse,
|
||||
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
|
||||
{
|
||||
type Err = E::Err;
|
||||
|
||||
|
@ -117,10 +135,16 @@ where
|
|||
Err(e) => handle_error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<T, E> ResponseSchema for Result<Raw<T>, E>
|
||||
where
|
||||
Raw<T>: IntoResponseWithSchema,
|
||||
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
|
||||
{
|
||||
fn schema() -> OpenapiSchema {
|
||||
<Raw<T> as ResourceResult>::schema()
|
||||
<Raw<T> as ResponseSchema>::schema()
|
||||
}
|
||||
}
|
||||
|
151
src/response/redirect.rs
Normal file
151
src/response/redirect.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use super::{handle_error, IntoResponse};
|
||||
use crate::{IntoResponseError, Response};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::{NoContent, ResponseSchema};
|
||||
use futures_util::future::{BoxFuture, FutureExt, TryFutureExt};
|
||||
use gotham::hyper::{
|
||||
header::{InvalidHeaderValue, LOCATION},
|
||||
Body, StatusCode
|
||||
};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiSchema;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{Debug, Display}
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/**
|
||||
This is the return type of a resource that only returns a redirect. It will result
|
||||
in a _303 See Other_ answer, meaning the redirect will always result in a GET request
|
||||
on the target.
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[read_all]
|
||||
fn read_all() -> Redirect {
|
||||
Redirect {
|
||||
to: "http://localhost:8080/cool/new/location".to_owned()
|
||||
}
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Redirect {
|
||||
pub to: String
|
||||
}
|
||||
|
||||
impl IntoResponse for Redirect {
|
||||
type Err = InvalidHeaderValue;
|
||||
|
||||
fn into_response(self) -> BoxFuture<'static, Result<Response, Self::Err>> {
|
||||
async move {
|
||||
let mut res = Response::new(StatusCode::SEE_OTHER, Body::empty(), None);
|
||||
res.header(LOCATION, self.to.parse()?);
|
||||
Ok(res)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl ResponseSchema for Redirect {
|
||||
fn default_status() -> StatusCode {
|
||||
StatusCode::SEE_OTHER
|
||||
}
|
||||
|
||||
fn schema() -> OpenapiSchema {
|
||||
<NoContent as ResponseSchema>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
// private type due to parent mod
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RedirectError<E: StdError + 'static> {
|
||||
#[error("{0}")]
|
||||
InvalidLocation(#[from] InvalidHeaderValue),
|
||||
#[error("{0}")]
|
||||
Other(#[source] E)
|
||||
}
|
||||
|
||||
#[allow(ambiguous_associated_items)] // an enum variant is not a type. never.
|
||||
impl<E> IntoResponse for Result<Redirect, E>
|
||||
where
|
||||
E: Display + IntoResponseError,
|
||||
<E as IntoResponseError>::Err: StdError + Sync
|
||||
{
|
||||
type Err = RedirectError<<E as IntoResponseError>::Err>;
|
||||
|
||||
fn into_response(self) -> BoxFuture<'static, Result<Response, Self::Err>> {
|
||||
match self {
|
||||
Ok(nc) => nc.into_response().map_err(Into::into).boxed(),
|
||||
Err(e) => handle_error(e).map_err(|e| RedirectError::Other(e)).boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<E> ResponseSchema for Result<Redirect, E>
|
||||
where
|
||||
E: Display + IntoResponseError,
|
||||
<E as IntoResponseError>::Err: StdError + Sync
|
||||
{
|
||||
fn default_status() -> StatusCode {
|
||||
Redirect::default_status()
|
||||
}
|
||||
|
||||
fn schema() -> OpenapiSchema {
|
||||
<Redirect as ResponseSchema>::schema()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
#[error("An Error")]
|
||||
struct MsgError;
|
||||
|
||||
#[test]
|
||||
fn rediect_has_redirect_response() {
|
||||
let redir = Redirect {
|
||||
to: "http://localhost/foo".to_owned()
|
||||
};
|
||||
let res = block_on(redir.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::SEE_OTHER);
|
||||
assert_eq!(res.mime, None);
|
||||
assert_eq!(
|
||||
res.headers.get(LOCATION).map(|hdr| hdr.to_str().unwrap()),
|
||||
Some("http://localhost/foo")
|
||||
);
|
||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_result() {
|
||||
let redir: Result<Redirect, MsgError> = Ok(Redirect {
|
||||
to: "http://localhost/foo".to_owned()
|
||||
});
|
||||
let res = block_on(redir.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::SEE_OTHER);
|
||||
assert_eq!(res.mime, None);
|
||||
assert_eq!(
|
||||
res.headers.get(LOCATION).map(|hdr| hdr.to_str().unwrap()),
|
||||
Some("http://localhost/foo")
|
||||
);
|
||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
use super::{handle_error, into_response_helper, ResourceResult};
|
||||
use super::{handle_error, IntoResponse, ResourceError};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use crate::{result::ResourceError, Response, ResponseBody, StatusCode};
|
||||
use crate::ResponseSchema;
|
||||
use crate::{Response, ResponseBody, Success};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiSchema;
|
||||
|
||||
use futures_core::future::Future;
|
||||
use gotham::hyper::StatusCode;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{error::Error, fmt::Display, pin::Pin};
|
||||
|
||||
pub trait IntoResponseError {
|
||||
type Err: Error + Send + 'static;
|
||||
type Err: Display + Send + 'static;
|
||||
|
||||
fn into_response_error(self) -> Result<Response, Self::Err>;
|
||||
}
|
||||
|
@ -24,7 +28,7 @@ impl<E: Error> IntoResponseError for E {
|
|||
}
|
||||
}
|
||||
|
||||
impl<R, E> ResourceResult for Result<R, E>
|
||||
impl<R, E> IntoResponse for Result<R, E>
|
||||
where
|
||||
R: ResponseBody,
|
||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||
|
@ -33,7 +37,7 @@ where
|
|||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
|
||||
match self {
|
||||
Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))),
|
||||
Ok(r) => Success::from(r).into_response(),
|
||||
Err(e) => handle_error(e)
|
||||
}
|
||||
}
|
||||
|
@ -41,8 +45,14 @@ where
|
|||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<R, E> ResponseSchema for Result<R, E>
|
||||
where
|
||||
R: ResponseBody,
|
||||
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||
{
|
||||
fn schema() -> OpenapiSchema {
|
||||
R::schema()
|
||||
}
|
||||
|
@ -51,12 +61,12 @@ where
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::result::OrAllTypes;
|
||||
use crate::response::OrAllTypes;
|
||||
use futures_executor::block_on;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Msg {
|
||||
msg: String
|
||||
}
|
||||
|
@ -71,7 +81,7 @@ mod test {
|
|||
let res = block_on(ok.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::OK);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes());
|
||||
assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#);
|
||||
}
|
||||
|
||||
#[test]
|
130
src/response/success.rs
Normal file
130
src/response/success.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use super::IntoResponse;
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::ResponseSchema;
|
||||
use crate::{Response, ResponseBody};
|
||||
use futures_util::future::{self, FutureExt};
|
||||
use gotham::hyper::{
|
||||
header::{HeaderMap, HeaderValue, IntoHeaderName},
|
||||
StatusCode
|
||||
};
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiSchema;
|
||||
use std::{fmt::Debug, future::Future, pin::Pin};
|
||||
|
||||
/**
|
||||
This can be returned from a resource when there is no cause of an error.
|
||||
|
||||
Usage example:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[derive(Deserialize, Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct MyResponse {
|
||||
message: &'static str
|
||||
}
|
||||
|
||||
#[read_all]
|
||||
fn read_all() -> Success<MyResponse> {
|
||||
let res = MyResponse { message: "I'm always happy" };
|
||||
res.into()
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Success<T> {
|
||||
value: T,
|
||||
headers: HeaderMap
|
||||
}
|
||||
|
||||
impl<T> From<T> for Success<T> {
|
||||
fn from(t: T) -> Self {
|
||||
Self {
|
||||
value: t,
|
||||
headers: HeaderMap::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Success<T> {
|
||||
/// Set a custom HTTP header. If a header with this name was set before, its value is being updated.
|
||||
pub fn header<K: IntoHeaderName>(&mut self, name: K, value: HeaderValue) {
|
||||
self.headers.insert(name, value);
|
||||
}
|
||||
|
||||
/// Allow manipulating HTTP headers.
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResponseBody> IntoResponse for Success<T> {
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
|
||||
let res =
|
||||
serde_json::to_string(&self.value).map(|body| Response::json(StatusCode::OK, body).with_headers(self.headers));
|
||||
future::ready(res).boxed()
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<T: ResponseBody> ResponseSchema for Success<T> {
|
||||
fn schema() -> OpenapiSchema {
|
||||
T::schema()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::response::OrAllTypes;
|
||||
use futures_executor::block_on;
|
||||
use gotham::hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||
struct Msg {
|
||||
msg: String
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_always_successfull() {
|
||||
let success: Success<Msg> = Msg::default().into();
|
||||
let res = block_on(success.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::OK);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.full_body().unwrap(), br#"{"msg":""}"#);
|
||||
#[cfg(feature = "openapi")]
|
||||
assert_eq!(<Success<Msg>>::default_status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_custom_headers() {
|
||||
let mut success: Success<Msg> = Msg::default().into();
|
||||
success.header(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
|
||||
let res = block_on(success.into_response()).expect("didn't expect error response");
|
||||
let cors = res.headers.get(ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
assert_eq!(cors.map(|value| value.to_str().unwrap()), Some("*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_accepts_json() {
|
||||
assert!(<Success<Msg>>::accepted_types().or_all_types().contains(&APPLICATION_JSON))
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use crate::Response;
|
||||
use futures_util::future::FutureExt;
|
||||
use mime::{Mime, STAR_STAR};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Debug, Display},
|
||||
future::Future,
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
mod auth_result;
|
||||
pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess};
|
||||
|
||||
mod no_content;
|
||||
pub use no_content::NoContent;
|
||||
|
||||
mod raw;
|
||||
pub use raw::Raw;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod result;
|
||||
pub use result::IntoResponseError;
|
||||
|
||||
mod success;
|
||||
pub use success::Success;
|
||||
|
||||
pub(crate) trait OrAllTypes {
|
||||
fn or_all_types(self) -> Vec<Mime>;
|
||||
}
|
||||
|
||||
impl OrAllTypes for Option<Vec<Mime>> {
|
||||
fn or_all_types(self) -> Vec<Mime> {
|
||||
self.unwrap_or_else(|| vec![STAR_STAR])
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait provided to convert a resource's result to json.
|
||||
pub trait ResourceResult {
|
||||
type Err: Error + Send + Sync + 'static;
|
||||
|
||||
/// Turn this into a response that can be returned to the browser. This api will likely
|
||||
/// change in the future.
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>>;
|
||||
|
||||
/// Return a list of supported mime types.
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema;
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode {
|
||||
crate::StatusCode::OK
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
impl<Res: ResourceResult> crate::OpenapiType for Res {
|
||||
fn schema() -> OpenapiSchema {
|
||||
Self::schema()
|
||||
}
|
||||
}
|
||||
|
||||
/// The default json returned on an 500 Internal Server Error.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ResourceError {
|
||||
error: bool,
|
||||
message: String
|
||||
}
|
||||
|
||||
impl<T: ToString> From<T> for ResourceError {
|
||||
fn from(message: T) -> Self {
|
||||
Self {
|
||||
error: true,
|
||||
message: message.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn into_response_helper<Err, F>(create_response: F) -> Pin<Box<dyn Future<Output = Result<Response, Err>> + Send>>
|
||||
where
|
||||
Err: Send + 'static,
|
||||
F: FnOnce() -> Result<Response, Err>
|
||||
{
|
||||
let res = create_response();
|
||||
async move { res }.boxed()
|
||||
}
|
||||
|
||||
#[cfg(feature = "errorlog")]
|
||||
fn errorlog<E: Display>(e: E) {
|
||||
error!("The handler encountered an error: {}", e);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "errorlog"))]
|
||||
fn errorlog<E>(_e: E) {}
|
||||
|
||||
fn handle_error<E>(e: E) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
|
||||
where
|
||||
E: Display + IntoResponseError
|
||||
{
|
||||
into_response_helper(|| {
|
||||
let msg = e.to_string();
|
||||
let res = e.into_response_error();
|
||||
match &res {
|
||||
Ok(res) if res.status.is_server_error() => errorlog(msg),
|
||||
Err(err) => {
|
||||
errorlog(msg);
|
||||
errorlog(&err);
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
res
|
||||
})
|
||||
}
|
||||
|
||||
impl<Res> ResourceResult for Pin<Box<dyn Future<Output = Res> + Send>>
|
||||
where
|
||||
Res: ResourceResult + 'static
|
||||
{
|
||||
type Err = Res::Err;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
|
||||
self.then(|result| result.into_response()).boxed()
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
Res::accepted_types()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema {
|
||||
Res::schema()
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn default_status() -> crate::StatusCode {
|
||||
Res::default_status()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use futures_executor::block_on;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||
struct Msg {
|
||||
msg: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Error)]
|
||||
#[error("An Error")]
|
||||
struct MsgError;
|
||||
|
||||
#[test]
|
||||
fn result_from_future() {
|
||||
let nc = NoContent::default();
|
||||
let res = block_on(nc.into_response()).unwrap();
|
||||
|
||||
let fut_nc = async move { NoContent::default() }.boxed();
|
||||
let fut_res = block_on(fut_nc.into_response()).unwrap();
|
||||
|
||||
assert_eq!(res.status, fut_res.status);
|
||||
assert_eq!(res.mime, fut_res.mime);
|
||||
assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap());
|
||||
}
|
||||
}
|
|
@ -1,139 +0,0 @@
|
|||
use super::{into_response_helper, ResourceResult};
|
||||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiSchema;
|
||||
use crate::{Response, ResponseBody};
|
||||
use gotham::hyper::StatusCode;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin
|
||||
};
|
||||
|
||||
/**
|
||||
This can be returned from a resource when there is no cause of an error. It behaves similar to a
|
||||
smart pointer like box, it that it implements [AsRef], [Deref] and the likes.
|
||||
|
||||
Usage example:
|
||||
|
||||
```
|
||||
# #[macro_use] extern crate gotham_restful_derive;
|
||||
# mod doc_tests_are_broken {
|
||||
# use gotham::state::State;
|
||||
# use gotham_restful::*;
|
||||
# use serde::{Deserialize, Serialize};
|
||||
#
|
||||
# #[derive(Resource)]
|
||||
# #[resource(read_all)]
|
||||
# struct MyResource;
|
||||
#
|
||||
#[derive(Deserialize, Serialize)]
|
||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
struct MyResponse {
|
||||
message: &'static str
|
||||
}
|
||||
|
||||
#[read_all(MyResource)]
|
||||
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
||||
let res = MyResponse { message: "I'm always happy" };
|
||||
res.into()
|
||||
}
|
||||
# }
|
||||
```
|
||||
*/
|
||||
#[derive(Debug)]
|
||||
pub struct Success<T>(T);
|
||||
|
||||
impl<T> AsMut<T> for Success<T> {
|
||||
fn as_mut(&mut self) -> &mut T {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Success<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Success<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Success<T> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Success<T> {
|
||||
fn from(t: T) -> Self {
|
||||
Self(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Clone for Success<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Copy for Success<T> {}
|
||||
|
||||
impl<T: Default> Default for Success<T> {
|
||||
fn default() -> Self {
|
||||
Self(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResponseBody> ResourceResult for Success<T>
|
||||
where
|
||||
Self: Send
|
||||
{
|
||||
type Err = serde_json::Error;
|
||||
|
||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, Self::Err>> + Send>> {
|
||||
into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?)))
|
||||
}
|
||||
|
||||
fn accepted_types() -> Option<Vec<Mime>> {
|
||||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
|
||||
#[cfg(feature = "openapi")]
|
||||
fn schema() -> OpenapiSchema {
|
||||
T::schema()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::result::OrAllTypes;
|
||||
use futures_executor::block_on;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(crate::OpenapiType))]
|
||||
struct Msg {
|
||||
msg: String
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_always_successfull() {
|
||||
let success: Success<Msg> = Msg::default().into();
|
||||
let res = block_on(success.into_response()).expect("didn't expect error response");
|
||||
assert_eq!(res.status, StatusCode::OK);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_accepts_json() {
|
||||
assert!(<Success<Msg>>::accepted_types().or_all_types().contains(&APPLICATION_JSON))
|
||||
}
|
||||
}
|
360
src/routing.rs
360
src/routing.rs
|
@ -3,34 +3,31 @@ use crate::openapi::{
|
|||
builder::{OpenapiBuilder, OpenapiInfo},
|
||||
router::OpenapiRouter
|
||||
};
|
||||
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
|
||||
#[cfg(feature = "cors")]
|
||||
use crate::CorsRoute;
|
||||
use crate::{
|
||||
resource::*,
|
||||
result::{ResourceError, ResourceResult},
|
||||
RequestBody, Response, StatusCode
|
||||
};
|
||||
|
||||
use futures_util::{future, future::FutureExt};
|
||||
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
|
||||
use gotham::{
|
||||
handler::{HandlerError, HandlerFuture},
|
||||
handler::HandlerError,
|
||||
helpers::http::response::{create_empty_response, create_response},
|
||||
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method},
|
||||
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method, StatusCode},
|
||||
pipeline::chain::PipelineHandleChain,
|
||||
router::{
|
||||
builder::*,
|
||||
builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder},
|
||||
non_match::RouteNonMatch,
|
||||
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
|
||||
},
|
||||
state::{FromState, State}
|
||||
};
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
use std::{future::Future, panic::RefUnwindSafe, pin::Pin};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiType;
|
||||
use std::{any::TypeId, panic::RefUnwindSafe};
|
||||
|
||||
/// Allow us to extract an id from a path.
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||
id: ID
|
||||
#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||
pub id: ID
|
||||
}
|
||||
|
||||
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
|
||||
|
@ -45,43 +42,36 @@ pub trait WithOpenapi<D> {
|
|||
|
||||
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
||||
/// any RESTful [Resource] with a path.
|
||||
#[_private_openapi_trait(DrawResourcesWithSchema)]
|
||||
pub trait DrawResources {
|
||||
fn resource<R: Resource>(&mut self, path: &str);
|
||||
#[openapi_bound("R: crate::ResourceWithSchema")]
|
||||
#[non_openapi_bound("R: crate::Resource")]
|
||||
fn resource<R>(&mut self, path: &str);
|
||||
}
|
||||
|
||||
/// This trait allows to draw routes within an resource. Use this only inside the
|
||||
/// [Resource::setup] method.
|
||||
#[_private_openapi_trait(DrawResourceRoutesWithSchema)]
|
||||
pub trait DrawResourceRoutes {
|
||||
fn read_all<Handler: ResourceReadAll>(&mut self);
|
||||
|
||||
fn read<Handler: ResourceRead>(&mut self);
|
||||
|
||||
fn search<Handler: ResourceSearch>(&mut self);
|
||||
|
||||
fn create<Handler: ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static;
|
||||
|
||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static;
|
||||
|
||||
fn change<Handler: ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static;
|
||||
|
||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self);
|
||||
|
||||
fn remove<Handler: ResourceRemove>(&mut self);
|
||||
#[openapi_bound("E: crate::EndpointWithSchema")]
|
||||
#[non_openapi_bound("E: crate::Endpoint")]
|
||||
fn endpoint<E: 'static>(&mut self);
|
||||
}
|
||||
|
||||
fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
|
||||
let mut r = create_empty_response(state, res.status);
|
||||
let headers = r.headers_mut();
|
||||
if let Some(mime) = res.mime {
|
||||
r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap());
|
||||
headers.insert(CONTENT_TYPE, mime.as_ref().parse().unwrap());
|
||||
}
|
||||
let mut last_name = None;
|
||||
for (name, value) in res.headers {
|
||||
if name.is_some() {
|
||||
last_name = name;
|
||||
}
|
||||
// this unwrap is safe: the first item will always be Some
|
||||
let name = last_name.clone().unwrap();
|
||||
headers.insert(name, value);
|
||||
}
|
||||
|
||||
let method = Method::borrow_from(state);
|
||||
|
@ -95,149 +85,51 @@ fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body>
|
|||
r
|
||||
}
|
||||
|
||||
async fn to_handler_future<F, R>(
|
||||
state: State,
|
||||
get_result: F
|
||||
) -> Result<(State, gotham::hyper::Response<Body>), (State, HandlerError)>
|
||||
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError>
|
||||
where
|
||||
F: FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
||||
R: ResourceResult
|
||||
E: Endpoint,
|
||||
<E::Output as IntoResponse>::Err: Into<HandlerError>
|
||||
{
|
||||
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()))
|
||||
}
|
||||
trace!("entering endpoint_handler");
|
||||
let placeholders = E::Placeholders::take_from(state);
|
||||
// workaround for E::Placeholders and E::Param being the same type
|
||||
// when fixed remove `Clone` requirement on endpoint
|
||||
if TypeId::of::<E::Placeholders>() == TypeId::of::<E::Params>() {
|
||||
state.put(placeholders.clone());
|
||||
}
|
||||
let params = E::Params::take_from(state);
|
||||
|
||||
async fn body_to_res<B, F, R>(
|
||||
mut state: State,
|
||||
get_result: F
|
||||
) -> (State, Result<gotham::hyper::Response<Body>, HandlerError>)
|
||||
where
|
||||
B: RequestBody,
|
||||
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
||||
R: ResourceResult
|
||||
{
|
||||
let body = to_bytes(Body::take_from(&mut state)).await;
|
||||
let body = match E::needs_body() {
|
||||
true => {
|
||||
let body = to_bytes(Body::take_from(state)).await?;
|
||||
|
||||
let body = match body {
|
||||
Ok(body) => body,
|
||||
Err(e) => return (state, Err(e.into()))
|
||||
};
|
||||
|
||||
let content_type: Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
|
||||
let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) {
|
||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||
None => {
|
||||
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
return (state, Ok(res));
|
||||
debug!("Missing Content-Type: Returning 415 Response");
|
||||
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||
return Ok(res);
|
||||
}
|
||||
};
|
||||
|
||||
let res = {
|
||||
let body = match B::from_body(body, content_type) {
|
||||
Ok(body) => body,
|
||||
match E::Body::from_body(body, content_type) {
|
||||
Ok(body) => Some(body),
|
||||
Err(e) => {
|
||||
debug!("Invalid Body: Returning 400 Response");
|
||||
let error: ResourceError = e.into();
|
||||
let res = match serde_json::to_string(&error) {
|
||||
Ok(json) => {
|
||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
Ok(res)
|
||||
let json = serde_json::to_string(&error)?;
|
||||
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => Err(e.into())
|
||||
};
|
||||
return (state, res);
|
||||
}
|
||||
};
|
||||
get_result(state, body)
|
||||
false => None
|
||||
};
|
||||
|
||||
let (state, res) = res.await;
|
||||
let res = res.into_response().await;
|
||||
|
||||
let res = match res {
|
||||
Ok(res) => {
|
||||
let r = response_from(res, &state);
|
||||
Ok(r)
|
||||
},
|
||||
Err(e) => Err(e.into())
|
||||
};
|
||||
(state, res)
|
||||
}
|
||||
|
||||
fn handle_with_body<B, F, R>(state: State, get_result: F) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
B: RequestBody + 'static,
|
||||
F: FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
|
||||
R: ResourceResult + Send + 'static
|
||||
{
|
||||
body_to_res(state, get_result)
|
||||
.then(|(state, res)| match res {
|
||||
Ok(ok) => future::ok((state, ok)),
|
||||
Err(err) => future::err((state, err))
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn read_all_handler<Handler: ResourceReadAll>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
to_handler_future(state, |state| Handler::read_all(state)).boxed()
|
||||
}
|
||||
|
||||
fn read_handler<Handler: ResourceRead>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let id = {
|
||||
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
to_handler_future(state, |state| Handler::read(state, id)).boxed()
|
||||
}
|
||||
|
||||
fn search_handler<Handler: ResourceSearch>(mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let query = Handler::Query::take_from(&mut state);
|
||||
to_handler_future(state, |state| Handler::search(state, query)).boxed()
|
||||
}
|
||||
|
||||
fn create_handler<Handler: ResourceCreate>(state: State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
|
||||
}
|
||||
|
||||
fn change_all_handler<Handler: ResourceChangeAll>(state: State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body))
|
||||
}
|
||||
|
||||
fn change_handler<Handler: ResourceChange>(state: State) -> Pin<Box<HandlerFuture>>
|
||||
where
|
||||
Handler::Res: 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let id = {
|
||||
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body))
|
||||
}
|
||||
|
||||
fn remove_all_handler<Handler: ResourceRemoveAll>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
to_handler_future(state, |state| Handler::remove_all(state)).boxed()
|
||||
}
|
||||
|
||||
fn remove_handler<Handler: ResourceRemove>(state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let id = {
|
||||
let path: &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
||||
path.id.clone()
|
||||
};
|
||||
to_handler_future(state, |state| Handler::remove(state, id)).boxed()
|
||||
let out = E::handle(state, placeholders, params, body).await;
|
||||
let res = out.into_response().await.map_err(Into::into)?;
|
||||
debug!("Returning response {:?}", res);
|
||||
Ok(response_from(res, state))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -254,8 +146,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
impl MaybeMatchAcceptHeader {
|
||||
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||
let types = match types {
|
||||
Some(types) if types.is_empty() => None,
|
||||
types => types
|
||||
|
@ -266,6 +158,12 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
Self::new(types)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MaybeMatchContentTypeHeader {
|
||||
matcher: Option<ContentTypeHeaderRouteMatcher>
|
||||
|
@ -280,14 +178,20 @@ impl RouteMatcher for MaybeMatchContentTypeHeader {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
impl MaybeMatchContentTypeHeader {
|
||||
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||
Self {
|
||||
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
||||
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||
Self::new(types)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! implDrawResourceRoutes {
|
||||
($implType:ident) => {
|
||||
#[cfg(feature = "openapi")]
|
||||
|
@ -319,108 +223,30 @@ macro_rules! implDrawResourceRoutes {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters
|
||||
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
||||
where
|
||||
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||
P: RefUnwindSafe + Send + Sync + 'static
|
||||
{
|
||||
fn read_all<Handler: ResourceReadAll>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.get(&self.1)
|
||||
.extend_route_matcher(matcher)
|
||||
.to(|state| read_all_handler::<Handler>(state));
|
||||
}
|
||||
fn endpoint<E: Endpoint + 'static>(&mut self) {
|
||||
let uri = format!("{}/{}", self.1, E::uri());
|
||||
debug!("Registering endpoint for {}", uri);
|
||||
self.0.associate(&uri, |assoc| {
|
||||
assoc
|
||||
.request(vec![E::http_method()])
|
||||
.add_route_matcher(MaybeMatchAcceptHeader::new(E::Output::accepted_types()))
|
||||
.with_path_extractor::<E::Placeholders>()
|
||||
.with_query_string_extractor::<E::Params>()
|
||||
.to_async_borrowing(endpoint_handler::<E>);
|
||||
|
||||
fn read<Handler: ResourceRead>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.get(&format!("{}/:id", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| read_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn search<Handler: ResourceSearch>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.get(&format!("{}/search", self.1))
|
||||
.extend_route_matcher(matcher)
|
||||
.with_query_string_extractor::<Handler::Query>()
|
||||
.to(|state| search_handler::<Handler>(state));
|
||||
}
|
||||
|
||||
fn create<Handler: ResourceCreate>(&mut self)
|
||||
where
|
||||
Handler::Res: Send + 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0
|
||||
.post(&self.1)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.to(|state| create_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&self.1, Method::POST);
|
||||
if E::http_method() != Method::GET {
|
||||
assoc
|
||||
.options()
|
||||
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
|
||||
.to(crate::cors::cors_preflight_handler);
|
||||
}
|
||||
|
||||
fn change_all<Handler: ResourceChangeAll>(&mut self)
|
||||
where
|
||||
Handler::Res: Send + 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
self.0
|
||||
.put(&self.1)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.to(|state| change_all_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&self.1, Method::PUT);
|
||||
}
|
||||
|
||||
fn change<Handler: ResourceChange>(&mut self)
|
||||
where
|
||||
Handler::Res: Send + 'static,
|
||||
Handler::Body: 'static
|
||||
{
|
||||
let accept_matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let content_matcher: MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
||||
let path = format!("{}/:id", self.1);
|
||||
self.0
|
||||
.put(&path)
|
||||
.extend_route_matcher(accept_matcher)
|
||||
.extend_route_matcher(content_matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| change_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&path, Method::PUT);
|
||||
}
|
||||
|
||||
fn remove_all<Handler: ResourceRemoveAll>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
self.0
|
||||
.delete(&self.1)
|
||||
.extend_route_matcher(matcher)
|
||||
.to(|state| remove_all_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&self.1, Method::DELETE);
|
||||
}
|
||||
|
||||
fn remove<Handler: ResourceRemove>(&mut self) {
|
||||
let matcher: MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
||||
let path = format!("{}/:id", self.1);
|
||||
self.0
|
||||
.delete(&path)
|
||||
.extend_route_matcher(matcher)
|
||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
||||
.to(|state| remove_handler::<Handler>(state));
|
||||
#[cfg(feature = "cors")]
|
||||
self.0.cors(&path, Method::POST);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
16
src/types.rs
16
src/types.rs
|
@ -1,10 +1,9 @@
|
|||
#[cfg(feature = "openapi")]
|
||||
use crate::OpenapiType;
|
||||
|
||||
use gotham::hyper::body::Bytes;
|
||||
use mime::{Mime, APPLICATION_JSON};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiType;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{error::Error, panic::RefUnwindSafe};
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(not(feature = "openapi"))]
|
||||
pub trait ResourceType {}
|
||||
|
@ -98,12 +97,3 @@ impl<T: ResourceType + DeserializeOwned> RequestBody for T {
|
|||
Some(vec![APPLICATION_JSON])
|
||||
}
|
||||
}
|
||||
|
||||
/// A type than can be used as a parameter to a resource method. Implemented for every type
|
||||
/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of
|
||||
/// type [OpenapiType].
|
||||
///
|
||||
/// [OpenapiType]: trait.OpenapiType.html
|
||||
pub trait ResourceID: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync {}
|
||||
|
||||
impl<T: ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T {}
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_derive;
|
||||
|
||||
use gotham::{router::builder::*, test::TestServer};
|
||||
use gotham::{
|
||||
hyper::{HeaderMap, Method},
|
||||
router::builder::*,
|
||||
state::State,
|
||||
test::TestServer
|
||||
};
|
||||
use gotham_restful::*;
|
||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiType;
|
||||
use serde::Deserialize;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
mod util {
|
||||
include!("util/mod.rs");
|
||||
|
@ -12,7 +20,7 @@ mod util {
|
|||
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all, read, search, create, change_all, change, remove_all, remove)]
|
||||
#[resource(read_all, read, search, create, change_all, change, remove_all, remove, state_test)]
|
||||
struct FooResource;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -22,7 +30,7 @@ struct FooBody {
|
|||
data: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
#[allow(dead_code)]
|
||||
struct FooSearch {
|
||||
|
@ -30,55 +38,66 @@ struct FooSearch {
|
|||
}
|
||||
|
||||
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
async fn read_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||
#[read(FooResource)]
|
||||
#[read]
|
||||
async fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||
#[search(FooResource)]
|
||||
#[search]
|
||||
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||
#[create(FooResource)]
|
||||
#[create]
|
||||
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||
#[change_all(FooResource)]
|
||||
#[change_all]
|
||||
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||
#[change(FooResource)]
|
||||
#[change]
|
||||
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||
#[remove_all(FooResource)]
|
||||
#[remove_all]
|
||||
async fn remove_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||
#[remove(FooResource)]
|
||||
#[remove]
|
||||
async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM";
|
||||
#[endpoint(method = "Method::GET", uri = "state_test")]
|
||||
async fn state_test(state: &mut State) -> Raw<&'static [u8]> {
|
||||
sleep(Duration::from_nanos(1)).await;
|
||||
state.borrow::<HeaderMap>();
|
||||
sleep(Duration::from_nanos(1)).await;
|
||||
Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_methods() {
|
||||
let _ = pretty_env_logger::try_init_timed();
|
||||
|
||||
let server = TestServer::new(build_simple_router(|router| {
|
||||
router.resource::<FooResource>("foo");
|
||||
}))
|
||||
|
@ -110,4 +129,5 @@ fn async_methods() {
|
|||
);
|
||||
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
|
||||
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
|
||||
test_get_response(&server, "http://localhost/foo/state_test", STATE_TEST_RESPONSE);
|
||||
}
|
||||
|
|
|
@ -5,18 +5,21 @@ use gotham::{
|
|||
router::builder::*,
|
||||
test::{Server, TestRequest, TestServer}
|
||||
};
|
||||
use gotham_restful::{change_all, read_all, CorsConfig, DrawResources, Origin, Raw, Resource};
|
||||
use itertools::Itertools;
|
||||
use gotham_restful::{
|
||||
change_all,
|
||||
cors::{Headers, Origin},
|
||||
read_all, CorsConfig, DrawResources, Raw, Resource
|
||||
};
|
||||
use mime::TEXT_PLAIN;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all, change_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() {}
|
||||
|
||||
#[change_all(FooResource)]
|
||||
#[change_all]
|
||||
fn change_all(_body: Raw<Vec<u8>>) {}
|
||||
|
||||
fn test_server(cfg: CorsConfig) -> TestServer {
|
||||
|
@ -35,7 +38,7 @@ where
|
|||
.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
let headers = res.headers();
|
||||
println!("{}", headers.keys().join(","));
|
||||
println!("{}", headers.keys().map(|name| name.as_str()).collect::<Vec<_>>().join(","));
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
|
@ -65,7 +68,7 @@ fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary:
|
|||
.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
let headers = res.headers();
|
||||
println!("{}", headers.keys().join(","));
|
||||
println!("{}", headers.keys().map(|name| name.as_str()).collect::<Vec<_>>().join(","));
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(ACCESS_CONTROL_ALLOW_METHODS)
|
||||
|
@ -98,12 +101,45 @@ fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary:
|
|||
);
|
||||
}
|
||||
|
||||
fn test_preflight_headers(
|
||||
server: &TestServer,
|
||||
method: &str,
|
||||
request_headers: Option<&str>,
|
||||
allowed_headers: Option<&str>,
|
||||
vary: &str
|
||||
) {
|
||||
let client = server.client();
|
||||
let mut res = client
|
||||
.options("http://example.org/foo")
|
||||
.with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap())
|
||||
.with_header(ORIGIN, "http://example.org".parse().unwrap());
|
||||
if let Some(hdr) = request_headers {
|
||||
res = res.with_header(ACCESS_CONTROL_REQUEST_HEADERS, hdr.parse().unwrap());
|
||||
}
|
||||
let res = res.perform().unwrap();
|
||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||
let headers = res.headers();
|
||||
println!("{}", headers.keys().map(|name| name.as_str()).collect::<Vec<_>>().join(","));
|
||||
if let Some(hdr) = allowed_headers {
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(ACCESS_CONTROL_ALLOW_HEADERS)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.as_deref(),
|
||||
Some(hdr)
|
||||
)
|
||||
} else {
|
||||
assert!(!headers.contains_key(ACCESS_CONTROL_ALLOW_HEADERS));
|
||||
}
|
||||
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_origin_none() {
|
||||
let cfg = Default::default();
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 0);
|
||||
test_preflight(&server, "PUT", None, "access-control-request-method", false, 0);
|
||||
|
||||
test_response(server.client().get("http://example.org/foo"), None, None, false);
|
||||
test_response(
|
||||
|
@ -122,7 +158,7 @@ fn cors_origin_star() {
|
|||
};
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight(&server, "PUT", Some("*"), "Access-Control-Request-Method", false, 0);
|
||||
test_preflight(&server, "PUT", Some("*"), "access-control-request-method", false, 0);
|
||||
|
||||
test_response(server.client().get("http://example.org/foo"), Some("*"), None, false);
|
||||
test_response(
|
||||
|
@ -145,7 +181,7 @@ fn cors_origin_single() {
|
|||
&server,
|
||||
"PUT",
|
||||
Some("https://foo.com"),
|
||||
"Access-Control-Request-Method",
|
||||
"access-control-request-method",
|
||||
false,
|
||||
0
|
||||
);
|
||||
|
@ -176,7 +212,7 @@ fn cors_origin_copy() {
|
|||
&server,
|
||||
"PUT",
|
||||
Some("http://example.org"),
|
||||
"Access-Control-Request-Method,Origin",
|
||||
"access-control-request-method,origin",
|
||||
false,
|
||||
0
|
||||
);
|
||||
|
@ -184,17 +220,68 @@ fn cors_origin_copy() {
|
|||
test_response(
|
||||
server.client().get("http://example.org/foo"),
|
||||
Some("http://example.org"),
|
||||
Some("Origin"),
|
||||
Some("origin"),
|
||||
false
|
||||
);
|
||||
test_response(
|
||||
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||
Some("http://example.org"),
|
||||
Some("Origin"),
|
||||
Some("origin"),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_headers_none() {
|
||||
let cfg = Default::default();
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight_headers(&server, "PUT", None, None, "access-control-request-method");
|
||||
test_preflight_headers(&server, "PUT", Some("Content-Type"), None, "access-control-request-method");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_headers_list() {
|
||||
let cfg = CorsConfig {
|
||||
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||
..Default::default()
|
||||
};
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight_headers(&server, "PUT", None, Some("content-type"), "access-control-request-method");
|
||||
test_preflight_headers(
|
||||
&server,
|
||||
"PUT",
|
||||
Some("content-type"),
|
||||
Some("content-type"),
|
||||
"access-control-request-method"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_headers_copy() {
|
||||
let cfg = CorsConfig {
|
||||
headers: Headers::Copy,
|
||||
..Default::default()
|
||||
};
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight_headers(
|
||||
&server,
|
||||
"PUT",
|
||||
None,
|
||||
None,
|
||||
"access-control-request-method,access-control-request-headers"
|
||||
);
|
||||
test_preflight_headers(
|
||||
&server,
|
||||
"PUT",
|
||||
Some("content-type"),
|
||||
Some("content-type"),
|
||||
"access-control-request-method,access-control-request-headers"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_credentials() {
|
||||
let cfg = CorsConfig {
|
||||
|
@ -204,7 +291,7 @@ fn cors_credentials() {
|
|||
};
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight(&server, "PUT", None, "Access-Control-Request-Method", true, 0);
|
||||
test_preflight(&server, "PUT", None, "access-control-request-method", true, 0);
|
||||
|
||||
test_response(server.client().get("http://example.org/foo"), None, None, true);
|
||||
test_response(
|
||||
|
@ -224,7 +311,7 @@ fn cors_max_age() {
|
|||
};
|
||||
let server = test_server(cfg);
|
||||
|
||||
test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 31536000);
|
||||
test_preflight(&server, "PUT", None, "access-control-request-method", false, 31536000);
|
||||
|
||||
test_response(server.client().get("http://example.org/foo"), None, None, false);
|
||||
test_response(
|
||||
|
|
|
@ -15,7 +15,7 @@ struct Foo {
|
|||
content_type: Mime
|
||||
}
|
||||
|
||||
#[create(FooResource)]
|
||||
#[create]
|
||||
fn create(body: Foo) -> Raw<Vec<u8>> {
|
||||
Raw::new(body.content, body.content_type)
|
||||
}
|
||||
|
|
|
@ -44,6 +44,56 @@
|
|||
},
|
||||
"openapi": "3.0.2",
|
||||
"paths": {
|
||||
"/custom": {
|
||||
"patch": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/custom/read/{from}/with/{id}": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "from",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"style": "simple"
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"style": "simple"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/img/{id}": {
|
||||
"get": {
|
||||
"operationId": "getImage",
|
||||
|
|
|
@ -5,6 +5,7 @@ extern crate gotham_derive;
|
|||
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use gotham::{
|
||||
hyper::Method,
|
||||
pipeline::{new_pipeline, single::single_pipeline},
|
||||
router::builder::*,
|
||||
test::TestServer
|
||||
|
@ -22,23 +23,23 @@ use util::{test_get_response, test_openapi_response};
|
|||
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read, change)]
|
||||
#[resource(get_image, set_image)]
|
||||
struct ImageResource;
|
||||
|
||||
#[derive(FromBody, RequestBody)]
|
||||
#[supported_types(IMAGE_PNG)]
|
||||
struct Image(Vec<u8>);
|
||||
|
||||
#[read(ImageResource, operation_id = "getImage")]
|
||||
#[read(operation_id = "getImage")]
|
||||
fn get_image(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
||||
}
|
||||
|
||||
#[change(ImageResource, operation_id = "setImage")]
|
||||
#[change(operation_id = "setImage")]
|
||||
fn set_image(_id: u64, _image: Image) {}
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read, search)]
|
||||
#[resource(read_secret, search_secret)]
|
||||
struct SecretResource;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
|
@ -67,13 +68,13 @@ struct SecretQuery {
|
|||
minute: Option<u16>
|
||||
}
|
||||
|
||||
#[read(SecretResource)]
|
||||
#[read]
|
||||
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
|
||||
auth.ok()?;
|
||||
Ok(Secret { code: 4.2 })
|
||||
}
|
||||
|
||||
#[search(SecretResource)]
|
||||
#[search]
|
||||
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
|
||||
auth.ok()?;
|
||||
Ok(Secrets {
|
||||
|
@ -81,8 +82,24 @@ fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets>
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(custom_read_with, custom_patch)]
|
||||
struct CustomResource;
|
||||
|
||||
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
|
||||
struct ReadWithPath {
|
||||
from: String,
|
||||
id: u64
|
||||
}
|
||||
|
||||
#[endpoint(method = "Method::GET", uri = "read/:from/with/:id")]
|
||||
fn custom_read_with(_path: ReadWithPath) {}
|
||||
|
||||
#[endpoint(method = "Method::PATCH", uri = "", body = true)]
|
||||
fn custom_patch(_body: String) {}
|
||||
|
||||
#[test]
|
||||
fn openapi_supports_scope() {
|
||||
fn openapi_specification() {
|
||||
let info = OpenapiInfo {
|
||||
title: "This is just a test".to_owned(),
|
||||
version: "1.2.3".to_owned(),
|
||||
|
@ -97,8 +114,9 @@ fn openapi_supports_scope() {
|
|||
let server = TestServer::new(build_router(chain, pipelines, |router| {
|
||||
router.with_openapi(info, |mut router| {
|
||||
router.resource::<ImageResource>("img");
|
||||
router.get_openapi("openapi");
|
||||
router.resource::<SecretResource>("secret");
|
||||
router.resource::<CustomResource>("custom");
|
||||
router.get_openapi("openapi");
|
||||
});
|
||||
}))
|
||||
.unwrap();
|
||||
|
|
|
@ -15,7 +15,7 @@ const RESPONSE: &[u8] = b"This is the only valid response.";
|
|||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ enum Error {
|
|||
InternalServerError(String)
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
mod resource_error {
|
||||
use super::Error;
|
||||
use gotham::hyper::StatusCode;
|
||||
|
@ -20,8 +21,8 @@ mod resource_error {
|
|||
fn io_error() {
|
||||
let err = Error::IoError(std::io::Error::last_os_error());
|
||||
let res = err.into_response_error().unwrap();
|
||||
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(res.mime(), Some(&APPLICATION_JSON));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -30,7 +31,7 @@ mod resource_error {
|
|||
assert_eq!(&format!("{}", err), "Internal Server Error: Brocken");
|
||||
|
||||
let res = err.into_response_error().unwrap();
|
||||
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(res.mime, None); // TODO shouldn't this be a json error message?
|
||||
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(res.mime(), None); // TODO shouldn't this be a json error message?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ extern crate gotham_derive;
|
|||
use gotham::{router::builder::*, test::TestServer};
|
||||
use gotham_restful::*;
|
||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||
#[cfg(feature = "openapi")]
|
||||
use openapi_type::OpenapiType;
|
||||
use serde::Deserialize;
|
||||
|
||||
mod util {
|
||||
|
@ -22,7 +24,7 @@ struct FooBody {
|
|||
data: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
||||
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||
#[allow(dead_code)]
|
||||
struct FooSearch {
|
||||
|
@ -30,55 +32,57 @@ struct FooSearch {
|
|||
}
|
||||
|
||||
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||
#[read_all(FooResource)]
|
||||
#[read_all]
|
||||
fn read_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||
#[read(FooResource)]
|
||||
#[read]
|
||||
fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||
#[search(FooResource)]
|
||||
#[search]
|
||||
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||
#[create(FooResource)]
|
||||
#[create]
|
||||
fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||
#[change_all(FooResource)]
|
||||
#[change_all]
|
||||
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||
#[change(FooResource)]
|
||||
#[change]
|
||||
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||
#[remove_all(FooResource)]
|
||||
#[remove_all]
|
||||
fn remove_all() -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||
#[remove(FooResource)]
|
||||
#[remove]
|
||||
fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_methods() {
|
||||
let _ = pretty_env_logger::try_init_timed();
|
||||
|
||||
let server = TestServer::new(build_simple_router(|router| {
|
||||
router.resource::<FooResource>("foo");
|
||||
}))
|
||||
|
|
|
@ -4,25 +4,7 @@ use trybuild::TestCases;
|
|||
#[ignore]
|
||||
fn trybuild_ui() {
|
||||
let t = TestCases::new();
|
||||
|
||||
// always enabled
|
||||
t.compile_fail("tests/ui/from_body_enum.rs");
|
||||
t.compile_fail("tests/ui/method_async_state.rs");
|
||||
t.compile_fail("tests/ui/method_for_unknown_resource.rs");
|
||||
t.compile_fail("tests/ui/method_no_resource.rs");
|
||||
t.compile_fail("tests/ui/method_self.rs");
|
||||
t.compile_fail("tests/ui/method_too_few_args.rs");
|
||||
t.compile_fail("tests/ui/method_too_many_args.rs");
|
||||
t.compile_fail("tests/ui/method_unsafe.rs");
|
||||
t.compile_fail("tests/ui/resource_unknown_method.rs");
|
||||
|
||||
// require the openapi feature
|
||||
if cfg!(feature = "openapi") {
|
||||
t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_nullable_non_bool.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_rename_non_string.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_tuple_struct.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_union.rs");
|
||||
t.compile_fail("tests/ui/openapi_type_unknown_key.rs");
|
||||
}
|
||||
t.compile_fail("tests/ui/endpoint/*.rs");
|
||||
t.compile_fail("tests/ui/from_body/*.rs");
|
||||
t.compile_fail("tests/ui/resource/*.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::state::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) {}
|
||||
| ^^^^^
|
11
tests/ui/endpoint/custom_method_invalid_expr.rs
Normal file
11
tests/ui/endpoint/custom_method_invalid_expr.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(method = "I like pizza", uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
11
tests/ui/endpoint/custom_method_invalid_expr.stderr
Normal file
11
tests/ui/endpoint/custom_method_invalid_expr.stderr
Normal file
|
@ -0,0 +1,11 @@
|
|||
error: unexpected token
|
||||
--> $DIR/custom_method_invalid_expr.rs:8:21
|
||||
|
|
||||
8 | #[endpoint(method = "I like pizza", uri = "custom_read")]
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/custom_method_invalid_expr.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/custom_method_invalid_type.rs
Normal file
11
tests/ui/endpoint/custom_method_invalid_type.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(method = "String::new()", uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
8
tests/ui/endpoint/custom_method_invalid_type.stderr
Normal file
8
tests/ui/endpoint/custom_method_invalid_type.stderr
Normal file
|
@ -0,0 +1,8 @@
|
|||
error[E0308]: mismatched types
|
||||
--> $DIR/custom_method_invalid_type.rs:8:21
|
||||
|
|
||||
8 | #[endpoint(method = "String::new()", uri = "custom_read")]
|
||||
| --------------------^^^^^^^^^^^^^^^-----------------------
|
||||
| | |
|
||||
| | expected struct `Method`, found struct `std::string::String`
|
||||
| expected `Method` because of return type
|
11
tests/ui/endpoint/custom_method_missing.rs
Normal file
11
tests/ui/endpoint/custom_method_missing.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(uri = "custom_read")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
13
tests/ui/endpoint/custom_method_missing.stderr
Normal file
13
tests/ui/endpoint/custom_method_missing.stderr
Normal file
|
@ -0,0 +1,13 @@
|
|||
error: Missing `method` attribute (e.g. `#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]`)
|
||||
--> $DIR/custom_method_missing.rs:8:1
|
||||
|
|
||||
8 | #[endpoint(uri = "custom_read")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/custom_method_missing.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
11
tests/ui/endpoint/custom_uri_missing.rs
Normal file
11
tests/ui/endpoint/custom_uri_missing.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]
|
||||
async fn read_all() {}
|
||||
|
||||
fn main() {}
|
13
tests/ui/endpoint/custom_uri_missing.stderr
Normal file
13
tests/ui/endpoint/custom_uri_missing.stderr
Normal file
|
@ -0,0 +1,13 @@
|
|||
error: Missing `uri` attribute (e.g. `#[endpoint(uri = "custom_endpoint")]`)
|
||||
--> $DIR/custom_uri_missing.rs:8:1
|
||||
|
|
||||
8 | #[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope
|
||||
--> $DIR/custom_uri_missing.rs:5:12
|
||||
|
|
||||
5 | #[resource(read_all)]
|
||||
| ^^^^^^^^ not found in this scope
|
|
@ -1,14 +1,11 @@
|
|||
#[macro_use] extern crate gotham_restful;
|
||||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(read_all)]
|
||||
struct FooResource;
|
||||
|
||||
#[read_all(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
|
19
tests/ui/endpoint/invalid_body_ty.rs
Normal file
19
tests/ui/endpoint/invalid_body_ty.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
use gotham_restful::gotham::hyper::Method;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(endpoint)]
|
||||
struct FooResource;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FooBody {
|
||||
foo: String
|
||||
}
|
||||
|
||||
#[endpoint(method = "Method::GET", uri = "", body = true)]
|
||||
fn endpoint(_: FooBody) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn main() {}
|
24
tests/ui/endpoint/invalid_body_ty.stderr
Normal file
24
tests/ui/endpoint/invalid_body_ty.stderr
Normal file
|
@ -0,0 +1,24 @@
|
|||
error[E0277]: the trait bound `FooBody: OpenapiType` is not satisfied
|
||||
--> $DIR/invalid_body_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooBody) {
|
||||
| ^^^^^^^ the trait `OpenapiType` is not implemented for `FooBody`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| type Body: RequestBody + Send;
|
||||
| ----------- required by this bound in `gotham_restful::EndpointWithSchema::Body`
|
||||
|
||||
error[E0277]: the trait bound `for<'de> FooBody: serde::de::Deserialize<'de>` is not satisfied
|
||||
--> $DIR/invalid_body_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooBody) {
|
||||
| ^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooBody`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| type Body: RequestBody + Send;
|
||||
| ----------- required by this bound in `gotham_restful::EndpointWithSchema::Body`
|
||||
|
|
||||
= note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `FooBody`
|
||||
= note: required because of the requirements on the impl of `gotham_restful::RequestBody` for `FooBody`
|
19
tests/ui/endpoint/invalid_params_ty.rs
Normal file
19
tests/ui/endpoint/invalid_params_ty.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
#[macro_use]
|
||||
extern crate gotham_restful;
|
||||
use gotham_restful::gotham::hyper::Method;
|
||||
|
||||
#[derive(Resource)]
|
||||
#[resource(endpoint)]
|
||||
struct FooResource;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FooParams {
|
||||
foo: String
|
||||
}
|
||||
|
||||
#[endpoint(method = "Method::GET", uri = "", params = true)]
|
||||
fn endpoint(_: FooParams) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn main() {}
|
54
tests/ui/endpoint/invalid_params_ty.stderr
Normal file
54
tests/ui/endpoint/invalid_params_ty.stderr
Normal file
|
@ -0,0 +1,54 @@
|
|||
error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied
|
||||
--> $DIR/invalid_params_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooParams) {
|
||||
| ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| #[openapi_bound("Params: OpenapiType")]
|
||||
| --------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||
|
||||
error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied
|
||||
--> $DIR/invalid_params_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooParams) {
|
||||
| ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooParams`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
||||
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||
|
||||
error[E0277]: the trait bound `FooParams: StateData` is not satisfied
|
||||
--> $DIR/invalid_params_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooParams) {
|
||||
| ^^^^^^^^^ the trait `StateData` is not implemented for `FooParams`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
||||
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||
|
||||
error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied
|
||||
--> $DIR/invalid_params_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooParams) {
|
||||
| ^^^^^^^^^ the trait `StaticResponseExtender` is not implemented for `FooParams`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
||||
| -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
||||
|
||||
error[E0277]: the trait bound `FooParams: Clone` is not satisfied
|
||||
--> $DIR/invalid_params_ty.rs:15:16
|
||||
|
|
||||
15 | fn endpoint(_: FooParams) {
|
||||
| ^^^^^^^^^ the trait `Clone` is not implemented for `FooParams`
|
||||
|
|
||||
::: $WORKSPACE/src/endpoint.rs
|
||||
|
|
||||
| type Params: QueryStringExtractor<Body> + Clone + Sync;
|
||||
| ----- required by this bound in `gotham_restful::EndpointWithSchema::Params`
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue