mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-19 22:44:38 +00:00
Compare commits
90 commits
v0.1.0-rc0
...
master
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 | |||
71961268c4 | |||
9efc1cb192 | |||
1ac323accc | |||
7851b50bcd | |||
8a8e01e757 | |||
fee9dc89b0 | |||
4fd5464e44 | |||
f9c2009023 | |||
f1e1c5e521 | |||
4ae860dd32 | |||
ed1bbbd1fb | |||
bb945e2cc6 | |||
2cb326a4c3 | |||
00ffe95354 | |||
![]() |
37aa497e59 | ||
e6721275ae | |||
24893223b2 | |||
ce570c4787 | |||
6ded517824 | |||
d3da7d0182 | |||
1369d25c8a | |||
a0059fd7b9 | |||
d9c22579b0 | |||
eeea62859f | |||
0729d5b8ed | |||
6470bd59ef | |||
31835fe57f | |||
38feb5b781 | |||
d55b0897e9 | |||
5317e50961 | |||
0541ee2e0b |
162 changed files with 6278 additions and 4972 deletions
130
.gitlab-ci.yml
130
.gitlab-ci.yml
|
@ -6,75 +6,128 @@ stages:
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
CARGO_HOME: $CI_PROJECT_DIR/cargo
|
||||||
|
RUST_LOG: info,gotham=debug,gotham_restful=trace
|
||||||
|
|
||||||
test-default:
|
check-example:
|
||||||
stage: test
|
stage: test
|
||||||
image: msrd0/rust:alpine-sweep
|
image: rust:slim
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
- cargo sweep -s
|
|
||||||
script:
|
script:
|
||||||
- cargo test --workspace --doc
|
- cargo check --manifest-path example/Cargo.toml
|
||||||
- cargo test --workspace --tests
|
|
||||||
- cargo test --workspace --tests -- --ignored
|
|
||||||
after_script:
|
|
||||||
- cargo sweep -f
|
|
||||||
cache:
|
cache:
|
||||||
key: cargo-default
|
key: cargo-stable-example
|
||||||
paths:
|
paths:
|
||||||
- cargo/
|
- cargo/
|
||||||
- target/
|
- target/
|
||||||
|
|
||||||
test-all:
|
test-default:
|
||||||
stage: test
|
stage: test
|
||||||
image: msrd0/rust:alpine-tarpaulin-sweep
|
image: rust:1.49-slim
|
||||||
before_script:
|
before_script:
|
||||||
- apk add --no-cache postgresql-dev
|
|
||||||
- cargo -V
|
- cargo -V
|
||||||
- cargo sweep -s
|
|
||||||
script:
|
script:
|
||||||
- cargo test --workspace --all-features --doc
|
- cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild
|
||||||
- cargo test --workspace --tests -- --ignored
|
- cargo test
|
||||||
- cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v
|
cache:
|
||||||
after_script:
|
key: cargo-1-49-default
|
||||||
- cargo sweep -f
|
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 --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild
|
||||||
|
- cargo test --no-default-features --features full
|
||||||
|
cache:
|
||||||
|
key: cargo-1-49-all
|
||||||
|
paths:
|
||||||
|
- cargo/
|
||||||
|
- target/
|
||||||
|
|
||||||
|
test-tarpaulin:
|
||||||
|
stage: test
|
||||||
|
image: rust:slim
|
||||||
|
before_script:
|
||||||
|
- apt update -y
|
||||||
|
- apt install -y --no-install-recommends libpq-dev libssl-dev pkgconf
|
||||||
|
- cargo -V
|
||||||
|
- cargo install cargo-tarpaulin
|
||||||
|
script:
|
||||||
|
- 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:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- tarpaulin-report.html
|
- tarpaulin-report.html
|
||||||
|
reports:
|
||||||
|
cobertura: cobertura.xml
|
||||||
cache:
|
cache:
|
||||||
key: cargo-all
|
key: cargo-stable-all
|
||||||
|
paths:
|
||||||
|
- cargo/
|
||||||
|
- target/
|
||||||
|
|
||||||
|
test-trybuild-ui:
|
||||||
|
stage: test
|
||||||
|
image: rust:1.50-slim
|
||||||
|
before_script:
|
||||||
|
- apt update -y
|
||||||
|
- apt install -y --no-install-recommends libpq-dev
|
||||||
|
- cargo -V
|
||||||
|
script:
|
||||||
|
- cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild
|
||||||
|
- cargo test --no-default-features --features full --tests -- --ignored
|
||||||
|
cache:
|
||||||
|
key: cargo-1-50-all
|
||||||
paths:
|
paths:
|
||||||
- cargo/
|
- cargo/
|
||||||
- target/
|
- target/
|
||||||
|
|
||||||
readme:
|
readme:
|
||||||
stage: test
|
stage: test
|
||||||
image: msrd0/cargo-readme
|
image: ghcr.io/msrd0/cargo-readme
|
||||||
|
before_script:
|
||||||
|
- cargo readme -V
|
||||||
script:
|
script:
|
||||||
- cargo readme -t README.tpl >README.md.new
|
- cargo readme -t README.tpl -o README.md.new
|
||||||
- diff README.md README.md.new
|
- diff README.md README.md.new
|
||||||
|
|
||||||
|
rustfmt:
|
||||||
|
stage: test
|
||||||
|
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 --all -- --check
|
||||||
|
- ./tests/ui/rustfmt.sh --check
|
||||||
|
- ./openapi_type/tests/fail/rustfmt.sh --check
|
||||||
|
|
||||||
doc:
|
doc:
|
||||||
stage: build
|
stage: build
|
||||||
image: msrd0/rust:alpine-sweep
|
image: rust:slim
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
- cargo sweep -s
|
|
||||||
script:
|
script:
|
||||||
- cargo doc --all-features
|
- cargo doc --no-default-features --features full
|
||||||
- echo '<!DOCTYPE HTML><html><head><meta http-equiv="Refresh" content="0; url=./gotham_restful/index.html"/></head><body>The documentation is located <a href="./gotham_restful/index.html">here</a></body></html>' >target/doc/index.html
|
|
||||||
after_script:
|
|
||||||
- cargo sweep -f
|
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- target/doc/
|
- target/doc/
|
||||||
cache:
|
cache:
|
||||||
key: cargo-doc
|
key: cargo-stable-doc
|
||||||
paths:
|
paths:
|
||||||
- cargo/
|
- cargo/
|
||||||
- target/
|
- target/
|
||||||
only:
|
|
||||||
- master
|
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: publish
|
stage: publish
|
||||||
|
@ -82,24 +135,9 @@ pages:
|
||||||
script:
|
script:
|
||||||
- mv target/doc public
|
- mv target/doc public
|
||||||
- mv tarpaulin-report.html public/coverage.html
|
- mv tarpaulin-report.html public/coverage.html
|
||||||
|
- echo '<!DOCTYPE HTML><html><head><meta http-equiv="Refresh" content="0; url=./gotham_restful/index.html"/></head><body>The documentation is located <a href="./gotham_restful/index.html">here</a></body></html>' >public/index.html
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
publish:
|
|
||||||
stage: publish
|
|
||||||
image: msrd0/rust:alpine
|
|
||||||
before_script:
|
|
||||||
- cargo -V
|
|
||||||
- cargo login $CRATES_IO_TOKEN
|
|
||||||
script:
|
|
||||||
- cd gotham_restful_derive
|
|
||||||
- cargo publish
|
|
||||||
- sleep 1m
|
|
||||||
- cd ../gotham_restful
|
|
||||||
- cargo publish
|
|
||||||
- cd ..
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
|
|
48
CHANGELOG.md
Normal file
48
CHANGELOG.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- Support for `NonZeroU` types in the OpenAPI Specification
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- cookie auth does not require a middleware for parsing cookies anymore
|
||||||
|
- the derive macro produces no more private `mod`s which makes error message more readable
|
||||||
|
- documentation now makes use of the `[Type]` syntax introduced in Rust 1.48
|
||||||
|
|
||||||
|
## [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
|
63
Cargo.toml
63
Cargo.toml
|
@ -1,60 +1,77 @@
|
||||||
# -*- eval: (cargo-minor-mode 1) -*-
|
# -*- eval: (cargo-minor-mode 1) -*-
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["derive", "example"]
|
members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "gotham_restful"
|
name = "gotham_restful"
|
||||||
version = "0.1.0-rc0"
|
version = "0.3.0-dev"
|
||||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "RESTful additions for the gotham web framework"
|
description = "RESTful additions for the gotham web framework"
|
||||||
keywords = ["gotham", "rest", "restful", "web", "http"]
|
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"
|
readme = "README.md"
|
||||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||||
|
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { version = "0.12.1", optional = true }
|
futures-core = "0.3.7"
|
||||||
chrono = { version = "0.4.11", features = ["serde"], optional = true }
|
futures-util = "0.3.7"
|
||||||
cookie = { version = "0.13.3", optional = true }
|
gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false }
|
||||||
futures-core = "0.3.5"
|
gotham_derive = "0.5.0"
|
||||||
futures-util = "0.3.5"
|
gotham_restful_derive = "0.3.0-dev"
|
||||||
gotham = { version = "0.5.0-rc.1", default-features = false }
|
|
||||||
gotham_derive = "0.5.0-rc.1"
|
|
||||||
gotham_middleware_diesel = { version = "0.1.2", optional = true }
|
|
||||||
gotham_restful_derive = { version = "0.1.0-rc0" }
|
|
||||||
indexmap = { version = "1.3.2", optional = true }
|
|
||||||
itertools = "0.9.0"
|
|
||||||
jsonwebtoken = { version = "7.1.0", optional = true }
|
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
openapiv3 = { version = "0.3.2", optional = true }
|
|
||||||
serde = { version = "1.0.110", features = ["derive"] }
|
serde = { version = "1.0.110", features = ["derive"] }
|
||||||
serde_json = "1.0.53"
|
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]
|
[dev-dependencies]
|
||||||
diesel = { version = "1.4.4", features = ["postgres"] }
|
diesel = { version = "1.4.4", features = ["postgres"] }
|
||||||
futures-executor = "0.3.5"
|
futures-executor = "0.3.5"
|
||||||
paste = "0.1.12"
|
paste = "1.0"
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
|
tokio = { version = "1.0", features = ["time"], default-features = false }
|
||||||
thiserror = "1.0.18"
|
thiserror = "1.0.18"
|
||||||
trybuild = "1.0.27"
|
trybuild = "1.0.27"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cors", "errorlog"]
|
default = ["cors", "errorlog", "without-openapi"]
|
||||||
|
full = ["auth", "cors", "database", "errorlog", "openapi"]
|
||||||
|
|
||||||
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
||||||
cors = []
|
cors = []
|
||||||
errorlog = []
|
|
||||||
database = ["gotham_restful_derive/database", "gotham_middleware_diesel"]
|
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]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
no-default-features = true
|
||||||
|
features = ["full"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
gotham_restful = { path = "." }
|
gotham_restful = { path = "." }
|
||||||
gotham_restful_derive = { path = "./derive" }
|
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)
|
|
||||||
|
|
263
README.md
263
README.md
|
@ -1,263 +1,4 @@
|
||||||
<div align="center">
|
# Moved to GitHub
|
||||||
<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/>
|
|
||||||
|
|
||||||
**Note:** The `stable` branch contains some bugfixes against the last release. The `master`
|
This project has moved to GitHub: https://github.com/msrd0/gotham_restful
|
||||||
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 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.
|
|
||||||
|
|
||||||
## Design Goals
|
|
||||||
|
|
||||||
This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as
|
|
||||||
request/response bodies and does that in a RESTful way, this framework is probably a bad fit for
|
|
||||||
your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful
|
|
||||||
web server in Rust as convenient as possible with the least amount of boilerplate neccessary.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
### 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");
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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">
|
<br/>
|
||||||
<h1>gotham-restful</h1>
|
<div>
|
||||||
</div>
|
|
||||||
<div align="center">
|
|
||||||
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
||||||
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
|
<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"/>
|
<img alt="coverage report" src="https://gitlab.com/msrd0/gotham-restful/badges/master/coverage.svg"/>
|
||||||
</a>
|
</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">
|
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
||||||
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://blog.rust-lang.org/2020/03/12/Rust-1.42.html">
|
<a href="https://blog.rust-lang.org/2020/12/31/Rust-1.49.0.html">
|
||||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.42+-orange.svg"/>
|
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.49+-orange.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
|
||||||
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
|
||||||
|
@ -26,8 +18,44 @@
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
**Note:** The `stable` branch contains some bugfixes against the last release. The `master`
|
This repository contains the following crates:
|
||||||
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.
|
- **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}}
|
{{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]
|
[package]
|
||||||
name = "gotham_restful_derive"
|
name = "gotham_restful_derive"
|
||||||
version = "0.1.0-rc0"
|
version = "0.3.0-dev"
|
||||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "RESTful additions for the gotham web framework - Derive"
|
description = "Derive macros for gotham_restful"
|
||||||
keywords = ["gotham", "rest", "restful", "web", "http", "derive"]
|
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"
|
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||||
|
workspace = ".."
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
@ -17,10 +18,12 @@ proc-macro = true
|
||||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
heck = "0.3.1"
|
once_cell = "1.5"
|
||||||
|
paste = "1.0"
|
||||||
proc-macro2 = "1.0.13"
|
proc-macro2 = "1.0.13"
|
||||||
quote = "1.0.6"
|
quote = "1.0.6"
|
||||||
syn = "1.0.22"
|
regex = "1.4"
|
||||||
|
syn = { version = "1.0.22", features = ["full"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
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,50 +1,42 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use syn::{
|
use syn::{spanned::Spanned, Data, DeriveInput, Error, Field, Fields, Ident, Result, Type};
|
||||||
spanned::Spanned,
|
|
||||||
Data,
|
|
||||||
DeriveInput,
|
|
||||||
Error,
|
|
||||||
Field,
|
|
||||||
Fields,
|
|
||||||
Ident,
|
|
||||||
Result,
|
|
||||||
Type
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ParsedFields
|
struct ParsedFields {
|
||||||
{
|
fields: Vec<(Ident, Type)>,
|
||||||
fields : Vec<(Ident, Type)>,
|
named: bool
|
||||||
named : bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParsedFields
|
impl ParsedFields {
|
||||||
{
|
fn from_named<I>(fields: I) -> Self
|
||||||
fn from_named<I>(fields : I) -> Self
|
|
||||||
where
|
where
|
||||||
I : Iterator<Item = Field>
|
I: Iterator<Item = Field>
|
||||||
{
|
{
|
||||||
let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect();
|
let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect();
|
||||||
Self { fields, named: true }
|
Self { fields, named: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_unnamed<I>(fields : I) -> Self
|
fn from_unnamed<I>(fields: I) -> Self
|
||||||
where
|
where
|
||||||
I : Iterator<Item = Field>
|
I: Iterator<Item = Field>
|
||||||
{
|
{
|
||||||
let fields = fields.enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect();
|
let fields = fields
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| (format_ident!("arg{}", i), field.ty))
|
||||||
|
.collect();
|
||||||
Self { fields, named: false }
|
Self { fields, named: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_unit() -> Self
|
fn from_unit() -> Self {
|
||||||
{
|
Self {
|
||||||
Self { fields: Vec::new(), named: false }
|
fields: Vec::new(),
|
||||||
|
named: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
pub fn expand_from_body(input: DeriveInput) -> Result<TokenStream> {
|
||||||
{
|
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
let ident = input.ident;
|
let ident = input.ident;
|
||||||
let generics = input.generics;
|
let generics = input.generics;
|
||||||
|
@ -53,7 +45,8 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
||||||
Data::Enum(inum) => Err(inum.enum_token.span()),
|
Data::Enum(inum) => Err(inum.enum_token.span()),
|
||||||
Data::Struct(strukt) => Ok(strukt),
|
Data::Struct(strukt) => Ok(strukt),
|
||||||
Data::Union(uni) => Err(uni.union_token.span())
|
Data::Union(uni) => Err(uni.union_token.span())
|
||||||
}.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?;
|
}
|
||||||
|
.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?;
|
||||||
|
|
||||||
let fields = match strukt.fields {
|
let fields = match strukt.fields {
|
||||||
Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()),
|
Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()),
|
||||||
|
@ -66,8 +59,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
||||||
let mut body_ident = format_ident!("_body");
|
let mut body_ident = format_ident!("_body");
|
||||||
let mut type_ident = format_ident!("_type");
|
let mut type_ident = format_ident!("_type");
|
||||||
|
|
||||||
if let Some(body_field) = fields.fields.get(0)
|
if let Some(body_field) = fields.fields.get(0) {
|
||||||
{
|
|
||||||
body_ident = body_field.0.clone();
|
body_ident = body_field.0.clone();
|
||||||
let body_ty = &body_field.1;
|
let body_ty = &body_field.1;
|
||||||
where_clause = quote! {
|
where_clause = quote! {
|
||||||
|
@ -81,8 +73,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(type_field) = fields.fields.get(1)
|
if let Some(type_field) = fields.fields.get(1) {
|
||||||
{
|
|
||||||
type_ident = type_field.0.clone();
|
type_ident = type_field.0.clone();
|
||||||
let type_ty = &type_field.1;
|
let type_ty = &type_field.1;
|
||||||
where_clause = quote! {
|
where_clause = quote! {
|
||||||
|
@ -95,8 +86,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for field in &fields.fields[min(2, fields.fields.len())..]
|
for field in &fields.fields[min(2, fields.fields.len())..] {
|
||||||
{
|
|
||||||
let field_ident = &field.0;
|
let field_ident = &field.0;
|
||||||
let field_ty = &field.1;
|
let field_ty = &field.1;
|
||||||
where_clause = quote! {
|
where_clause = quote! {
|
||||||
|
@ -109,7 +99,7 @@ pub fn expand_from_body(input : DeriveInput) -> Result<TokenStream>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let field_names : Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect();
|
let field_names: Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect();
|
||||||
let ctor = if fields.named {
|
let ctor = if fields.named {
|
||||||
quote!(Self { #(#field_names),* })
|
quote!(Self { #(#field_names),* })
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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_macro::TokenStream;
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
@ -5,130 +9,121 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re
|
||||||
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
mod endpoint;
|
||||||
|
use endpoint::{expand_endpoint, EndpointType};
|
||||||
|
|
||||||
mod from_body;
|
mod from_body;
|
||||||
use from_body::expand_from_body;
|
use from_body::expand_from_body;
|
||||||
mod method;
|
|
||||||
use method::{expand_method, Method};
|
|
||||||
mod request_body;
|
mod request_body;
|
||||||
use request_body::expand_request_body;
|
use request_body::expand_request_body;
|
||||||
|
|
||||||
mod resource;
|
mod resource;
|
||||||
use resource::expand_resource;
|
use resource::expand_resource;
|
||||||
|
|
||||||
mod resource_error;
|
mod resource_error;
|
||||||
use resource_error::expand_resource_error;
|
use resource_error::expand_resource_error;
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
mod openapi_type;
|
mod private_openapi_trait;
|
||||||
#[cfg(feature = "openapi")]
|
use private_openapi_trait::expand_private_openapi_trait;
|
||||||
use openapi_type::expand_openapi_type;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn print_tokens(tokens : TokenStream2) -> TokenStream
|
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
||||||
{
|
// eprintln!("{}", tokens);
|
||||||
//eprintln!("{}", tokens);
|
|
||||||
tokens.into()
|
tokens.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn expand_derive<F>(input : TokenStream, expand : F) -> TokenStream
|
fn expand_derive<F>(input: TokenStream, expand: F) -> TokenStream
|
||||||
where
|
where
|
||||||
F : FnOnce(DeriveInput) -> Result<TokenStream2>
|
F: FnOnce(DeriveInput) -> Result<TokenStream2>
|
||||||
{
|
{
|
||||||
print_tokens(expand(parse_macro_input!(input))
|
print_tokens(expand(parse_macro_input!(input)).unwrap_or_else(|err| err.to_compile_error()))
|
||||||
.unwrap_or_else(|err| err.to_compile_error()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn expand_macro<F, A, I>(attrs : TokenStream, item : TokenStream, expand : F) -> TokenStream
|
fn expand_macro<F, A, I>(attrs: TokenStream, item: TokenStream, expand: F) -> TokenStream
|
||||||
where
|
where
|
||||||
F : FnOnce(A, I) -> Result<TokenStream2>,
|
F: FnOnce(A, I) -> Result<TokenStream2>,
|
||||||
A : ParseMacroInput,
|
A: ParseMacroInput,
|
||||||
I : ParseMacroInput
|
I: ParseMacroInput
|
||||||
{
|
{
|
||||||
print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item))
|
print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)).unwrap_or_else(|err| err.to_compile_error()))
|
||||||
.unwrap_or_else(|err| err.to_compile_error()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn krate() -> TokenStream2
|
fn krate() -> TokenStream2 {
|
||||||
{
|
|
||||||
quote!(::gotham_restful)
|
quote!(::gotham_restful)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(FromBody)]
|
#[proc_macro_derive(FromBody)]
|
||||||
pub fn derive_from_body(input : TokenStream) -> TokenStream
|
pub fn derive_from_body(input: TokenStream) -> TokenStream {
|
||||||
{
|
|
||||||
expand_derive(input, expand_from_body)
|
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))]
|
#[proc_macro_derive(RequestBody, attributes(supported_types))]
|
||||||
pub fn derive_request_body(input : TokenStream) -> TokenStream
|
pub fn derive_request_body(input: TokenStream) -> TokenStream {
|
||||||
{
|
|
||||||
expand_derive(input, expand_request_body)
|
expand_derive(input, expand_request_body)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(Resource, attributes(resource))]
|
#[proc_macro_derive(Resource, attributes(resource))]
|
||||||
pub fn derive_resource(input : TokenStream) -> TokenStream
|
pub fn derive_resource(input: TokenStream) -> TokenStream {
|
||||||
{
|
|
||||||
expand_derive(input, expand_resource)
|
expand_derive(input, expand_resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_derive(ResourceError, attributes(display, from, status))]
|
#[proc_macro_derive(ResourceError, attributes(display, from, status))]
|
||||||
pub fn derive_resource_error(input : TokenStream) -> TokenStream
|
pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
||||||
{
|
|
||||||
expand_derive(input, expand_resource_error)
|
expand_derive(input, expand_resource_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn read_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::custom(), attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn read(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::Read, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn search(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::Search, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn create(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::Create, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn change_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn change(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::Change, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn remove_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, attr, item))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn remove(attr : TokenStream, item : TokenStream) -> TokenStream
|
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
{
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item))
|
||||||
expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item))
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
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,480 +0,0 @@
|
||||||
use crate::util::CollectToResult;
|
|
||||||
use heck::{CamelCase, SnakeCase};
|
|
||||||
use proc_macro2::{Ident, Span, TokenStream};
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use syn::{
|
|
||||||
spanned::Spanned,
|
|
||||||
Attribute,
|
|
||||||
AttributeArgs,
|
|
||||||
Error,
|
|
||||||
FnArg,
|
|
||||||
ItemFn,
|
|
||||||
Lit,
|
|
||||||
LitBool,
|
|
||||||
Meta,
|
|
||||||
NestedMeta,
|
|
||||||
PatType,
|
|
||||||
Result,
|
|
||||||
ReturnType,
|
|
||||||
Type
|
|
||||||
};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
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 mod_ident(&self, resource : &str) -> Ident
|
|
||||||
{
|
|
||||||
format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident())
|
|
||||||
}
|
|
||||||
|
|
||||||
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!("{}_{}_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_state_ref(&self) -> bool
|
|
||||||
{
|
|
||||||
matches!(self, Self::StateRef | Self::StateMutRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
|
||||||
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_ident = &fun.sig.ident;
|
|
||||||
let fun_vis = &fun.vis;
|
|
||||||
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 mod_ident = method.mod_ident(&resource_name);
|
|
||||||
let handler_ident = method.handler_struct_ident(&resource_name);
|
|
||||||
let setup_ident = method.setup_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| (*arg).ty.is_state_ref())
|
|
||||||
{
|
|
||||||
return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync, consider boxing"));
|
|
||||||
}
|
|
||||||
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
|
|
||||||
Ok(quote! {
|
|
||||||
#fun
|
|
||||||
|
|
||||||
#fun_vis mod #mod_ident
|
|
||||||
{
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[deny(dead_code)]
|
|
||||||
pub fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D)
|
|
||||||
{
|
|
||||||
route.#method_ident::<#handler_ident>();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,282 +0,0 @@
|
||||||
use crate::util::{CollectToResult, remove_parens};
|
|
||||||
use proc_macro2::{Ident, TokenStream};
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{
|
|
||||||
parse_macro_input,
|
|
||||||
spanned::Spanned,
|
|
||||||
Attribute,
|
|
||||||
AttributeArgs,
|
|
||||||
Data,
|
|
||||||
DataEnum,
|
|
||||||
DataStruct,
|
|
||||||
DeriveInput,
|
|
||||||
Error,
|
|
||||||
Field,
|
|
||||||
Fields,
|
|
||||||
Generics,
|
|
||||||
GenericParam,
|
|
||||||
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 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
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,66 +6,70 @@ use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
punctuated::Punctuated,
|
punctuated::Punctuated,
|
||||||
spanned::Spanned,
|
spanned::Spanned,
|
||||||
DeriveInput,
|
DeriveInput, Error, Generics, Path, Result, Token
|
||||||
Error,
|
|
||||||
Generics,
|
|
||||||
Path,
|
|
||||||
Result,
|
|
||||||
Token
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MimeList(Punctuated<Path, Token![,]>);
|
struct MimeList(Punctuated<Path, Token![,]>);
|
||||||
|
|
||||||
impl Parse for MimeList
|
impl Parse for MimeList {
|
||||||
{
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
fn parse(input: ParseStream) -> Result<Self>
|
|
||||||
{
|
|
||||||
let list = Punctuated::parse_separated_nonempty(&input)?;
|
let list = Punctuated::parse_separated_nonempty(&input)?;
|
||||||
Ok(Self(list))
|
Ok(Self(list))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
#[cfg(not(feature = "openapi"))]
|
||||||
fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream
|
fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream {
|
||||||
{
|
|
||||||
quote!()
|
quote!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream
|
fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
|
||||||
{
|
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
|
let openapi = quote!(#krate::private::openapi);
|
||||||
|
|
||||||
quote! {
|
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};
|
#krate::private::OpenapiSchema::new(
|
||||||
|
#openapi::SchemaKind::Type(
|
||||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
#openapi::Type::String(
|
||||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
#openapi::StringType {
|
||||||
..Default::default()
|
format: #openapi::VariantOrUnknownOrEmpty::Item(
|
||||||
})))
|
#openapi::StringFormat::Binary
|
||||||
|
),
|
||||||
|
.. ::std::default::Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_request_body(input : DeriveInput) -> Result<TokenStream>
|
pub fn expand_request_body(input: DeriveInput) -> Result<TokenStream> {
|
||||||
{
|
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
let ident = input.ident;
|
let ident = input.ident;
|
||||||
let generics = input.generics;
|
let generics = input.generics;
|
||||||
|
|
||||||
let types = input.attrs.into_iter()
|
let types = input
|
||||||
.filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()))
|
.attrs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|attr| {
|
||||||
|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())
|
||||||
|
})
|
||||||
.flat_map(|attr| {
|
.flat_map(|attr| {
|
||||||
let span = attr.span();
|
let span = attr.span();
|
||||||
attr.parse_args::<MimeList>()
|
attr.parse_args::<MimeList>()
|
||||||
.map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>)
|
.map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>)
|
||||||
.unwrap_or_else(|mut err| {
|
.unwrap_or_else(|mut err| {
|
||||||
err.combine(Error::new(span, "Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]"));
|
err.combine(Error::new(
|
||||||
|
span,
|
||||||
|
"Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]"
|
||||||
|
));
|
||||||
Box::new(iter::once(Err(err)))
|
Box::new(iter::once(Err(err)))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
use crate::{method::Method, util::CollectToResult};
|
use crate::{
|
||||||
|
endpoint::endpoint_ident,
|
||||||
|
util::{CollectToResult, PathEndsWith}
|
||||||
|
};
|
||||||
use proc_macro2::{Ident, TokenStream};
|
use proc_macro2::{Ident, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use std::iter;
|
||||||
use syn::{
|
use syn::{
|
||||||
parenthesized,
|
parenthesized,
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
punctuated::Punctuated,
|
punctuated::Punctuated,
|
||||||
DeriveInput,
|
DeriveInput, Result, Token
|
||||||
Error,
|
|
||||||
Result,
|
|
||||||
Token
|
|
||||||
};
|
};
|
||||||
use std::{iter, str::FromStr};
|
|
||||||
|
|
||||||
struct MethodList(Punctuated<Ident, Token![,]>);
|
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||||
|
|
||||||
impl Parse for MethodList
|
impl Parse for MethodList {
|
||||||
{
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
fn parse(input: ParseStream) -> Result<Self>
|
|
||||||
{
|
|
||||||
let content;
|
let content;
|
||||||
let _paren = parenthesized!(content in input);
|
let _paren = parenthesized!(content in input);
|
||||||
let list = Punctuated::parse_separated_nonempty(&content)?;
|
let list = Punctuated::parse_separated_nonempty(&content)?;
|
||||||
|
@ -25,27 +23,25 @@ impl Parse for MethodList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_resource(input : DeriveInput) -> Result<TokenStream>
|
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||||
{
|
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
let ident = input.ident;
|
let ident = input.ident;
|
||||||
let name = ident.to_string();
|
|
||||||
|
|
||||||
let methods = input.attrs.into_iter().filter(|attr|
|
let methods = input
|
||||||
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) // TODO wtf
|
.attrs
|
||||||
).map(|attr| {
|
.into_iter()
|
||||||
syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter())
|
.filter(|attr| attr.path.ends_with("resource"))
|
||||||
}).flat_map(|list| match list {
|
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||||
Ok(iter) => Box::new(iter.map(|method| {
|
.flat_map(|list| match list {
|
||||||
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
|
Ok(iter) => Box::new(iter.map(|method| {
|
||||||
let mod_ident = method.mod_ident(&name);
|
let ident = endpoint_ident(&method);
|
||||||
let ident = method.setup_ident(&name);
|
Ok(quote!(route.endpoint::<#ident>();))
|
||||||
Ok(quote!(#mod_ident::#ident(&mut route);))
|
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||||
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
Err(err) => Box::new(iter::once(Err(err)))
|
||||||
Err(err) => Box::new(iter::once(Err(err)))
|
})
|
||||||
}).collect_to_result()?;
|
.collect_to_result()?;
|
||||||
|
|
||||||
Ok(quote! {
|
let non_openapi_impl = quote! {
|
||||||
impl #krate::Resource for #ident
|
impl #krate::Resource for #ident
|
||||||
{
|
{
|
||||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||||
|
@ -53,5 +49,22 @@ pub fn expand_resource(input : DeriveInput) -> Result<TokenStream>
|
||||||
#(#methods)*
|
#(#methods)*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
let openapi_impl = if !cfg!(feature = "openapi") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(quote! {
|
||||||
|
impl #krate::ResourceWithSchema for #ident
|
||||||
|
{
|
||||||
|
fn setup<D : #krate::DrawResourceRoutesWithSchema>(mut route : D)
|
||||||
|
{
|
||||||
|
#(#methods)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
Ok(quote! {
|
||||||
|
#non_openapi_impl
|
||||||
|
#openapi_impl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +1,54 @@
|
||||||
use crate::util::{CollectToResult, remove_parens};
|
use crate::util::{remove_parens, CollectToResult};
|
||||||
use proc_macro2::{Ident, TokenStream};
|
use proc_macro2::{Ident, TokenStream};
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use syn::{
|
use syn::{
|
||||||
spanned::Spanned,
|
spanned::Spanned, Attribute, Data, DeriveInput, Error, Fields, GenericParam, LitStr, Path, PathSegment, Result, Type,
|
||||||
Attribute,
|
|
||||||
Data,
|
|
||||||
DeriveInput,
|
|
||||||
Error,
|
|
||||||
Fields,
|
|
||||||
GenericParam,
|
|
||||||
LitStr,
|
|
||||||
Path,
|
|
||||||
PathSegment,
|
|
||||||
Result,
|
|
||||||
Type,
|
|
||||||
Variant
|
Variant
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ErrorVariantField {
|
||||||
struct ErrorVariantField
|
attrs: Vec<Attribute>,
|
||||||
{
|
ident: Ident,
|
||||||
attrs : Vec<Attribute>,
|
ty: Type
|
||||||
ident : Ident,
|
|
||||||
ty : Type
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ErrorVariant
|
struct ErrorVariant {
|
||||||
{
|
ident: Ident,
|
||||||
ident : Ident,
|
status: Option<Path>,
|
||||||
status : Option<Path>,
|
is_named: bool,
|
||||||
is_named : bool,
|
fields: Vec<ErrorVariantField>,
|
||||||
fields : Vec<ErrorVariantField>,
|
from_ty: Option<(usize, Type)>,
|
||||||
from_ty : Option<(usize, Type)>,
|
display: Option<LitStr>
|
||||||
display : Option<LitStr>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_variant(variant : Variant) -> Result<ErrorVariant>
|
fn process_variant(variant: Variant) -> Result<ErrorVariant> {
|
||||||
{
|
let status =
|
||||||
let status = match variant.attrs.iter()
|
match variant.attrs.iter().find(|attr| {
|
||||||
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string()))
|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("status".to_string())
|
||||||
{
|
}) {
|
||||||
Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
|
Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut is_named = false;
|
let mut is_named = false;
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
match variant.fields {
|
match variant.fields {
|
||||||
Fields::Named(named) => {
|
Fields::Named(named) => {
|
||||||
is_named = true;
|
is_named = true;
|
||||||
for field in named.named
|
for field in named.named {
|
||||||
{
|
|
||||||
let span = field.span();
|
let span = field.span();
|
||||||
fields.push(ErrorVariantField {
|
fields.push(ErrorVariantField {
|
||||||
attrs: field.attrs,
|
attrs: field.attrs,
|
||||||
ident: field.ident.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?,
|
ident: field
|
||||||
|
.ident
|
||||||
|
.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?,
|
||||||
ty: field.ty
|
ty: field.ty
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Fields::Unnamed(unnamed) => {
|
Fields::Unnamed(unnamed) => {
|
||||||
for (i, field) in unnamed.unnamed.into_iter().enumerate()
|
for (i, field) in unnamed.unnamed.into_iter().enumerate() {
|
||||||
{
|
|
||||||
fields.push(ErrorVariantField {
|
fields.push(ErrorVariantField {
|
||||||
attrs: field.attrs,
|
attrs: field.attrs,
|
||||||
ident: format_ident!("arg{}", i),
|
ident: format_ident!("arg{}", i),
|
||||||
|
@ -73,14 +59,20 @@ fn process_variant(variant : Variant) -> Result<ErrorVariant>
|
||||||
Fields::Unit => {}
|
Fields::Unit => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let from_ty = fields.iter()
|
let from_ty = fields
|
||||||
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_, field)| field.attrs.iter().any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string())))
|
.find(|(_, field)| {
|
||||||
|
field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string()))
|
||||||
|
})
|
||||||
.map(|(i, field)| (i, field.ty.clone()));
|
.map(|(i, field)| (i, field.ty.clone()));
|
||||||
|
|
||||||
let display = match variant.attrs.iter()
|
let display = match variant.attrs.iter().find(|attr| {
|
||||||
.find(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string()))
|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string())
|
||||||
{
|
}) {
|
||||||
Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
|
Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?),
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
|
@ -95,18 +87,15 @@ fn process_variant(variant : Variant) -> Result<ErrorVariant>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_segment(name : &str) -> PathSegment
|
fn path_segment(name: &str) -> PathSegment {
|
||||||
{
|
|
||||||
PathSegment {
|
PathSegment {
|
||||||
ident: format_ident!("{}", name),
|
ident: format_ident!("{}", name),
|
||||||
arguments: Default::default()
|
arguments: Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorVariant
|
impl ErrorVariant {
|
||||||
{
|
fn fields_pat(&self) -> TokenStream {
|
||||||
fn fields_pat(&self) -> TokenStream
|
|
||||||
{
|
|
||||||
let mut fields = self.fields.iter().map(|field| &field.ident).peekable();
|
let mut fields = self.fields.iter().map(|field| &field.ident).peekable();
|
||||||
if fields.peek().is_none() {
|
if fields.peek().is_none() {
|
||||||
quote!()
|
quote!()
|
||||||
|
@ -117,49 +106,58 @@ impl ErrorVariant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result<TokenStream>
|
fn to_display_match_arm(&self, formatter_ident: &Ident, enum_ident: &Ident) -> Result<TokenStream> {
|
||||||
{
|
|
||||||
let ident = &self.ident;
|
let ident = &self.ident;
|
||||||
let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?;
|
let display = self
|
||||||
|
.display
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?;
|
||||||
|
|
||||||
// lets find all required format parameters
|
// lets find all required format parameters
|
||||||
let display_str = display.value();
|
let display_str = display.value();
|
||||||
let mut params : Vec<&str> = Vec::new();
|
let mut params: Vec<&str> = Vec::new();
|
||||||
let len = display_str.len();
|
let len = display_str.len();
|
||||||
let mut start = len;
|
let mut start = len;
|
||||||
let mut iter = display_str.chars().enumerate().peekable();
|
let mut iter = display_str.chars().enumerate().peekable();
|
||||||
while let Some((i, c)) = iter.next()
|
while let Some((i, c)) = iter.next() {
|
||||||
{
|
|
||||||
// we found a new opening brace
|
// we found a new opening brace
|
||||||
if start == len && c == '{'
|
if start == len && c == '{' {
|
||||||
{
|
|
||||||
start = i + 1;
|
start = i + 1;
|
||||||
}
|
}
|
||||||
// we found a duplicate opening brace
|
// we found a duplicate opening brace
|
||||||
else if start == i && c == '{'
|
else if start == i && c == '{' {
|
||||||
{
|
|
||||||
start = len;
|
start = len;
|
||||||
}
|
}
|
||||||
// we found a closing brace
|
// we found a closing brace
|
||||||
else if start < i && c == '}'
|
else if start < i && c == '}' {
|
||||||
{
|
|
||||||
match iter.peek() {
|
match iter.peek() {
|
||||||
Some((_, '}')) => return Err(Error::new(display.span(), "Error parsing format string: curly braces not allowed inside parameter name")),
|
Some((_, '}')) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
display.span(),
|
||||||
|
"Error parsing format string: curly braces not allowed inside parameter name"
|
||||||
|
))
|
||||||
|
},
|
||||||
_ => params.push(&display_str[start..i])
|
_ => params.push(&display_str[start..i])
|
||||||
};
|
};
|
||||||
start = len;
|
start = len;
|
||||||
}
|
}
|
||||||
// we found a closing brace without content
|
// we found a closing brace without content
|
||||||
else if start == i && c == '}'
|
else if start == i && c == '}' {
|
||||||
{
|
return Err(Error::new(
|
||||||
return Err(Error::new(display.span(), "Error parsing format string: parameter name must not be empty"))
|
display.span(),
|
||||||
|
"Error parsing format string: parameter name must not be empty"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if start != len
|
if start != len {
|
||||||
{
|
return Err(Error::new(
|
||||||
return Err(Error::new(display.span(), "Error parsing format string: Unmatched opening brace"));
|
display.span(),
|
||||||
|
"Error parsing format string: Unmatched opening brace"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let params = params.into_iter().map(|name| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, name));
|
let params = params
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, name));
|
||||||
|
|
||||||
let fields_pat = self.fields_pat();
|
let fields_pat = self.fields_pat();
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
|
@ -167,21 +165,28 @@ impl ErrorVariant
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> Result<TokenStream>
|
fn into_match_arm(self, krate: &TokenStream, enum_ident: &Ident) -> Result<TokenStream> {
|
||||||
{
|
|
||||||
let ident = &self.ident;
|
let ident = &self.ident;
|
||||||
let fields_pat = self.fields_pat();
|
let fields_pat = self.fields_pat();
|
||||||
let status = self.status.map(|status| {
|
let status = self.status.map(|status| {
|
||||||
// the status might be relative to StatusCode, so let's fix that
|
// the status might be relative to StatusCode, so let's fix that
|
||||||
if status.leading_colon.is_none() && status.segments.len() < 2
|
if status.leading_colon.is_none() && status.segments.len() < 2 {
|
||||||
{
|
|
||||||
let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK"));
|
let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK"));
|
||||||
Path {
|
Path {
|
||||||
leading_colon: Some(Default::default()),
|
leading_colon: Some(Default::default()),
|
||||||
segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect()
|
segments: vec![
|
||||||
|
path_segment("gotham_restful"),
|
||||||
|
path_segment("gotham"),
|
||||||
|
path_segment("hyper"),
|
||||||
|
path_segment("StatusCode"),
|
||||||
|
status_ident,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
status
|
||||||
}
|
}
|
||||||
else { status }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// the response will come directly from the from_ty if present
|
// the response will come directly from the from_ty if present
|
||||||
|
@ -191,11 +196,11 @@ impl ErrorVariant
|
||||||
quote!(#from_field.into_response_error())
|
quote!(#from_field.into_response_error())
|
||||||
},
|
},
|
||||||
(Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")),
|
(Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")),
|
||||||
(None, Some(status)) => quote!(Ok(#krate::Response {
|
(None, Some(status)) => quote!(Ok(#krate::Response::new(
|
||||||
status: { #status }.into(),
|
{ #status }.into(),
|
||||||
body: #krate::gotham::hyper::Body::empty(),
|
#krate::gotham::hyper::Body::empty(),
|
||||||
mime: None
|
None
|
||||||
})),
|
))),
|
||||||
(None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant"))
|
(None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant"))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -204,8 +209,7 @@ impl ErrorVariant
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn were(&self) -> Option<TokenStream>
|
fn were(&self) -> Option<TokenStream> {
|
||||||
{
|
|
||||||
match self.from_ty.as_ref() {
|
match self.from_ty.as_ref() {
|
||||||
Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )),
|
Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )),
|
||||||
None => None
|
None => None
|
||||||
|
@ -213,8 +217,7 @@ impl ErrorVariant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
pub fn expand_resource_error(input: DeriveInput) -> Result<TokenStream> {
|
||||||
{
|
|
||||||
let krate = super::krate();
|
let krate = super::krate();
|
||||||
let ident = input.ident;
|
let ident = input.ident;
|
||||||
let generics = input.generics;
|
let generics = input.generics;
|
||||||
|
@ -223,12 +226,13 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||||
Data::Enum(inum) => Ok(inum),
|
Data::Enum(inum) => Ok(inum),
|
||||||
Data::Struct(strukt) => Err(strukt.struct_token.span()),
|
Data::Struct(strukt) => Err(strukt.struct_token.span()),
|
||||||
Data::Union(uni) => Err(uni.union_token.span())
|
Data::Union(uni) => Err(uni.union_token.span())
|
||||||
}.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?;
|
}
|
||||||
let variants = inum.variants.into_iter()
|
.map_err(|span| Error::new(span, "#[derive(ResourceError)] only works for enums"))?;
|
||||||
.map(process_variant)
|
let variants = inum.variants.into_iter().map(process_variant).collect_to_result()?;
|
||||||
.collect_to_result()?;
|
|
||||||
|
|
||||||
let display_impl = if variants.iter().any(|v| v.display.is_none()) { None } else {
|
let display_impl = if variants.iter().any(|v| v.display.is_none()) {
|
||||||
|
None // TODO issue warning if display is present on some but not all
|
||||||
|
} else {
|
||||||
let were = generics.params.iter().filter_map(|param| match param {
|
let were = generics.params.iter().filter_map(|param| match param {
|
||||||
GenericParam::Type(ty) => {
|
GenericParam::Type(ty) => {
|
||||||
let ident = &ty.ident;
|
let ident = &ty.ident;
|
||||||
|
@ -237,7 +241,8 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||||
_ => None
|
_ => None
|
||||||
});
|
});
|
||||||
let formatter_ident = format_ident!("resource_error_display_formatter");
|
let formatter_ident = format_ident!("resource_error_display_formatter");
|
||||||
let match_arms = variants.iter()
|
let match_arms = variants
|
||||||
|
.iter()
|
||||||
.map(|v| v.to_display_match_arm(&formatter_ident, &ident))
|
.map(|v| v.to_display_match_arm(&formatter_ident, &ident))
|
||||||
.collect_to_result()?;
|
.collect_to_result()?;
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
|
@ -254,10 +259,9 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut from_impls : Vec<TokenStream> = Vec::new();
|
let mut from_impls: Vec<TokenStream> = Vec::new();
|
||||||
|
|
||||||
for var in &variants
|
for var in &variants {
|
||||||
{
|
|
||||||
let var_ident = &var.ident;
|
let var_ident = &var.ident;
|
||||||
let (from_index, from_ty) = match var.from_ty.as_ref() {
|
let (from_index, from_ty) = match var.from_ty.as_ref() {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
|
@ -266,14 +270,20 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||||
let from_ident = &var.fields[*from_index].ident;
|
let from_ident = &var.fields[*from_index].ident;
|
||||||
|
|
||||||
let fields_pat = var.fields_pat();
|
let fields_pat = var.fields_pat();
|
||||||
let fields_where = var.fields.iter().enumerate()
|
let fields_where = var
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
.filter(|(i, _)| i != from_index)
|
.filter(|(i, _)| i != from_index)
|
||||||
.map(|(_, field)| {
|
.map(|(_, field)| {
|
||||||
let ty = &field.ty;
|
let ty = &field.ty;
|
||||||
quote!( #ty : Default )
|
quote!( #ty : Default )
|
||||||
})
|
})
|
||||||
.chain(iter::once(quote!( #from_ty : ::std::error::Error )));
|
.chain(iter::once(quote!( #from_ty : ::std::error::Error )));
|
||||||
let fields_let = var.fields.iter().enumerate()
|
let fields_let = var
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
.filter(|(i, _)| i != from_index)
|
.filter(|(i, _)| i != from_index)
|
||||||
.map(|(_, field)| {
|
.map(|(_, field)| {
|
||||||
let id = &field.ident;
|
let id = &field.ident;
|
||||||
|
@ -295,9 +305,10 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||||
}
|
}
|
||||||
|
|
||||||
let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>();
|
let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>();
|
||||||
let variants = variants.into_iter()
|
let variants = variants
|
||||||
|
.into_iter()
|
||||||
.map(|variant| variant.into_match_arm(&krate, &ident))
|
.map(|variant| variant.into_match_arm(&krate, &ident))
|
||||||
.collect_to_result()?;
|
.collect_to_result()?;
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
#display_impl
|
#display_impl
|
||||||
|
@ -305,7 +316,7 @@ pub fn expand_resource_error(input : DeriveInput) -> Result<TokenStream>
|
||||||
impl #generics #krate::IntoResponseError for #ident #generics
|
impl #generics #krate::IntoResponseError for #ident #generics
|
||||||
where #( #were ),*
|
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>
|
fn into_response_error(self) -> Result<#krate::Response, Self::Err>
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,41 +1,70 @@
|
||||||
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use syn::Error;
|
use syn::{Error, Lit, LitBool, LitStr, Path, Result};
|
||||||
|
|
||||||
pub trait CollectToResult
|
pub(crate) trait CollectToResult {
|
||||||
{
|
|
||||||
type Item;
|
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
|
impl<Item, I> CollectToResult for I
|
||||||
where
|
where
|
||||||
I : Iterator<Item = Result<Item, Error>>
|
I: Iterator<Item = Result<Item>>
|
||||||
{
|
{
|
||||||
type Item = Item;
|
type Item = Item;
|
||||||
|
|
||||||
fn collect_to_result(self) -> Result<Vec<Item>, Error>
|
fn collect_to_result(self) -> Result<Vec<Item>> {
|
||||||
{
|
self.fold(Ok(Vec::new()), |res, code| match (code, res) {
|
||||||
self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| {
|
(Ok(code), Ok(mut codes)) => {
|
||||||
match (code, res) {
|
codes.push(code);
|
||||||
(Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) },
|
Ok(codes)
|
||||||
(Ok(_), Err(errors)) => Err(errors),
|
},
|
||||||
(Err(err), Ok(_)) => Err(err),
|
(Ok(_), Err(errors)) => Err(errors),
|
||||||
(Err(err), Err(mut errors)) => { errors.combine(err); Err(errors) }
|
(Err(err), Ok(_)) => Err(err),
|
||||||
}
|
(Err(err), Err(mut errors)) => {
|
||||||
|
errors.combine(err);
|
||||||
|
Err(errors)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) trait ExpectLit {
|
||||||
|
fn expect_bool(self) -> Result<LitBool>;
|
||||||
|
fn expect_str(self) -> Result<LitStr>;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_parens(input : TokenStream) -> TokenStream
|
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| {
|
let iter = input.into_iter().flat_map(|tt| {
|
||||||
if let TokenTree::Group(group) = &tt
|
if let TokenTree::Group(group) = &tt {
|
||||||
{
|
if group.delimiter() == Delimiter::Parenthesis {
|
||||||
if group.delimiter() == Delimiter::Parenthesis
|
|
||||||
{
|
|
||||||
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,22 +2,23 @@
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "example"
|
name = "example"
|
||||||
version = "0.0.1"
|
version = "0.0.0"
|
||||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "Unlicense"
|
license = "Unlicense"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
include = ["src/**/*", "Cargo.toml", "LICENSE"]
|
|
||||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||||
|
publish = false
|
||||||
|
workspace = ".."
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fake = "2.2.2"
|
fake = "2.2.2"
|
||||||
gotham = { version = "0.5.0-rc.1", default-features = false }
|
gotham = { version = "0.5.0", default-features = false }
|
||||||
gotham_derive = "0.5.0-rc.1"
|
gotham_derive = "0.5.0"
|
||||||
gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] }
|
gotham_restful = { version = "0.2.0", features = ["auth", "cors", "openapi"], default-features = false }
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
log4rs = { version = "0.12.0", features = ["console_appender"], default-features = false }
|
pretty_env_logger = "0.4"
|
||||||
serde = "1.0.110"
|
serde = "1.0.110"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#[macro_use] extern crate gotham_derive;
|
#[macro_use]
|
||||||
#[macro_use] extern crate log;
|
extern crate gotham_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
use fake::{faker::internet::en::Username, Fake};
|
use fake::{faker::internet::en::Username, Fake};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
@ -9,36 +11,24 @@ use gotham::{
|
||||||
router::builder::*,
|
router::builder::*,
|
||||||
state::State
|
state::State
|
||||||
};
|
};
|
||||||
use gotham_restful::*;
|
use gotham_restful::{cors::*, *};
|
||||||
use log::LevelFilter;
|
|
||||||
use log4rs::{
|
|
||||||
append::console::ConsoleAppender,
|
|
||||||
config::{Appender, Config, Root},
|
|
||||||
encode::pattern::PatternEncoder
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[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
|
struct Users {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(ReadAll)]
|
#[resource(auth_read_all)]
|
||||||
struct Auth
|
struct Auth {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)]
|
#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)]
|
||||||
struct User
|
struct User {
|
||||||
{
|
username: String
|
||||||
username : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(Users)]
|
#[read_all]
|
||||||
fn read_all() -> Success<Vec<Option<User>>>
|
fn read_all() -> Success<Vec<Option<User>>> {
|
||||||
{
|
|
||||||
vec![Username().fake(), Username().fake()]
|
vec![Username().fake(), Username().fake()]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|username| Some(User { username }))
|
.map(|username| Some(User { username }))
|
||||||
|
@ -46,114 +36,95 @@ fn read_all() -> Success<Vec<Option<User>>>
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read(Users)]
|
#[read]
|
||||||
fn read(id : u64) -> Success<User>
|
fn read(id: u64) -> Success<User> {
|
||||||
{
|
let username: String = Username().fake();
|
||||||
let username : String = Username().fake();
|
User {
|
||||||
User { username: format!("{}{}", username, id) }.into()
|
username: format!("{}{}", username, id)
|
||||||
|
}
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[search(Users)]
|
#[search]
|
||||||
fn search(query : User) -> Success<User>
|
fn search(query: User) -> Success<User> {
|
||||||
{
|
|
||||||
query.into()
|
query.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[create(Users)]
|
#[create]
|
||||||
fn create(body : User)
|
fn create(body: User) {
|
||||||
{
|
|
||||||
info!("Created User: {}", body.username);
|
info!("Created User: {}", body.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[change_all(Users)]
|
#[change_all]
|
||||||
fn update_all(body : Vec<User>)
|
fn update_all(body: Vec<User>) {
|
||||||
{
|
info!(
|
||||||
info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::<Vec<String>>());
|
"Changing all Users to {:?}",
|
||||||
|
body.into_iter().map(|u| u.username).collect::<Vec<String>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[change(Users)]
|
#[change]
|
||||||
fn update(id : u64, body : User)
|
fn update(id: u64, body: User) {
|
||||||
{
|
|
||||||
info!("Change User {} to {}", id, body.username);
|
info!("Change User {} to {}", id, body.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[remove_all(Users)]
|
#[remove_all]
|
||||||
fn remove_all()
|
fn remove_all() {
|
||||||
{
|
|
||||||
info!("Delete all Users");
|
info!("Delete all Users");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[remove(Users)]
|
#[remove]
|
||||||
fn remove(id : u64)
|
fn remove(id: u64) {
|
||||||
{
|
|
||||||
info!("Delete User {}", id);
|
info!("Delete User {}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(Auth)]
|
#[read_all]
|
||||||
fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess<String>
|
fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess<String> {
|
||||||
{
|
|
||||||
match auth {
|
match auth {
|
||||||
AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)),
|
AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)),
|
||||||
_ => Err(Forbidden)
|
_ => Err(Forbidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDR : &str = "127.0.0.1:18080";
|
const ADDR: &str = "127.0.0.1:18080";
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct Handler;
|
struct Handler;
|
||||||
impl<T> AuthHandler<T> for Handler
|
impl<T> AuthHandler<T> for Handler {
|
||||||
{
|
fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, _decode_data: F) -> Option<Vec<u8>> {
|
||||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
|
|
||||||
{
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main()
|
fn main() {
|
||||||
{
|
pretty_env_logger::init_timed();
|
||||||
let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S%.3f %Z)} [{l}] {M} - {m}\n");
|
|
||||||
let config = Config::builder()
|
|
||||||
.appender(
|
|
||||||
Appender::builder()
|
|
||||||
.build("stdout", Box::new(
|
|
||||||
ConsoleAppender::builder()
|
|
||||||
.encoder(Box::new(encoder))
|
|
||||||
.build()
|
|
||||||
)))
|
|
||||||
.build(Root::builder().appender("stdout").build(LevelFilter::Info))
|
|
||||||
.unwrap();
|
|
||||||
log4rs::init_config(config).unwrap();
|
|
||||||
|
|
||||||
let cors = CorsConfig {
|
let cors = CorsConfig {
|
||||||
origin: Origin::Copy,
|
origin: Origin::Copy,
|
||||||
headers: vec![CONTENT_TYPE],
|
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||||
credentials: true,
|
credentials: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let auth = <AuthMiddleware<(), Handler>>::from_source(AuthSource::AuthorizationHeader);
|
let auth = <AuthMiddleware<(), Handler>>::from_source(AuthSource::AuthorizationHeader);
|
||||||
let logging = RequestLogger::new(log::Level::Info);
|
let logging = RequestLogger::new(log::Level::Info);
|
||||||
let (chain, pipelines) = single_pipeline(
|
let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).add(logging).add(cors).build());
|
||||||
new_pipeline()
|
|
||||||
.add(auth)
|
|
||||||
.add(logging)
|
|
||||||
.add(cors)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
gotham::start(ADDR, build_router(chain, pipelines, |route| {
|
gotham::start(
|
||||||
let info = OpenapiInfo {
|
ADDR,
|
||||||
title: "Users Example".to_owned(),
|
build_router(chain, pipelines, |route| {
|
||||||
version: "0.0.1".to_owned(),
|
let info = OpenapiInfo {
|
||||||
urls: vec![format!("http://{}", ADDR)]
|
title: "Users Example".to_owned(),
|
||||||
};
|
version: "0.0.1".to_owned(),
|
||||||
route.with_openapi(info, |mut route| {
|
urls: vec![format!("http://{}", ADDR)]
|
||||||
route.resource::<Users>("users");
|
};
|
||||||
route.resource::<Auth>("auth");
|
route.with_openapi(info, |mut route| {
|
||||||
route.get_openapi("openapi");
|
route.resource::<Users>("users");
|
||||||
});
|
route.resource::<Auth>("auth");
|
||||||
}));
|
route.get_openapi("openapi");
|
||||||
|
route.swagger_ui("");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
println!("Gotham started on {} for testing", ADDR);
|
println!("Gotham started on {} for testing", ADDR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
rustfmt.toml
Normal file
19
rustfmt.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
edition = "2018"
|
||||||
|
max_width = 125
|
||||||
|
newline_style = "Unix"
|
||||||
|
unstable_features = true
|
||||||
|
|
||||||
|
# always use tabs.
|
||||||
|
hard_tabs = true
|
||||||
|
tab_spaces = 4
|
||||||
|
|
||||||
|
# commas inbetween but not after
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
trailing_comma = "Never"
|
||||||
|
|
||||||
|
# misc
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_try_shorthand = true
|
245
src/auth.rs
245
src/auth.rs
|
@ -1,29 +1,27 @@
|
||||||
use crate::{AuthError, Forbidden, HeaderName};
|
use crate::{AuthError, Forbidden};
|
||||||
|
|
||||||
use cookie::CookieJar;
|
use cookie::CookieJar;
|
||||||
use futures_util::{future, future::{FutureExt, TryFutureExt}};
|
use futures_util::{
|
||||||
|
future,
|
||||||
|
future::{FutureExt, TryFutureExt}
|
||||||
|
};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
anyhow,
|
||||||
handler::HandlerFuture,
|
handler::HandlerFuture,
|
||||||
hyper::header::{AUTHORIZATION, HeaderMap},
|
hyper::header::{HeaderMap, HeaderName, AUTHORIZATION},
|
||||||
middleware::{Middleware, NewMiddleware},
|
middleware::{cookie::CookieParser, Middleware, NewMiddleware},
|
||||||
state::{FromState, State}
|
state::{FromState, State}
|
||||||
};
|
};
|
||||||
use jsonwebtoken::{
|
use jsonwebtoken::{errors::ErrorKind, DecodingKey};
|
||||||
errors::ErrorKind,
|
|
||||||
DecodingKey
|
|
||||||
};
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::{
|
use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin};
|
||||||
marker::PhantomData,
|
|
||||||
panic::RefUnwindSafe,
|
|
||||||
pin::Pin
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[doc(no_inline)]
|
||||||
pub use jsonwebtoken::Validation as AuthValidation;
|
pub use jsonwebtoken::Validation as AuthValidation;
|
||||||
|
|
||||||
/// The authentication status returned by the auth middleware for each request.
|
/// The authentication status returned by the auth middleware for each request.
|
||||||
#[derive(Debug, StateData)]
|
#[derive(Debug, StateData)]
|
||||||
pub enum AuthStatus<T : Send + 'static>
|
pub enum AuthStatus<T: Send + 'static> {
|
||||||
{
|
|
||||||
/// The auth status is unknown.
|
/// The auth status is unknown.
|
||||||
Unknown,
|
Unknown,
|
||||||
/// The request has been performed without any kind of authentication.
|
/// The request has been performed without any kind of authentication.
|
||||||
|
@ -38,10 +36,9 @@ pub enum AuthStatus<T : Send + 'static>
|
||||||
|
|
||||||
impl<T> Clone for AuthStatus<T>
|
impl<T> Clone for AuthStatus<T>
|
||||||
where
|
where
|
||||||
T : Clone + Send + 'static
|
T: Clone + Send + 'static
|
||||||
{
|
{
|
||||||
fn clone(&self) -> Self
|
fn clone(&self) -> Self {
|
||||||
{
|
|
||||||
match self {
|
match self {
|
||||||
Self::Unknown => Self::Unknown,
|
Self::Unknown => Self::Unknown,
|
||||||
Self::Unauthenticated => Self::Unauthenticated,
|
Self::Unauthenticated => Self::Unauthenticated,
|
||||||
|
@ -52,16 +49,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Copy for AuthStatus<T>
|
impl<T> Copy for AuthStatus<T> where T: Copy + Send + 'static {}
|
||||||
where
|
|
||||||
T : Copy + Send + 'static
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Send + 'static> AuthStatus<T>
|
impl<T: Send + 'static> AuthStatus<T> {
|
||||||
{
|
pub fn ok(self) -> Result<T, AuthError> {
|
||||||
pub fn ok(self) -> Result<T, AuthError>
|
|
||||||
{
|
|
||||||
match self {
|
match self {
|
||||||
Self::Authenticated(data) => Ok(data),
|
Self::Authenticated(data) => Ok(data),
|
||||||
_ => Err(Forbidden)
|
_ => Err(Forbidden)
|
||||||
|
@ -71,8 +62,7 @@ impl<T : Send + 'static> AuthStatus<T>
|
||||||
|
|
||||||
/// The source of the authentication token in the request.
|
/// The source of the authentication token in the request.
|
||||||
#[derive(Clone, Debug, StateData)]
|
#[derive(Clone, Debug, StateData)]
|
||||||
pub enum AuthSource
|
pub enum AuthSource {
|
||||||
{
|
|
||||||
/// Take the token from a cookie with the given name.
|
/// Take the token from a cookie with the given name.
|
||||||
Cookie(String),
|
Cookie(String),
|
||||||
/// Take the token from a header with the given name.
|
/// Take the token from a header with the given name.
|
||||||
|
@ -87,8 +77,9 @@ pub enum AuthSource
|
||||||
This trait will help the auth middleware to determine the validity of an authentication token.
|
This trait will help the auth middleware to determine the validity of an authentication token.
|
||||||
|
|
||||||
A very basic implementation could look like this:
|
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";
|
const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc";
|
||||||
|
|
||||||
|
@ -100,36 +91,29 @@ impl<T> AuthHandler<T> for CustomAuthHandler {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
pub trait AuthHandler<Data>
|
pub trait AuthHandler<Data> {
|
||||||
{
|
|
||||||
/// Return the SHA256-HMAC secret used to verify the JWT token.
|
/// Return the SHA256-HMAC secret used to verify the JWT token.
|
||||||
fn jwt_secret<F : FnOnce() -> Option<Data>>(&self, state : &mut State, decode_data : F) -> Option<Vec<u8>>;
|
fn jwt_secret<F: FnOnce() -> Option<Data>>(&self, state: &mut State, decode_data: F) -> Option<Vec<u8>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An `AuthHandler` returning always the same secret. See `AuthMiddleware` for a usage example.
|
/// An [AuthHandler] returning always the same secret. See [AuthMiddleware] for a usage example.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct StaticAuthHandler
|
pub struct StaticAuthHandler {
|
||||||
{
|
secret: Vec<u8>
|
||||||
secret : Vec<u8>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticAuthHandler
|
impl StaticAuthHandler {
|
||||||
{
|
pub fn from_vec(secret: Vec<u8>) -> Self {
|
||||||
pub fn from_vec(secret : Vec<u8>) -> Self
|
|
||||||
{
|
|
||||||
Self { secret }
|
Self { secret }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_array(secret : &[u8]) -> Self
|
pub fn from_array(secret: &[u8]) -> Self {
|
||||||
{
|
|
||||||
Self::from_vec(secret.to_vec())
|
Self::from_vec(secret.to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> AuthHandler<T> for StaticAuthHandler
|
impl<T> AuthHandler<T> for StaticAuthHandler {
|
||||||
{
|
fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, _decode_data: F) -> Option<Vec<u8>> {
|
||||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
|
|
||||||
{
|
|
||||||
Some(self.secret.clone())
|
Some(self.secret.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +138,7 @@ struct AuthData {
|
||||||
exp: u64
|
exp: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(AuthResource)]
|
#[read_all]
|
||||||
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
fn read_all(auth : &AuthStatus<AuthData>) -> Success<String> {
|
||||||
format!("{:?}", auth).into()
|
format!("{:?}", auth).into()
|
||||||
}
|
}
|
||||||
|
@ -173,19 +157,18 @@ fn main() {
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AuthMiddleware<Data, Handler>
|
pub struct AuthMiddleware<Data, Handler> {
|
||||||
{
|
source: AuthSource,
|
||||||
source : AuthSource,
|
validation: AuthValidation,
|
||||||
validation : AuthValidation,
|
handler: Handler,
|
||||||
handler : Handler,
|
_data: PhantomData<Data>
|
||||||
_data : PhantomData<Data>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Data, Handler> Clone for AuthMiddleware<Data, Handler>
|
impl<Data, Handler> Clone for AuthMiddleware<Data, Handler>
|
||||||
where Handler : Clone
|
where
|
||||||
|
Handler: Clone
|
||||||
{
|
{
|
||||||
fn clone(&self) -> Self
|
fn clone(&self) -> Self {
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
source: self.source.clone(),
|
source: self.source.clone(),
|
||||||
validation: self.validation.clone(),
|
validation: self.validation.clone(),
|
||||||
|
@ -197,11 +180,10 @@ where Handler : Clone
|
||||||
|
|
||||||
impl<Data, Handler> AuthMiddleware<Data, Handler>
|
impl<Data, Handler> AuthMiddleware<Data, Handler>
|
||||||
where
|
where
|
||||||
Data : DeserializeOwned + Send,
|
Data: DeserializeOwned + Send,
|
||||||
Handler : AuthHandler<Data> + Default
|
Handler: AuthHandler<Data> + Default
|
||||||
{
|
{
|
||||||
pub fn from_source(source : AuthSource) -> Self
|
pub fn from_source(source: AuthSource) -> Self {
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
source,
|
source,
|
||||||
validation: Default::default(),
|
validation: Default::default(),
|
||||||
|
@ -213,11 +195,10 @@ where
|
||||||
|
|
||||||
impl<Data, Handler> AuthMiddleware<Data, Handler>
|
impl<Data, Handler> AuthMiddleware<Data, Handler>
|
||||||
where
|
where
|
||||||
Data : DeserializeOwned + Send,
|
Data: DeserializeOwned + Send,
|
||||||
Handler : AuthHandler<Data>
|
Handler: AuthHandler<Data>
|
||||||
{
|
{
|
||||||
pub fn new(source : AuthSource, validation : AuthValidation, handler : Handler) -> Self
|
pub fn new(source: AuthSource, validation: AuthValidation, handler: Handler) -> Self {
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
source,
|
source,
|
||||||
validation,
|
validation,
|
||||||
|
@ -226,28 +207,25 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_status(&self, state : &mut State) -> AuthStatus<Data>
|
fn auth_status(&self, state: &mut State) -> AuthStatus<Data> {
|
||||||
{
|
|
||||||
// extract the provided token, if any
|
// extract the provided token, if any
|
||||||
let token = match &self.source {
|
let token = match &self.source {
|
||||||
AuthSource::Cookie(name) => {
|
AuthSource::Cookie(name) => CookieJar::try_borrow_from(&state)
|
||||||
CookieJar::try_borrow_from(&state)
|
.map(|jar| jar.get(&name).map(|cookie| cookie.value().to_owned()))
|
||||||
.and_then(|jar| jar.get(&name))
|
.unwrap_or_else(|| {
|
||||||
.map(|cookie| cookie.value().to_owned())
|
CookieParser::from_state(&state)
|
||||||
},
|
.get(&name)
|
||||||
AuthSource::Header(name) => {
|
.map(|cookie| cookie.value().to_owned())
|
||||||
HeaderMap::try_borrow_from(&state)
|
}),
|
||||||
.and_then(|map| map.get(name))
|
AuthSource::Header(name) => HeaderMap::try_borrow_from(&state)
|
||||||
.and_then(|header| header.to_str().ok())
|
.and_then(|map| map.get(name))
|
||||||
.map(|value| value.to_owned())
|
.and_then(|header| header.to_str().ok())
|
||||||
},
|
.map(|value| value.to_owned()),
|
||||||
AuthSource::AuthorizationHeader => {
|
AuthSource::AuthorizationHeader => HeaderMap::try_borrow_from(&state)
|
||||||
HeaderMap::try_borrow_from(&state)
|
.and_then(|map| map.get(AUTHORIZATION))
|
||||||
.and_then(|map| map.get(AUTHORIZATION))
|
.and_then(|header| header.to_str().ok())
|
||||||
.and_then(|header| header.to_str().ok())
|
.and_then(|value| value.split_whitespace().nth(1))
|
||||||
.and_then(|value| value.split_whitespace().nth(1))
|
.map(|value| value.to_owned())
|
||||||
.map(|value| value.to_owned())
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// unauthed if no token
|
// unauthed if no token
|
||||||
|
@ -270,7 +248,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
// validate the token
|
// validate the token
|
||||||
let data : Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) {
|
let data: Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) {
|
||||||
Ok(data) => data.claims,
|
Ok(data) => data.claims,
|
||||||
Err(e) => match dbg!(e.into_kind()) {
|
Err(e) => match dbg!(e.into_kind()) {
|
||||||
ErrorKind::ExpiredSignature => return AuthStatus::Expired,
|
ErrorKind::ExpiredSignature => return AuthStatus::Expired,
|
||||||
|
@ -285,12 +263,12 @@ where
|
||||||
|
|
||||||
impl<Data, Handler> Middleware for AuthMiddleware<Data, Handler>
|
impl<Data, Handler> Middleware for AuthMiddleware<Data, Handler>
|
||||||
where
|
where
|
||||||
Data : DeserializeOwned + Send + 'static,
|
Data: DeserializeOwned + Send + 'static,
|
||||||
Handler : AuthHandler<Data>
|
Handler: AuthHandler<Data>
|
||||||
{
|
{
|
||||||
fn call<Chain>(self, mut state : State, chain : Chain) -> Pin<Box<HandlerFuture>>
|
fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
|
||||||
where
|
where
|
||||||
Chain : FnOnce(State) -> Pin<Box<HandlerFuture>>
|
Chain: FnOnce(State) -> Pin<Box<HandlerFuture>>
|
||||||
{
|
{
|
||||||
// put the source in our state, required for e.g. openapi
|
// put the source in our state, required for e.g. openapi
|
||||||
state.put(self.source.clone());
|
state.put(self.source.clone());
|
||||||
|
@ -306,26 +284,25 @@ where
|
||||||
|
|
||||||
impl<Data, Handler> NewMiddleware for AuthMiddleware<Data, Handler>
|
impl<Data, Handler> NewMiddleware for AuthMiddleware<Data, Handler>
|
||||||
where
|
where
|
||||||
Self : Clone + Middleware + Sync + RefUnwindSafe
|
Self: Clone + Middleware + Sync + RefUnwindSafe
|
||||||
{
|
{
|
||||||
type Instance = Self;
|
type Instance = Self;
|
||||||
|
|
||||||
fn new_middleware(&self) -> Result<Self::Instance, std::io::Error>
|
fn new_middleware(&self) -> anyhow::Result<Self> {
|
||||||
{
|
let c: Self = self.clone();
|
||||||
let c : Self = self.clone();
|
|
||||||
Ok(c)
|
Ok(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test
|
mod test {
|
||||||
{
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
|
use gotham::hyper::header::COOKIE;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
// 256-bit random string
|
// 256-bit random string
|
||||||
const JWT_SECRET : &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW";
|
const JWT_SECRET: &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW";
|
||||||
|
|
||||||
// some known tokens
|
// some known tokens
|
||||||
const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk";
|
const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk";
|
||||||
|
@ -333,18 +310,15 @@ mod test
|
||||||
const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9";
|
const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, PartialEq)]
|
#[derive(Debug, Deserialize, PartialEq)]
|
||||||
struct TestData
|
struct TestData {
|
||||||
{
|
iss: String,
|
||||||
iss : String,
|
sub: String,
|
||||||
sub : String,
|
iat: u64,
|
||||||
iat : u64,
|
exp: u64
|
||||||
exp : u64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TestData
|
impl Default for TestData {
|
||||||
{
|
fn default() -> Self {
|
||||||
fn default() -> Self
|
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
iss: "msrd0".to_owned(),
|
iss: "msrd0".to_owned(),
|
||||||
sub: "gotham-restful".to_owned(),
|
sub: "gotham-restful".to_owned(),
|
||||||
|
@ -356,17 +330,14 @@ mod test
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct NoneAuthHandler;
|
struct NoneAuthHandler;
|
||||||
impl<T> AuthHandler<T> for NoneAuthHandler
|
impl<T> AuthHandler<T> for NoneAuthHandler {
|
||||||
{
|
fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, _decode_data: F) -> Option<Vec<u8>> {
|
||||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, _decode_data : F) -> Option<Vec<u8>>
|
|
||||||
{
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_none_secret()
|
fn test_auth_middleware_none_secret() {
|
||||||
{
|
|
||||||
let middleware = <AuthMiddleware<TestData, NoneAuthHandler>>::from_source(AuthSource::AuthorizationHeader);
|
let middleware = <AuthMiddleware<TestData, NoneAuthHandler>>::from_source(AuthSource::AuthorizationHeader);
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
@ -379,18 +350,17 @@ mod test
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct TestAssertingHandler;
|
struct TestAssertingHandler;
|
||||||
impl<T> AuthHandler<T> for TestAssertingHandler
|
impl<T> AuthHandler<T> for TestAssertingHandler
|
||||||
where T : Debug + Default + PartialEq
|
where
|
||||||
|
T: Debug + Default + PartialEq
|
||||||
{
|
{
|
||||||
fn jwt_secret<F : FnOnce() -> Option<T>>(&self, _state : &mut State, decode_data : F) -> Option<Vec<u8>>
|
fn jwt_secret<F: FnOnce() -> Option<T>>(&self, _state: &mut State, decode_data: F) -> Option<Vec<u8>> {
|
||||||
{
|
|
||||||
assert_eq!(decode_data(), Some(T::default()));
|
assert_eq!(decode_data(), Some(T::default()));
|
||||||
Some(JWT_SECRET.to_vec())
|
Some(JWT_SECRET.to_vec())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_decode_data()
|
fn test_auth_middleware_decode_data() {
|
||||||
{
|
|
||||||
let middleware = <AuthMiddleware<TestData, TestAssertingHandler>>::from_source(AuthSource::AuthorizationHeader);
|
let middleware = <AuthMiddleware<TestData, TestAssertingHandler>>::from_source(AuthSource::AuthorizationHeader);
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
@ -400,15 +370,15 @@ mod test
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_middleware<T>(source : AuthSource) -> AuthMiddleware<T, StaticAuthHandler>
|
fn new_middleware<T>(source: AuthSource) -> AuthMiddleware<T, StaticAuthHandler>
|
||||||
where T : DeserializeOwned + Send
|
where
|
||||||
|
T: DeserializeOwned + Send
|
||||||
{
|
{
|
||||||
AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET))
|
AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_no_token()
|
fn test_auth_middleware_no_token() {
|
||||||
{
|
|
||||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
let status = middleware.auth_status(&mut state);
|
let status = middleware.auth_status(&mut state);
|
||||||
|
@ -420,8 +390,7 @@ mod test
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_expired_token()
|
fn test_auth_middleware_expired_token() {
|
||||||
{
|
|
||||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
@ -436,8 +405,7 @@ mod test
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_invalid_token()
|
fn test_auth_middleware_invalid_token() {
|
||||||
{
|
|
||||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
@ -452,8 +420,7 @@ mod test
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_auth_header_token()
|
fn test_auth_middleware_auth_header_token() {
|
||||||
{
|
|
||||||
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
let middleware = new_middleware::<TestData>(AuthSource::AuthorizationHeader);
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
|
@ -468,8 +435,7 @@ mod test
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_header_token()
|
fn test_auth_middleware_header_token() {
|
||||||
{
|
|
||||||
let header_name = "x-znoiprwmvfexju";
|
let header_name = "x-znoiprwmvfexju";
|
||||||
let middleware = new_middleware::<TestData>(AuthSource::Header(HeaderName::from_static(header_name)));
|
let middleware = new_middleware::<TestData>(AuthSource::Header(HeaderName::from_static(header_name)));
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
|
@ -485,8 +451,7 @@ mod test
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auth_middleware_cookie_token()
|
fn test_auth_middleware_cookie_token() {
|
||||||
{
|
|
||||||
let cookie_name = "znoiprwmvfexju";
|
let cookie_name = "znoiprwmvfexju";
|
||||||
let middleware = new_middleware::<TestData>(AuthSource::Cookie(cookie_name.to_owned()));
|
let middleware = new_middleware::<TestData>(AuthSource::Cookie(cookie_name.to_owned()));
|
||||||
State::with_new(|mut state| {
|
State::with_new(|mut state| {
|
||||||
|
@ -500,4 +465,20 @@ mod test
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_auth_middleware_cookie_no_jar() {
|
||||||
|
let cookie_name = "znoiprwmvfexju";
|
||||||
|
let middleware = new_middleware::<TestData>(AuthSource::Cookie(cookie_name.to_owned()));
|
||||||
|
State::with_new(|mut state| {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(COOKIE, format!("{}={}", cookie_name, VALID_TOKEN).parse().unwrap());
|
||||||
|
state.put(headers);
|
||||||
|
let status = middleware.auth_status(&mut state);
|
||||||
|
match status {
|
||||||
|
AuthStatus::Authenticated(data) => assert_eq!(data, TestData::default()),
|
||||||
|
_ => panic!("Expected AuthStatus::Authenticated, got {:?}", status)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
215
src/cors.rs
215
src/cors.rs
|
@ -1,25 +1,23 @@
|
||||||
use crate::matcher::AccessControlRequestMethodMatcher;
|
|
||||||
use gotham::{
|
use gotham::{
|
||||||
handler::HandlerFuture,
|
handler::HandlerFuture,
|
||||||
helpers::http::response::create_empty_response,
|
helpers::http::response::create_empty_response,
|
||||||
hyper::{
|
hyper::{
|
||||||
header::{
|
header::{
|
||||||
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
|
HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY,
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE,
|
||||||
HeaderMap, HeaderName, HeaderValue
|
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY
|
||||||
},
|
},
|
||||||
Body, Method, Response, StatusCode
|
Body, Method, Response, StatusCode
|
||||||
},
|
},
|
||||||
middleware::Middleware,
|
middleware::Middleware,
|
||||||
pipeline::chain::PipelineHandleChain,
|
pipeline::chain::PipelineHandleChain,
|
||||||
router::builder::*,
|
router::{
|
||||||
state::{FromState, State},
|
builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher},
|
||||||
};
|
route::matcher::AccessControlRequestMethodMatcher
|
||||||
use itertools::Itertools;
|
},
|
||||||
use std::{
|
state::{FromState, State}
|
||||||
panic::RefUnwindSafe,
|
|
||||||
pin::Pin
|
|
||||||
};
|
};
|
||||||
|
use std::{panic::RefUnwindSafe, pin::Pin};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Specify the allowed origins of the request. It is up to the browser to check the validity of the
|
Specify the allowed origins of the request. It is up to the browser to check the validity of the
|
||||||
|
@ -27,8 +25,7 @@ origin. This, when sent to the browser, will indicate whether or not the request
|
||||||
allowed to make the request.
|
allowed to make the request.
|
||||||
*/
|
*/
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Origin
|
pub enum Origin {
|
||||||
{
|
|
||||||
/// Do not send any `Access-Control-Allow-Origin` headers.
|
/// Do not send any `Access-Control-Allow-Origin` headers.
|
||||||
None,
|
None,
|
||||||
/// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials.
|
/// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials.
|
||||||
|
@ -39,19 +36,15 @@ pub enum Origin
|
||||||
Copy
|
Copy
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Origin
|
impl Default for Origin {
|
||||||
{
|
fn default() -> Self {
|
||||||
fn default() -> Self
|
|
||||||
{
|
|
||||||
Self::None
|
Self::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Origin
|
impl Origin {
|
||||||
{
|
|
||||||
/// Get the header value for the `Access-Control-Allow-Origin` header.
|
/// Get the header value for the `Access-Control-Allow-Origin` header.
|
||||||
fn header_value(&self, state : &State) -> Option<HeaderValue>
|
fn header_value(&self, state: &State) -> Option<HeaderValue> {
|
||||||
{
|
|
||||||
match self {
|
match self {
|
||||||
Self::None => None,
|
Self::None => None,
|
||||||
Self::Star => Some("*".parse().unwrap()),
|
Self::Star => Some("*".parse().unwrap()),
|
||||||
|
@ -62,17 +55,64 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This is the configuration that the CORS handler will follow. Its default configuration is basically
|
This is the configuration that the CORS handler will follow. Its default configuration is basically
|
||||||
not to touch any responses, resulting in the browser's default behaviour.
|
not to touch any responses, resulting in the browser's default behaviour.
|
||||||
|
|
||||||
To change settings, you need to put this type into gotham's [`State`]:
|
To change settings, you need to put this type into gotham's [State]:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
# 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() {
|
fn main() {
|
||||||
let cors = CorsConfig {
|
let cors = CorsConfig {
|
||||||
origin: Origin::Star,
|
origin: Origin::Star,
|
||||||
|
@ -90,7 +130,7 @@ configurations for different scopes, you need to register the middleware inside
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State};
|
# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State};
|
||||||
# use gotham_restful::*;
|
# use gotham_restful::{*, cors::Origin};
|
||||||
let pipelines = new_pipeline_set();
|
let pipelines = new_pipeline_set();
|
||||||
|
|
||||||
// The first cors configuration
|
// The first cors configuration
|
||||||
|
@ -122,27 +162,23 @@ gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
```
|
```
|
||||||
|
|
||||||
[`State`]: ../gotham/state/struct.State.html
|
|
||||||
*/
|
*/
|
||||||
#[derive(Clone, Debug, Default, NewMiddleware, StateData)]
|
#[derive(Clone, Debug, Default, NewMiddleware, StateData)]
|
||||||
pub struct CorsConfig
|
pub struct CorsConfig {
|
||||||
{
|
|
||||||
/// The allowed origins.
|
/// The allowed origins.
|
||||||
pub origin : Origin,
|
pub origin: Origin,
|
||||||
/// The allowed headers.
|
/// The allowed headers.
|
||||||
pub headers : Vec<HeaderName>,
|
pub headers: Headers,
|
||||||
/// The amount of seconds that the preflight request can be cached.
|
/// The amount of seconds that the preflight request can be cached.
|
||||||
pub max_age : u64,
|
pub max_age: u64,
|
||||||
/// Whether or not the request may be made with supplying credentials.
|
/// Whether or not the request may be made with supplying credentials.
|
||||||
pub credentials : bool
|
pub credentials: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Middleware for CorsConfig
|
impl Middleware for CorsConfig {
|
||||||
{
|
fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
|
||||||
fn call<Chain>(self, mut state : State, chain : Chain) -> Pin<Box<HandlerFuture>>
|
|
||||||
where
|
where
|
||||||
Chain : FnOnce(State) -> Pin<Box<HandlerFuture>>
|
Chain: FnOnce(State) -> Pin<Box<HandlerFuture>>
|
||||||
{
|
{
|
||||||
state.put(self);
|
state.put(self);
|
||||||
chain(state)
|
chain(state)
|
||||||
|
@ -151,38 +187,35 @@ impl Middleware for CorsConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that
|
Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that
|
||||||
the response is aligned with the `state`'s [`CorsConfig`].
|
the response is aligned with the `state`'s [CorsConfig].
|
||||||
|
|
||||||
If you are using the [`Resource`] type (which is the recommended way), you'll never have to call
|
If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never
|
||||||
this method. However, if you are writing your own handler method, you might want to call this
|
have to call this method. However, if you are writing your own handler method, you might want to
|
||||||
after your request to add the required CORS headers.
|
call this after your request to add the required CORS headers.
|
||||||
|
|
||||||
For further information on CORS, read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS.
|
For further information on CORS, read
|
||||||
|
[https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
|
||||||
[`CorsConfig`]: ./struct.CorsConfig.html
|
|
||||||
*/
|
*/
|
||||||
pub fn handle_cors(state : &State, res : &mut Response<Body>)
|
pub fn handle_cors(state: &State, res: &mut Response<Body>) {
|
||||||
{
|
|
||||||
let config = CorsConfig::try_borrow_from(state);
|
let config = CorsConfig::try_borrow_from(state);
|
||||||
let headers = res.headers_mut();
|
if let Some(cfg) = config {
|
||||||
|
let headers = res.headers_mut();
|
||||||
|
|
||||||
// non-preflight requests require the Access-Control-Allow-Origin header
|
// 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);
|
||||||
headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if the origin is copied over, we should tell the browser by specifying the Vary 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))
|
if cfg.origin.varies() {
|
||||||
{
|
let vary = headers.get(VARY).map(|vary| format!("{},origin", vary.to_str().unwrap()));
|
||||||
let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap()));
|
headers.insert(VARY, vary.as_deref().unwrap_or("origin").parse().unwrap());
|
||||||
headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if we allow credentials, tell the browser
|
// if we allow credentials, tell the browser
|
||||||
if config.map(|cfg| cfg.credentials).unwrap_or(false)
|
if cfg.credentials {
|
||||||
{
|
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
|
||||||
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap());
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,44 +239,49 @@ pub fn handle_cors(state : &State, res : &mut Response<Body>)
|
||||||
/// ```
|
/// ```
|
||||||
pub trait CorsRoute<C, P>
|
pub trait CorsRoute<C, P>
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
/// Handle a preflight request on `path` for `method`. To configure the behaviour, use
|
/// Handle a preflight request on `path` for `method`. To configure the behaviour, use
|
||||||
/// [`CorsConfig`](struct.CorsConfig.html).
|
/// [CorsConfig].
|
||||||
fn cors(&mut self, path : &str, method : Method);
|
fn cors(&mut self, path: &str, method: Method);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cors_preflight_handler(state : State) -> (State, Response<Body>)
|
pub(crate) fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
||||||
{
|
|
||||||
let config = CorsConfig::try_borrow_from(&state);
|
let config = CorsConfig::try_borrow_from(&state);
|
||||||
|
|
||||||
// prepare the response
|
// prepare the response
|
||||||
let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
|
let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
|
||||||
let headers = res.headers_mut();
|
let headers = res.headers_mut();
|
||||||
|
let mut vary: Vec<HeaderName> = Vec::new();
|
||||||
|
|
||||||
// copy the request method over to the response
|
// copy the request method over to the response
|
||||||
let method = HeaderMap::borrow_from(&state).get(ACCESS_CONTROL_REQUEST_METHOD).unwrap().clone();
|
let method = HeaderMap::borrow_from(&state)
|
||||||
|
.get(ACCESS_CONTROL_REQUEST_METHOD)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method);
|
headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method);
|
||||||
|
vary.push(ACCESS_CONTROL_REQUEST_METHOD);
|
||||||
|
|
||||||
// if we allow any headers, put them in
|
if let Some(cfg) = config {
|
||||||
if let Some(hdrs) = config.map(|cfg| &cfg.headers)
|
// if we allow any headers, copy them over
|
||||||
{
|
if let Some(header) = cfg.headers.header_value(&state) {
|
||||||
if hdrs.len() > 0
|
headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, header);
|
||||||
{
|
}
|
||||||
// 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 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// 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);
|
handle_cors(&state, &mut res);
|
||||||
(state, res)
|
(state, res)
|
||||||
|
@ -251,15 +289,12 @@ fn cors_preflight_handler(state : State) -> (State, Response<Body>)
|
||||||
|
|
||||||
impl<D, C, P> CorsRoute<C, P> for D
|
impl<D, C, P> CorsRoute<C, P> for D
|
||||||
where
|
where
|
||||||
D : DrawRoutes<C, P>,
|
D: DrawRoutes<C, P>,
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn cors(&mut self, path : &str, method : Method)
|
fn cors(&mut self, path: &str, method: Method) {
|
||||||
{
|
|
||||||
let matcher = AccessControlRequestMethodMatcher::new(method);
|
let matcher = AccessControlRequestMethodMatcher::new(method);
|
||||||
self.options(path)
|
self.options(path).extend_route_matcher(matcher).to(cors_preflight_handler);
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.to(cors_preflight_handler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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)
|
||||||
|
}
|
||||||
|
}
|
377
src/lib.rs
377
src/lib.rs
|
@ -1,35 +1,52 @@
|
||||||
#![allow(clippy::tabs_in_doc_comments)]
|
|
||||||
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||||
#![deny(intra_doc_link_resolution_failure)]
|
#![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
|
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
||||||
create resources with assigned methods that aim to be a more convenient way of creating handlers
|
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
|
||||||
for requests.
|
for requests.
|
||||||
|
|
||||||
# Design Goals
|
# Features
|
||||||
|
|
||||||
This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as
|
- Automatically parse **JSON** request and produce response bodies
|
||||||
request/response bodies and does that in a RESTful way, this framework is probably a bad fit for
|
- Allow using **raw** request and response bodies
|
||||||
your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful
|
- Convenient **macros** to create responses that can be registered with gotham's router
|
||||||
web server in Rust as convenient as possible with the least amount of boilerplate neccessary.
|
- 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
|
||||||
|
|
||||||
# Methods
|
# Safety
|
||||||
|
|
||||||
Assuming you assign `/foobar` to your resource, you can implement the following methods:
|
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.
|
||||||
|
|
||||||
| Method Name | Required Arguments | HTTP Verb | HTTP Path |
|
# Endpoints
|
||||||
| ----------- | ------------------ | --------- | ----------- |
|
|
||||||
| 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
|
There are a set of pre-defined endpoints that should cover the majority of REST APIs. However,
|
||||||
simple example could look like this:
|
it is also possible to define your own endpoints.
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
| 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 endpoints has a macro that creates the neccessary boilerplate for the Resource. A
|
||||||
|
simple example looks like this:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -41,15 +58,15 @@ simple example could look like this:
|
||||||
#[resource(read)]
|
#[resource(read)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
/// The return type of the foo read method.
|
/// The return type of the foo read endpoint.
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
struct Foo {
|
struct Foo {
|
||||||
id: u64
|
id: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The foo read method handler.
|
/// The foo read endpoint.
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
fn read(id: u64) -> Success<Foo> {
|
fn read(id: u64) -> Success<Foo> {
|
||||||
Foo { id }.into()
|
Foo { id }.into()
|
||||||
}
|
}
|
||||||
|
@ -60,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
|
# Arguments
|
||||||
|
|
||||||
Some methods require arguments. Those should be
|
Some endpoints require arguments. Those should be
|
||||||
* **id** Should be a deserializable json-primitive like `i64` or `String`.
|
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
|
||||||
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||||
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
||||||
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
||||||
type needs to implement [`QueryStringExtractor`].
|
type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor).
|
||||||
|
|
||||||
Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to
|
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
|
||||||
have an async handler (that is, the function that the method macro is invoked on is declared
|
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
|
||||||
as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement
|
improve.
|
||||||
`Sync` there is unfortunately no more convenient way.
|
|
||||||
|
|
||||||
# Uploads and Downloads
|
# Uploads and Downloads
|
||||||
|
|
||||||
|
@ -96,7 +147,7 @@ struct RawImage {
|
||||||
content_type: Mime
|
content_type: Mime
|
||||||
}
|
}
|
||||||
|
|
||||||
#[create(ImageResource)]
|
#[create]
|
||||||
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||||
Raw::new(body.content, body.content_type)
|
Raw::new(body.content, body.content_type)
|
||||||
}
|
}
|
||||||
|
@ -107,20 +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
|
# Features
|
||||||
|
|
||||||
To make life easier for common use-cases, this create offers a few features that might be helpful
|
To make life easier for common use-cases, this create offers a few features that might be helpful
|
||||||
when you implement your web server.
|
when you implement your web server. The complete feature list is
|
||||||
|
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||||
|
- `chrono` openapi support for chrono types
|
||||||
|
- `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 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
|
## Authentication Feature
|
||||||
|
|
||||||
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
In order to enable authentication support, enable the `auth` feature gate. This allows you to
|
||||||
register a middleware that can automatically check for the existence of an JWT authentication
|
register a middleware that can automatically check for the existence of an JWT authentication
|
||||||
token. Besides being supported by the method macros, it supports to lookup the required JWT secret
|
token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret
|
||||||
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
||||||
None of this is currently supported by gotham's own JWT middleware.
|
None of this is currently supported by gotham's own JWT middleware.
|
||||||
|
|
||||||
A simple example that uses only a single secret could look like this:
|
A simple example that uses only a single secret looks like this:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -134,7 +225,7 @@ A simple example that uses only a single secret could look like this:
|
||||||
struct SecretResource;
|
struct SecretResource;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
struct Secret {
|
struct Secret {
|
||||||
id: u64,
|
id: u64,
|
||||||
intended_for: String
|
intended_for: String
|
||||||
|
@ -146,7 +237,7 @@ struct AuthData {
|
||||||
exp: u64
|
exp: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read(SecretResource)]
|
#[read]
|
||||||
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
fn read(auth: AuthStatus<AuthData>, id: u64) -> AuthSuccess<Secret> {
|
||||||
let intended_for = auth.ok()?.sub;
|
let intended_for = auth.ok()?.sub;
|
||||||
Ok(Secret { id, intended_for })
|
Ok(Secret { id, intended_for })
|
||||||
|
@ -173,20 +264,20 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a
|
||||||
configuration as a middleware.
|
configuration as a middleware.
|
||||||
|
|
||||||
A simple example that allows authentication from every origin (note that `*` always disallows
|
A simple example that allows authentication from every origin (note that `*` always disallows
|
||||||
authentication), and every content type, could look like this:
|
authentication), and every content type, looks like this:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
# #[cfg(feature = "cors")]
|
# #[cfg(feature = "cors")]
|
||||||
# mod cors_feature_enabled {
|
# mod cors_feature_enabled {
|
||||||
# use gotham::{hyper::header::*, router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
# 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};
|
# use serde::{Deserialize, Serialize};
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() {
|
fn read_all() {
|
||||||
// your handler
|
// your handler
|
||||||
}
|
}
|
||||||
|
@ -194,7 +285,7 @@ fn read_all() {
|
||||||
fn main() {
|
fn main() {
|
||||||
let cors = CorsConfig {
|
let cors = CorsConfig {
|
||||||
origin: Origin::Copy,
|
origin: Origin::Copy,
|
||||||
headers: vec![CONTENT_TYPE],
|
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||||
max_age: 0,
|
max_age: 0,
|
||||||
credentials: true
|
credentials: true
|
||||||
};
|
};
|
||||||
|
@ -216,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,
|
to run async code while holding a database connection. If you need to combine async and database,
|
||||||
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
|
you'll need to borrow the connection from the [`State`] yourself and return a boxed future.
|
||||||
|
|
||||||
A simple non-async example could look like this:
|
A simple non-async example looks like this:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
# #[macro_use] extern crate diesel;
|
# #[macro_use] extern crate diesel;
|
||||||
|
@ -240,13 +331,13 @@ A simple non-async example could look like this:
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[derive(Queryable, Serialize)]
|
#[derive(Queryable, Serialize)]
|
||||||
# #[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
struct Foo {
|
struct Foo {
|
||||||
id: i64,
|
id: i64,
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||||
foo::table.load(conn)
|
foo::table.load(conn)
|
||||||
}
|
}
|
||||||
|
@ -265,52 +356,120 @@ fn main() {
|
||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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`[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;
|
||||||
|
# #[cfg(feature = "openapi")]
|
||||||
|
# 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)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[derive(OpenapiType, Serialize)]
|
||||||
|
struct Foo {
|
||||||
|
bar: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
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`.
|
||||||
|
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, 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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate gotham_restful;
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
|
struct Foo;
|
||||||
|
```
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
There is a lack of good examples, but there is currently a collection of code in the [example]
|
This readme and the crate documentation contain some of example. In addition to that, there is
|
||||||
directory, that might help you. Any help writing more examples is highly appreciated.
|
a collection of code in the [example] directory that might help you. Any help writing more
|
||||||
|
examples is highly appreciated.
|
||||||
# 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/
|
[diesel]: https://diesel.rs/
|
||||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||||
[gotham]: https://gotham.rs/
|
[gotham]: https://gotham.rs/
|
||||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
||||||
[`CorsRoute`]: trait.CorsRoute.html
|
[`State`]: gotham::state::State
|
||||||
[`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html
|
|
||||||
[`RequestBody`]: trait.RequestBody.html
|
|
||||||
[`State`]: ../gotham/state/struct.State.html
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#[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
|
// weird proc macro issue
|
||||||
extern crate self as gotham_restful;
|
extern crate self as gotham_restful;
|
||||||
|
|
||||||
#[macro_use] extern crate gotham_derive;
|
#[macro_use]
|
||||||
#[macro_use] extern crate log;
|
extern crate gotham_derive;
|
||||||
#[macro_use] extern crate serde;
|
#[macro_use]
|
||||||
|
extern crate gotham_restful_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde;
|
||||||
|
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
pub use gotham;
|
pub use gotham;
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
pub use gotham::{
|
|
||||||
hyper::{header::HeaderName, StatusCode},
|
|
||||||
state::{FromState, State}
|
|
||||||
};
|
|
||||||
#[doc(no_inline)]
|
|
||||||
pub use mime::Mime;
|
pub use mime::Mime;
|
||||||
|
|
||||||
pub use gotham_restful_derive::*;
|
pub use gotham_restful_derive::*;
|
||||||
|
|
||||||
/// Not public API
|
/// Not public API
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod export
|
pub mod private {
|
||||||
{
|
pub use crate::routing::PathExtractor as IdPlaceholder;
|
||||||
pub use futures_util::future::FutureExt;
|
|
||||||
|
pub use futures_util::future::{BoxFuture, FutureExt};
|
||||||
|
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
|
|
||||||
|
@ -320,77 +479,55 @@ pub mod export
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use indexmap::IndexMap;
|
pub use indexmap::IndexMap;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use openapi_type::{OpenapiSchema, OpenapiType};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
pub use openapiv3 as openapi;
|
pub use openapiv3 as openapi;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
mod auth;
|
mod auth;
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
pub use auth::{
|
pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler};
|
||||||
AuthHandler,
|
|
||||||
AuthMiddleware,
|
|
||||||
AuthSource,
|
|
||||||
AuthStatus,
|
|
||||||
AuthValidation,
|
|
||||||
StaticAuthHandler
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "cors")]
|
#[cfg(feature = "cors")]
|
||||||
mod cors;
|
pub mod cors;
|
||||||
#[cfg(feature = "cors")]
|
#[cfg(feature = "cors")]
|
||||||
pub use cors::{
|
pub use cors::{handle_cors, CorsConfig, CorsRoute};
|
||||||
handle_cors,
|
|
||||||
CorsConfig,
|
|
||||||
CorsRoute,
|
|
||||||
Origin
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod matcher;
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
mod openapi;
|
mod openapi;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use openapi::{
|
pub use openapi::{builder::OpenapiInfo, router::GetOpenapi};
|
||||||
builder::OpenapiInfo,
|
|
||||||
router::GetOpenapi,
|
|
||||||
types::{OpenapiSchema, OpenapiType}
|
|
||||||
};
|
|
||||||
|
|
||||||
mod resource;
|
mod endpoint;
|
||||||
pub use resource::{
|
#[cfg(feature = "openapi")]
|
||||||
Resource,
|
pub use endpoint::EndpointWithSchema;
|
||||||
ResourceMethod,
|
pub use endpoint::{Endpoint, NoopExtractor};
|
||||||
ResourceReadAll,
|
|
||||||
ResourceRead,
|
|
||||||
ResourceSearch,
|
|
||||||
ResourceCreate,
|
|
||||||
ResourceChangeAll,
|
|
||||||
ResourceChange,
|
|
||||||
ResourceRemoveAll,
|
|
||||||
ResourceRemove
|
|
||||||
};
|
|
||||||
|
|
||||||
mod response;
|
mod response;
|
||||||
pub use response::Response;
|
pub use response::{
|
||||||
|
AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent,
|
||||||
mod result;
|
Raw, Redirect, Response, Success
|
||||||
pub use result::{
|
|
||||||
AuthError,
|
|
||||||
AuthError::Forbidden,
|
|
||||||
AuthErrorOrOther,
|
|
||||||
AuthResult,
|
|
||||||
AuthSuccess,
|
|
||||||
IntoResponseError,
|
|
||||||
NoContent,
|
|
||||||
Raw,
|
|
||||||
ResourceResult,
|
|
||||||
Success
|
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use response::{IntoResponseWithSchema, ResponseSchema};
|
||||||
|
|
||||||
mod routing;
|
mod routing;
|
||||||
pub use routing::{DrawResources, DrawResourceRoutes};
|
pub use routing::{DrawResourceRoutes, DrawResources};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub use routing::WithOpenapi;
|
pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi};
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
||||||
|
/// This trait must be implemented for every resource. It allows you to register the different
|
||||||
|
/// endpoints that can be handled by this resource to be registered with the underlying router.
|
||||||
|
///
|
||||||
|
/// It is not recommended to implement this yourself, 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,106 +0,0 @@
|
||||||
use gotham::{
|
|
||||||
hyper::{header::{ACCESS_CONTROL_REQUEST_METHOD, HeaderMap}, Method, StatusCode},
|
|
||||||
router::{non_match::RouteNonMatch, route::matcher::RouteMatcher},
|
|
||||||
state::{FromState, State}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A route matcher that checks whether the value of the `Access-Control-Request-Method` header matches the defined value.
|
|
||||||
///
|
|
||||||
/// Usage:
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use gotham::{helpers::http::response::create_empty_response,
|
|
||||||
/// # hyper::{header::ACCESS_CONTROL_ALLOW_METHODS, Method, StatusCode},
|
|
||||||
/// # router::builder::*
|
|
||||||
/// # };
|
|
||||||
/// # use gotham_restful::matcher::AccessControlRequestMethodMatcher;
|
|
||||||
/// let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
|
|
||||||
///
|
|
||||||
/// # build_simple_router(|route| {
|
|
||||||
/// // use the matcher for your request
|
|
||||||
/// route.options("/foo")
|
|
||||||
/// .extend_route_matcher(matcher)
|
|
||||||
/// .to(|state| {
|
|
||||||
/// // we know that this is a CORS preflight for a PUT request
|
|
||||||
/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
|
|
||||||
/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap());
|
|
||||||
/// (state, res)
|
|
||||||
/// });
|
|
||||||
/// # });
|
|
||||||
/// ```
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct AccessControlRequestMethodMatcher
|
|
||||||
{
|
|
||||||
method : Method
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccessControlRequestMethodMatcher
|
|
||||||
{
|
|
||||||
/// Construct a new matcher that matches if the `Access-Control-Request-Method` header matches `method`.
|
|
||||||
/// Note that during matching the method is normalized according to the fetch specification, that is,
|
|
||||||
/// byte-uppercased. This means that when using a custom `method` instead of a predefined one, make sure
|
|
||||||
/// it is uppercased or this matcher will never succeed.
|
|
||||||
pub fn new(method : Method) -> Self
|
|
||||||
{
|
|
||||||
Self { method }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouteMatcher for AccessControlRequestMethodMatcher
|
|
||||||
{
|
|
||||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
|
||||||
{
|
|
||||||
// according to the fetch specification, methods should be normalized by byte-uppercase
|
|
||||||
// https://fetch.spec.whatwg.org/#concept-method
|
|
||||||
match HeaderMap::borrow_from(state).get(ACCESS_CONTROL_REQUEST_METHOD)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.and_then(|str| str.to_ascii_uppercase().parse::<Method>().ok())
|
|
||||||
{
|
|
||||||
Some(m) if m == self.method => Ok(()),
|
|
||||||
_ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test
|
|
||||||
{
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn with_state<F>(accept : Option<&str>, block : F)
|
|
||||||
where F : FnOnce(&mut State) -> ()
|
|
||||||
{
|
|
||||||
State::with_new(|state| {
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
if let Some(acc) = accept
|
|
||||||
{
|
|
||||||
headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap());
|
|
||||||
}
|
|
||||||
state.put(headers);
|
|
||||||
block(state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_acrm_header()
|
|
||||||
{
|
|
||||||
let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
|
|
||||||
with_state(None, |state| assert!(matcher.is_match(&state).is_err()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn correct_acrm_header()
|
|
||||||
{
|
|
||||||
let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
|
|
||||||
with_state(Some("PUT"), |state| assert!(matcher.is_match(&state).is_ok()));
|
|
||||||
with_state(Some("put"), |state| assert!(matcher.is_match(&state).is_ok()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn incorrect_acrm_header()
|
|
||||||
{
|
|
||||||
let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
|
|
||||||
with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
mod access_control_request_method;
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
pub use access_control_request_method::AccessControlRequestMethodMatcher;
|
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
use crate::{OpenapiType, OpenapiSchema};
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use openapi_type::OpenapiSchema;
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema,
|
Components, OpenAPI, PathItem, ReferenceOr,
|
||||||
Server
|
ReferenceOr::{Item, Reference},
|
||||||
|
Schema, Server
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OpenapiInfo
|
pub struct OpenapiInfo {
|
||||||
{
|
pub title: String,
|
||||||
pub title : String,
|
pub version: String,
|
||||||
pub version : String,
|
pub urls: Vec<String>
|
||||||
pub urls : Vec<String>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OpenapiBuilder
|
pub struct OpenapiBuilder {
|
||||||
{
|
pub openapi: Arc<RwLock<OpenAPI>>
|
||||||
pub openapi : Arc<RwLock<OpenAPI>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenapiBuilder
|
impl OpenapiBuilder {
|
||||||
{
|
pub fn new(info: OpenapiInfo) -> Self {
|
||||||
pub fn new(info : OpenapiInfo) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
openapi: Arc::new(RwLock::new(OpenAPI {
|
openapi: Arc::new(RwLock::new(OpenAPI {
|
||||||
openapi: "3.0.2".to_string(),
|
openapi: "3.0.2".to_string(),
|
||||||
|
@ -32,8 +29,13 @@ impl OpenapiBuilder
|
||||||
version: info.version,
|
version: info.version,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
servers: info.urls.into_iter()
|
servers: info
|
||||||
.map(|url| Server { url, ..Default::default() })
|
.urls
|
||||||
|
.into_iter()
|
||||||
|
.map(|url| Server {
|
||||||
|
url,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
|
@ -42,8 +44,7 @@ impl OpenapiBuilder
|
||||||
|
|
||||||
/// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to
|
/// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to
|
||||||
/// modify the path and add it back after the modification
|
/// modify the path and add it back after the modification
|
||||||
pub fn remove_path(&mut self, path : &str) -> PathItem
|
pub fn remove_path(&mut self, path: &str) -> PathItem {
|
||||||
{
|
|
||||||
let mut openapi = self.openapi.write().unwrap();
|
let mut openapi = self.openapi.write().unwrap();
|
||||||
match openapi.paths.swap_remove(path) {
|
match openapi.paths.swap_remove(path) {
|
||||||
Some(Item(item)) => item,
|
Some(Item(item)) => item,
|
||||||
|
@ -51,14 +52,12 @@ impl OpenapiBuilder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem)
|
pub fn add_path<Path: ToString>(&mut self, path: Path, item: PathItem) {
|
||||||
{
|
|
||||||
let mut openapi = self.openapi.write().unwrap();
|
let mut openapi = self.openapi.write().unwrap();
|
||||||
openapi.paths.insert(path.to_string(), Item(item));
|
openapi.paths.insert(path.to_string(), Item(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema)
|
fn add_schema_impl(&mut self, name: String, mut schema: OpenapiSchema) {
|
||||||
{
|
|
||||||
self.add_schema_dependencies(&mut schema.dependencies);
|
self.add_schema_dependencies(&mut schema.dependencies);
|
||||||
|
|
||||||
let mut openapi = self.openapi.write().unwrap();
|
let mut openapi = self.openapi.write().unwrap();
|
||||||
|
@ -74,25 +73,22 @@ impl OpenapiBuilder
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>)
|
fn add_schema_dependencies(&mut self, dependencies: &mut IndexMap<String, OpenapiSchema>) {
|
||||||
{
|
let keys: Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
|
||||||
let keys : Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
|
for dep in keys {
|
||||||
for dep in keys
|
|
||||||
{
|
|
||||||
let dep_schema = dependencies.swap_remove(&dep);
|
let dep_schema = dependencies.swap_remove(&dep);
|
||||||
if let Some(dep_schema) = dep_schema
|
if let Some(dep_schema) = dep_schema {
|
||||||
{
|
|
||||||
self.add_schema_impl(dep, dep_schema);
|
self.add_schema_impl(dep, dep_schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_schema<T : OpenapiType>(&mut self) -> ReferenceOr<Schema>
|
pub fn add_schema(&mut self, mut schema: OpenapiSchema) -> ReferenceOr<Schema> {
|
||||||
{
|
|
||||||
let mut schema = T::schema();
|
|
||||||
match schema.name.clone() {
|
match schema.name.clone() {
|
||||||
Some(name) => {
|
Some(name) => {
|
||||||
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
|
let reference = Reference {
|
||||||
|
reference: format!("#/components/schemas/{}", name)
|
||||||
|
};
|
||||||
self.add_schema_impl(name, schema);
|
self.add_schema_impl(name, schema);
|
||||||
reference
|
reference
|
||||||
},
|
},
|
||||||
|
@ -104,27 +100,23 @@ impl OpenapiBuilder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod test
|
mod test {
|
||||||
{
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
#[derive(OpenapiType)]
|
||||||
struct Message
|
struct Message {
|
||||||
{
|
msg: String
|
||||||
msg : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
#[derive(OpenapiType)]
|
||||||
struct Messages
|
struct Messages {
|
||||||
{
|
msgs: Vec<Message>
|
||||||
msgs : Vec<Message>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info() -> OpenapiInfo
|
fn info() -> OpenapiInfo {
|
||||||
{
|
|
||||||
OpenapiInfo {
|
OpenapiInfo {
|
||||||
title: "TEST CASE".to_owned(),
|
title: "TEST CASE".to_owned(),
|
||||||
version: "1.2.3".to_owned(),
|
version: "1.2.3".to_owned(),
|
||||||
|
@ -132,14 +124,12 @@ mod test
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openapi(builder : OpenapiBuilder) -> OpenAPI
|
fn openapi(builder: OpenapiBuilder) -> OpenAPI {
|
||||||
{
|
|
||||||
Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap()
|
Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_builder()
|
fn new_builder() {
|
||||||
{
|
|
||||||
let info = info();
|
let info = info();
|
||||||
let builder = OpenapiBuilder::new(info.clone());
|
let builder = OpenapiBuilder::new(info.clone());
|
||||||
let openapi = openapi(builder);
|
let openapi = openapi(builder);
|
||||||
|
@ -150,13 +140,18 @@ mod test
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_schema()
|
fn add_schema() {
|
||||||
{
|
|
||||||
let mut builder = OpenapiBuilder::new(info());
|
let mut builder = OpenapiBuilder::new(info());
|
||||||
builder.add_schema::<Option<Messages>>();
|
builder.add_schema(<Option<Messages>>::schema());
|
||||||
let openapi = openapi(builder);
|
let openapi = openapi(builder);
|
||||||
|
|
||||||
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Message"] , ReferenceOr::Item(Message ::schema().into_schema()));
|
assert_eq!(
|
||||||
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Messages"], ReferenceOr::Item(Messages::schema().into_schema()));
|
openapi.components.clone().unwrap_or_default().schemas["Message"],
|
||||||
|
ReferenceOr::Item(Message::schema().into_schema())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
openapi.components.clone().unwrap_or_default().schemas["Messages"],
|
||||||
|
ReferenceOr::Item(Messages::schema().into_schema())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +1,31 @@
|
||||||
|
#![cfg_attr(not(feature = "auth"), allow(unused_imports))]
|
||||||
use super::SECURITY_NAME;
|
use super::SECURITY_NAME;
|
||||||
use futures_util::{future, future::FutureExt};
|
use futures_util::{future, future::FutureExt};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
error::Result,
|
anyhow,
|
||||||
handler::{Handler, HandlerFuture, NewHandler},
|
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
|
state::State
|
||||||
};
|
};
|
||||||
use indexmap::IndexMap;
|
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 openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::{
|
use std::{
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{Arc, RwLock}
|
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) -> Result<Self>
|
|
||||||
{
|
|
||||||
Ok(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>>
|
fn get_security(state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
|
||||||
{
|
|
||||||
use crate::AuthSource;
|
use crate::AuthSource;
|
||||||
use gotham::state::FromState;
|
use gotham::state::FromState;
|
||||||
|
|
||||||
|
@ -64,47 +49,213 @@ fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecuritySche
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut security_schemes : IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default();
|
let mut security_schemes: IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default();
|
||||||
security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme));
|
security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme));
|
||||||
|
|
||||||
security_schemes
|
security_schemes
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "auth"))]
|
#[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()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler for OpenapiHandler
|
fn create_openapi_response(state: &mut State, openapi: &Arc<RwLock<OpenAPI>>) -> Response<Body> {
|
||||||
{
|
let openapi = match openapi.read() {
|
||||||
fn handle(self, mut state : State) -> Pin<Box<HandlerFuture>>
|
Ok(openapi) => openapi,
|
||||||
{
|
Err(e) => {
|
||||||
let openapi = match self.openapi.read() {
|
error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
|
||||||
Ok(openapi) => openapi,
|
return create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||||
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()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut openapi = openapi.clone();
|
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();
|
let mut components = openapi.components.unwrap_or_default();
|
||||||
components.security_schemes = security_schemes;
|
components.security_schemes = security_schemes;
|
||||||
openapi.components = Some(components);
|
openapi.components = Some(components);
|
||||||
|
|
||||||
match serde_json::to_string(&openapi) {
|
match serde_json::to_string(&openapi) {
|
||||||
Ok(body) => {
|
Ok(body) => {
|
||||||
let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body);
|
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);
|
||||||
|
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()
|
future::ok((state, res)).boxed()
|
||||||
},
|
},
|
||||||
Err(e) => {
|
_ => {
|
||||||
error!("Unable to handle OpenAPI request due to error: {}", e);
|
{
|
||||||
let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
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()
|
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)
|
||||||
|
});
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
|
const SECURITY_NAME: &str = "authToken";
|
||||||
const SECURITY_NAME : &str = "authToken";
|
|
||||||
|
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod operation;
|
pub mod operation;
|
||||||
pub mod router;
|
pub mod router;
|
||||||
pub mod types;
|
|
||||||
|
|
|
@ -1,50 +1,48 @@
|
||||||
use crate::{
|
|
||||||
resource::*,
|
|
||||||
result::*,
|
|
||||||
OpenapiSchema,
|
|
||||||
RequestBody
|
|
||||||
};
|
|
||||||
use super::SECURITY_NAME;
|
use super::SECURITY_NAME;
|
||||||
|
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
use openapi_type::OpenapiSchema;
|
||||||
use openapiv3::{
|
use openapiv3::{
|
||||||
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr,
|
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
|
||||||
ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind,
|
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type
|
||||||
StatusCode, Type
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct OperationParams<'a>
|
struct OperationParams {
|
||||||
{
|
path_params: Option<OpenapiSchema>,
|
||||||
path_params : Vec<(&'a str, ReferenceOr<Schema>)>,
|
query_params: Option<OpenapiSchema>
|
||||||
query_params : Option<OpenapiSchema>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> OperationParams<'a>
|
impl OperationParams {
|
||||||
{
|
fn add_path_params(path_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||||
fn add_path_params(&self, params : &mut Vec<ReferenceOr<Parameter>>)
|
let path_params = match path_params {
|
||||||
{
|
Some(pp) => pp.schema,
|
||||||
for param in &self.path_params
|
None => return
|
||||||
{
|
};
|
||||||
|
let path_params = match path_params {
|
||||||
|
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||||
|
_ => panic!("Path Parameters needs to be a plain struct")
|
||||||
|
};
|
||||||
|
for (name, schema) in path_params.properties {
|
||||||
|
let required = path_params.required.contains(&name);
|
||||||
params.push(Item(Parameter::Path {
|
params.push(Item(Parameter::Path {
|
||||||
parameter_data: ParameterData {
|
parameter_data: ParameterData {
|
||||||
name: (*param).0.to_string(),
|
name,
|
||||||
description: None,
|
description: None,
|
||||||
required: true,
|
required,
|
||||||
deprecated: None,
|
deprecated: None,
|
||||||
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
|
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||||
example: None,
|
example: None,
|
||||||
examples: IndexMap::new()
|
examples: IndexMap::new()
|
||||||
},
|
},
|
||||||
style: Default::default(),
|
style: Default::default()
|
||||||
}));
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_query_params(self, params : &mut Vec<ReferenceOr<Parameter>>)
|
fn add_query_params(query_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||||
{
|
let query_params = match query_params {
|
||||||
let query_params = match self.query_params {
|
|
||||||
Some(qp) => qp.schema,
|
Some(qp) => qp.schema,
|
||||||
None => return
|
None => return
|
||||||
};
|
};
|
||||||
|
@ -52,8 +50,7 @@ impl<'a> OperationParams<'a>
|
||||||
SchemaKind::Type(Type::Object(ty)) => ty,
|
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||||
_ => panic!("Query Parameters needs to be a plain struct")
|
_ => panic!("Query Parameters needs to be a plain struct")
|
||||||
};
|
};
|
||||||
for (name, schema) in query_params.properties
|
for (name, schema) in query_params.properties {
|
||||||
{
|
|
||||||
let required = query_params.required.contains(&name);
|
let required = query_params.required.contains(&name);
|
||||||
params.push(Item(Parameter::Query {
|
params.push(Item(Parameter::Query {
|
||||||
parameter_data: ParameterData {
|
parameter_data: ParameterData {
|
||||||
|
@ -72,68 +69,55 @@ impl<'a> OperationParams<'a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_params(self) -> Vec<ReferenceOr<Parameter>>
|
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
|
||||||
{
|
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
|
||||||
let mut params : Vec<ReferenceOr<Parameter>> = Vec::new();
|
Self::add_path_params(self.path_params, &mut params);
|
||||||
self.add_path_params(&mut params);
|
Self::add_query_params(self.query_params, &mut params);
|
||||||
self.add_query_params(&mut params);
|
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OperationDescription<'a>
|
pub struct OperationDescription {
|
||||||
{
|
operation_id: Option<String>,
|
||||||
operation_id : Option<String>,
|
default_status: gotham::hyper::StatusCode,
|
||||||
default_status : crate::StatusCode,
|
accepted_types: Option<Vec<Mime>>,
|
||||||
accepted_types : Option<Vec<Mime>>,
|
schema: ReferenceOr<Schema>,
|
||||||
schema : ReferenceOr<Schema>,
|
params: OperationParams,
|
||||||
params : OperationParams<'a>,
|
body_schema: Option<ReferenceOr<Schema>>,
|
||||||
body_schema : Option<ReferenceOr<Schema>>,
|
supported_types: Option<Vec<Mime>>,
|
||||||
supported_types : Option<Vec<Mime>>,
|
requires_auth: bool
|
||||||
requires_auth : bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> OperationDescription<'a>
|
impl OperationDescription {
|
||||||
{
|
pub fn new<E: EndpointWithSchema>(schema: ReferenceOr<Schema>) -> Self {
|
||||||
pub fn new<Handler : ResourceMethod>(schema : ReferenceOr<Schema>) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
operation_id: Handler::operation_id(),
|
operation_id: E::operation_id(),
|
||||||
default_status: Handler::Res::default_status(),
|
default_status: E::Output::default_status(),
|
||||||
accepted_types: Handler::Res::accepted_types(),
|
accepted_types: E::Output::accepted_types(),
|
||||||
schema,
|
schema,
|
||||||
params: Default::default(),
|
params: Default::default(),
|
||||||
body_schema: None,
|
body_schema: None,
|
||||||
supported_types: None,
|
supported_types: None,
|
||||||
requires_auth: Handler::wants_auth()
|
requires_auth: E::wants_auth()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr<Schema>) -> Self
|
pub fn set_path_params(&mut self, params: OpenapiSchema) {
|
||||||
{
|
self.params.path_params = Some(params);
|
||||||
self.params.path_params.push((name, schema));
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_query_params(mut self, params : OpenapiSchema) -> Self
|
pub fn set_query_params(&mut self, params: OpenapiSchema) {
|
||||||
{
|
|
||||||
self.params.query_params = Some(params);
|
self.params.query_params = Some(params);
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_body<Body : RequestBody>(mut self, schema : ReferenceOr<Schema>) -> Self
|
pub fn set_body<Body: RequestBody>(&mut self, schema: ReferenceOr<Schema>) {
|
||||||
{
|
|
||||||
self.body_schema = Some(schema);
|
self.body_schema = Some(schema);
|
||||||
self.supported_types = Body::supported_types();
|
self.supported_types = Body::supported_types();
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schema_to_content(types: Vec<Mime>, schema: ReferenceOr<Schema>) -> IndexMap<String, MediaType> {
|
||||||
fn schema_to_content(types : Vec<Mime>, schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType>
|
let mut content: IndexMap<String, MediaType> = IndexMap::new();
|
||||||
{
|
for ty in types {
|
||||||
let mut content : IndexMap<String, MediaType> = IndexMap::new();
|
|
||||||
for ty in types
|
|
||||||
{
|
|
||||||
content.insert(ty.to_string(), MediaType {
|
content.insert(ty.to_string(), MediaType {
|
||||||
schema: Some(schema.clone()),
|
schema: Some(schema.clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -142,30 +126,41 @@ impl<'a> OperationDescription<'a>
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_operation(self) -> Operation
|
pub fn into_operation(self) -> Operation {
|
||||||
{
|
|
||||||
// this is unfortunately neccessary to prevent rust from complaining about partially moving self
|
// this is unfortunately neccessary to prevent rust from complaining about partially moving self
|
||||||
let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = (
|
let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = (
|
||||||
self.operation_id, self.default_status, self.accepted_types, self.schema, self.params, self.body_schema, self.supported_types, self.requires_auth);
|
self.operation_id,
|
||||||
|
self.default_status,
|
||||||
|
self.accepted_types,
|
||||||
|
self.schema,
|
||||||
|
self.params,
|
||||||
|
self.body_schema,
|
||||||
|
self.supported_types,
|
||||||
|
self.requires_auth
|
||||||
|
);
|
||||||
|
|
||||||
let content = Self::schema_to_content(accepted_types.or_all_types(), schema);
|
let content = Self::schema_to_content(accepted_types.or_all_types(), schema);
|
||||||
|
|
||||||
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
|
let mut responses: IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
|
||||||
responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response {
|
responses.insert(
|
||||||
description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(),
|
StatusCode::Code(default_status.as_u16()),
|
||||||
content,
|
Item(Response {
|
||||||
..Default::default()
|
description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(),
|
||||||
}));
|
content,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
let request_body = body_schema.map(|schema| Item(OARequestBody {
|
let request_body = body_schema.map(|schema| {
|
||||||
description: None,
|
Item(OARequestBody {
|
||||||
content: Self::schema_to_content(supported_types.or_all_types(), schema),
|
description: None,
|
||||||
required: true
|
content: Self::schema_to_content(supported_types.or_all_types(), schema),
|
||||||
}));
|
required: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let mut security = Vec::new();
|
let mut security = Vec::new();
|
||||||
if requires_auth
|
if requires_auth {
|
||||||
{
|
|
||||||
let mut sec = IndexMap::new();
|
let mut sec = IndexMap::new();
|
||||||
sec.insert(SECURITY_NAME.to_owned(), Vec::new());
|
sec.insert(SECURITY_NAME.to_owned(), Vec::new());
|
||||||
security.push(sec);
|
security.push(sec);
|
||||||
|
@ -187,27 +182,23 @@ impl<'a> OperationDescription<'a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test
|
mod test {
|
||||||
{
|
|
||||||
use crate::{OpenapiType, ResourceResult};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::{NoContent, Raw, ResponseSchema};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_content_schema_to_content()
|
fn no_content_schema_to_content() {
|
||||||
{
|
|
||||||
let types = NoContent::accepted_types();
|
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()));
|
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||||
assert!(content.is_empty());
|
assert!(content.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn raw_schema_to_content()
|
fn raw_schema_to_content() {
|
||||||
{
|
|
||||||
let types = Raw::<&str>::accepted_types();
|
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()));
|
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||||
assert_eq!(content.len(), 1);
|
assert_eq!(content.len(), 1);
|
||||||
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
||||||
|
|
|
@ -1,40 +1,38 @@
|
||||||
use crate::{
|
use super::{
|
||||||
resource::*,
|
builder::OpenapiBuilder,
|
||||||
routing::*,
|
handler::{OpenapiHandler, SwaggerUiHandler},
|
||||||
OpenapiType,
|
operation::OperationDescription
|
||||||
};
|
|
||||||
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
|
|
||||||
use gotham::{
|
|
||||||
pipeline::chain::PipelineHandleChain,
|
|
||||||
router::builder::*
|
|
||||||
};
|
};
|
||||||
|
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;
|
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
|
pub trait GetOpenapi {
|
||||||
{
|
fn get_openapi(&mut self, path: &str);
|
||||||
fn get_openapi(&mut self, path : &str);
|
fn swagger_ui(&mut self, path: &str);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OpenapiRouter<'a, D>
|
pub struct OpenapiRouter<'a, D> {
|
||||||
{
|
pub(crate) router: &'a mut D,
|
||||||
pub(crate) router : &'a mut D,
|
pub(crate) scope: Option<&'a str>,
|
||||||
pub(crate) scope : Option<&'a str>,
|
pub(crate) openapi_builder: &'a mut OpenapiBuilder
|
||||||
pub(crate) openapi_builder : &'a mut OpenapiBuilder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! implOpenapiRouter {
|
macro_rules! implOpenapiRouter {
|
||||||
($implType:ident) => {
|
($implType:ident) => {
|
||||||
|
|
||||||
impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
|
impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
pub fn scope<F>(&mut self, path : &str, callback : F)
|
pub fn scope<F>(&mut self, path: &str, callback: F)
|
||||||
where
|
where
|
||||||
F : FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
|
F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
|
||||||
{
|
{
|
||||||
let mut openapi_builder = self.openapi_builder.clone();
|
let mut openapi_builder = self.openapi_builder.clone();
|
||||||
let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/"));
|
let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/"));
|
||||||
|
@ -51,144 +49,85 @@ macro_rules! implOpenapiRouter {
|
||||||
|
|
||||||
impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
|
impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn get_openapi(&mut self, path : &str)
|
fn get_openapi(&mut self, path: &str) {
|
||||||
{
|
self.router
|
||||||
self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone()));
|
.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
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn resource<R : Resource>(&mut self, path : &str)
|
fn resource<R: ResourceWithSchema>(&mut self, path: &str) {
|
||||||
{
|
|
||||||
R::setup((self, path));
|
R::setup((self, path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn read_all<Handler : ResourceReadAll>(&mut self)
|
fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
|
||||||
{
|
let schema = (self.0).openapi_builder.add_schema(E::Output::schema());
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
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 op = descr.into_operation();
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
let mut item = (self.0).openapi_builder.remove_path(&path);
|
||||||
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
match E::http_method() {
|
||||||
|
Method::GET => item.get = Some(op),
|
||||||
|
Method::PUT => item.put = Some(op),
|
||||||
|
Method::POST => item.post = Some(op),
|
||||||
|
Method::DELETE => item.delete = Some(op),
|
||||||
|
Method::OPTIONS => item.options = Some(op),
|
||||||
|
Method::HEAD => item.head = Some(op),
|
||||||
|
Method::PATCH => item.patch = Some(op),
|
||||||
|
Method::TRACE => item.trace = Some(op),
|
||||||
|
method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method)
|
||||||
|
};
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
(self.0).openapi_builder.add_path(path, item);
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).read_all::<Handler>()
|
(&mut *(self.0).router, self.1).endpoint::<E>()
|
||||||
}
|
|
||||||
|
|
||||||
fn read<Handler : ResourceRead>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.get = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).read::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search<Handler : ResourceSearch>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.get = Some(OperationDescription::new::<Handler>(schema).with_query_params(Handler::Query::schema()).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).search::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create<Handler : ResourceCreate>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.post = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).create::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_all<Handler : ResourceChangeAll>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.put = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).change_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change<Handler : ResourceChange>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
|
||||||
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.put = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).with_body::<Handler::Body>(body_schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).change::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_all<Handler : ResourceRemoveAll>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).remove_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove<Handler : ResourceRemove>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
|
|
||||||
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
|
|
||||||
|
|
||||||
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
|
|
||||||
let mut item = (self.0).openapi_builder.remove_path(&path);
|
|
||||||
item.delete = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation());
|
|
||||||
(self.0).openapi_builder.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).router, self.1).remove::<Handler>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
implOpenapiRouter!(RouterBuilder);
|
implOpenapiRouter!(RouterBuilder);
|
||||||
|
|
|
@ -1,416 +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,
|
|
||||||
ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
|
|
||||||
};
|
|
||||||
#[cfg(feature = "uuid")]
|
|
||||||
use uuid::Uuid;
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeSet, HashMap, HashSet},
|
|
||||||
hash::BuildHasher
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
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`].
|
|
||||||
|
|
||||||
[`OpenapiType`]: trait.OpenapiType.html
|
|
||||||
*/
|
|
||||||
#[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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
[`OpenapiSchema`]: struct.OpenapiSchema.html
|
|
||||||
*/
|
|
||||||
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()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*};
|
|
||||||
|
|
||||||
(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()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*};
|
|
||||||
}
|
|
||||||
|
|
||||||
int_types!(isize);
|
|
||||||
int_types!(unsigned usize);
|
|
||||||
int_types!(bits = 8, i8);
|
|
||||||
int_types!(unsigned bits = 8, u8);
|
|
||||||
int_types!(bits = 16, i16);
|
|
||||||
int_types!(unsigned bits = 16, u16);
|
|
||||||
int_types!(bits = 32, i32);
|
|
||||||
int_types!(unsigned bits = 32, u32);
|
|
||||||
int_types!(bits = 64, i64);
|
|
||||||
int_types!(unsigned bits = 64, u64);
|
|
||||||
int_types!(bits = 128, i128);
|
|
||||||
int_types!(unsigned bits = 128, u128);
|
|
||||||
|
|
||||||
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!(f32 => r#"{"type":"number","format":"float"}"#);
|
|
||||||
assert_schema!(f64 => r#"{"type":"number","format":"double"}"#);
|
|
||||||
|
|
||||||
assert_schema!(String => r#"{"type":"string"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(Date<FixedOffset> => r#"{"type":"string","format":"date"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(Date<Local> => r#"{"type":"string","format":"date"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(Date<Utc> => r#"{"type":"string","format":"date"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(DateTime<FixedOffset> => r#"{"type":"string","format":"date-time"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(DateTime<Local> => r#"{"type":"string","format":"date-time"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(DateTime<Utc> => r#"{"type":"string","format":"date-time"}"#);
|
|
||||||
#[cfg(feature = "chrono")]
|
|
||||||
assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#);
|
|
||||||
#[cfg(feature = "uuid")]
|
|
||||||
assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#);
|
|
||||||
|
|
||||||
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}"#);
|
|
||||||
}
|
|
118
src/resource.rs
118
src/resource.rs
|
@ -1,118 +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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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`](trait.ResourceMethod.html).
|
|
||||||
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,64 +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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +1,22 @@
|
||||||
use gotham_restful_derive::ResourceError;
|
use gotham_restful_derive::ResourceError;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This is an error type that always yields a _403 Forbidden_ response. This type is best used in
|
This is an error type that always yields a _403 Forbidden_ response. This type is best used in
|
||||||
combination with [`AuthSuccess`] or [`AuthResult`].
|
combination with [AuthSuccess] or [AuthResult].
|
||||||
|
|
||||||
[`AuthSuccess`]: type.AuthSuccess.html
|
|
||||||
[`AuthResult`]: type.AuthResult.html
|
|
||||||
*/
|
*/
|
||||||
#[derive(Debug, Clone, Copy, ResourceError)]
|
#[derive(Debug, Clone, Copy, ResourceError)]
|
||||||
pub enum AuthError
|
pub enum AuthError {
|
||||||
{
|
|
||||||
#[status(FORBIDDEN)]
|
#[status(FORBIDDEN)]
|
||||||
#[display("Forbidden")]
|
#[display("Forbidden")]
|
||||||
Forbidden
|
Forbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This return type can be used to map another `ResourceResult` that can only be returned if the
|
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||||
client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can
|
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||||
look something like this (assuming the `auth` feature is enabled):
|
response will be issued.
|
||||||
|
|
||||||
|
Use can look something like this (assuming the `auth` feature is enabled):
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -36,7 +33,7 @@ look something like this (assuming the `auth` feature is enabled):
|
||||||
# #[derive(Clone, Deserialize)]
|
# #[derive(Clone, Deserialize)]
|
||||||
# struct MyAuthData { exp : u64 }
|
# struct MyAuthData { exp : u64 }
|
||||||
#
|
#
|
||||||
#[read_all(MyResource)]
|
#[read_all]
|
||||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
||||||
let auth_data = match auth {
|
let auth_data = match auth {
|
||||||
AuthStatus::Authenticated(data) => data,
|
AuthStatus::Authenticated(data) => data,
|
||||||
|
@ -52,13 +49,10 @@ pub type AuthSuccess<T> = Result<T, AuthError>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication
|
This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication
|
||||||
error, or delegates to another error type. This type is best used with [`AuthResult`].
|
error, or delegates to another error type. This type is best used with [AuthResult].
|
||||||
|
|
||||||
[`AuthResult`]: type.AuthResult.html
|
|
||||||
*/
|
*/
|
||||||
#[derive(Debug, ResourceError)]
|
#[derive(Debug, ResourceError)]
|
||||||
pub enum AuthErrorOrOther<E>
|
pub enum AuthErrorOrOther<E> {
|
||||||
{
|
|
||||||
#[status(FORBIDDEN)]
|
#[status(FORBIDDEN)]
|
||||||
#[display("Forbidden")]
|
#[display("Forbidden")]
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
@ -67,31 +61,36 @@ pub enum AuthErrorOrOther<E>
|
||||||
Other(E)
|
Other(E)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> From<AuthError> for AuthErrorOrOther<E>
|
impl<E> From<AuthError> for AuthErrorOrOther<E> {
|
||||||
{
|
fn from(err: AuthError) -> Self {
|
||||||
fn from(err : AuthError) -> Self
|
|
||||||
{
|
|
||||||
match err {
|
match err {
|
||||||
AuthError::Forbidden => Self::Forbidden
|
AuthError::Forbidden => Self::Forbidden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod private {
|
||||||
|
use gotham::handler::HandlerError;
|
||||||
|
pub trait Sealed {}
|
||||||
|
impl<E: Into<HandlerError>> Sealed for E {}
|
||||||
|
}
|
||||||
|
|
||||||
impl<E, F> From<F> for AuthErrorOrOther<E>
|
impl<E, F> From<F> for AuthErrorOrOther<E>
|
||||||
where
|
where
|
||||||
// TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20
|
// 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
|
fn from(err: F) -> Self {
|
||||||
{
|
|
||||||
Self::Other(err.into())
|
Self::Other(err.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This return type can be used to map another `ResourceResult` that can only be returned if the
|
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||||
client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can
|
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||||
look something like this (assuming the `auth` feature is enabled):
|
response will be issued.
|
||||||
|
|
||||||
|
Use can look something like this (assuming the `auth` feature is enabled):
|
||||||
|
|
||||||
```
|
```
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
@ -109,7 +108,7 @@ look something like this (assuming the `auth` feature is enabled):
|
||||||
# #[derive(Clone, Deserialize)]
|
# #[derive(Clone, Deserialize)]
|
||||||
# struct MyAuthData { exp : u64 }
|
# struct MyAuthData { exp : u64 }
|
||||||
#
|
#
|
||||||
#[read_all(MyResource)]
|
#[read_all]
|
||||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
||||||
let auth_data = match auth {
|
let auth_data = match auth {
|
||||||
AuthStatus::Authenticated(data) => data,
|
AuthStatus::Authenticated(data) => data,
|
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());
|
||||||
|
}
|
||||||
|
}
|
159
src/response/no_content.rs
Normal file
159
src/response/no_content.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
use super::{handle_error, IntoResponse};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
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};
|
||||||
|
|
||||||
|
/**
|
||||||
|
This is the return type of a resource that doesn't actually return something. It will result
|
||||||
|
in a _204 No Content_ answer by default. You don't need to use this type directly if using
|
||||||
|
the function attributes:
|
||||||
|
|
||||||
|
```
|
||||||
|
# #[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() {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct NoContent {
|
||||||
|
headers: HeaderMap
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<()> for NoContent {
|
||||||
|
fn from(_: ()) -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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().with_headers(self.headers)).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepted_types() -> Option<Vec<Mime>> {
|
||||||
|
Some(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_
|
||||||
|
fn default_status() -> StatusCode {
|
||||||
|
StatusCode::NO_CONTENT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> IntoResponse for Result<NoContent, E>
|
||||||
|
where
|
||||||
|
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||||
|
{
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
|
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, serde_json::Error>> + Send>> {
|
||||||
|
match self {
|
||||||
|
Ok(nc) => nc.into_response(),
|
||||||
|
Err(e) => handle_error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepted_types() -> Option<Vec<Mime>> {
|
||||||
|
NoContent::accepted_types()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use futures_executor::block_on;
|
||||||
|
use gotham::hyper::{header::ACCESS_CONTROL_ALLOW_ORIGIN, StatusCode};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Error)]
|
||||||
|
#[error("An Error")]
|
||||||
|
struct MsgError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_content_has_empty_response() {
|
||||||
|
let no_content = NoContent::default();
|
||||||
|
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
||||||
|
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
||||||
|
assert_eq!(res.mime, None);
|
||||||
|
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
assert_eq!(NoContent::default_status(), StatusCode::NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_content_result() {
|
||||||
|
let no_content: Result<NoContent, MsgError> = Ok(NoContent::default());
|
||||||
|
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
||||||
|
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
||||||
|
assert_eq!(res.mime, None);
|
||||||
|
assert_eq!(res.full_body().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,24 +1,26 @@
|
||||||
use super::{IntoResponseError, ResourceResult, handle_error};
|
use super::{handle_error, IntoResponse, IntoResponseError};
|
||||||
use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode};
|
use crate::{FromBody, RequestBody, ResourceType, Response};
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::OpenapiSchema;
|
use crate::{IntoResponseWithSchema, ResponseSchema};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::{OpenapiSchema, OpenapiType};
|
||||||
|
|
||||||
use futures_core::future::Future;
|
use futures_core::future::Future;
|
||||||
use futures_util::{future, future::FutureExt};
|
use futures_util::{future, future::FutureExt};
|
||||||
use gotham::hyper::body::{Body, Bytes};
|
use gotham::hyper::{
|
||||||
|
body::{Body, Bytes},
|
||||||
|
StatusCode
|
||||||
|
};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
||||||
use serde_json::error::Error as SerdeJsonError;
|
use serde_json::error::Error as SerdeJsonError;
|
||||||
use std::{
|
use std::{convert::Infallible, fmt::Display, pin::Pin};
|
||||||
convert::Infallible,
|
|
||||||
fmt::Display,
|
|
||||||
pin::Pin
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This type can be used both as a raw request body, as well as as a raw response. However, all types
|
This type can be used both as a raw request body, as well as as a raw response. However, all types
|
||||||
of request bodies are accepted by this type. It is therefore recommended to derive your own type
|
of request bodies are accepted by this type. It is therefore recommended to derive your own type
|
||||||
from [`RequestBody`] and only use this when you need to return a raw response. This is a usage
|
from [RequestBody] and only use this when you need to return a raw response. This is a usage
|
||||||
example that simply returns its body:
|
example that simply returns its body:
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
|
@ -29,7 +31,7 @@ example that simply returns its body:
|
||||||
#[resource(create)]
|
#[resource(create)]
|
||||||
struct ImageResource;
|
struct ImageResource;
|
||||||
|
|
||||||
#[create(ImageResource)]
|
#[create]
|
||||||
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
|
@ -39,48 +41,39 @@ fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||||
# }));
|
# }));
|
||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
[`OpenapiType`]: trait.OpenapiType.html
|
|
||||||
*/
|
*/
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Raw<T>
|
pub struct Raw<T> {
|
||||||
{
|
pub raw: T,
|
||||||
pub raw : T,
|
pub mime: Mime
|
||||||
pub mime : Mime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Raw<T>
|
impl<T> Raw<T> {
|
||||||
{
|
pub fn new(raw: T, mime: Mime) -> Self {
|
||||||
pub fn new(raw : T, mime : Mime) -> Self
|
|
||||||
{
|
|
||||||
Self { raw, mime }
|
Self { raw, mime }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, U> AsMut<U> for Raw<T>
|
impl<T, U> AsMut<U> for Raw<T>
|
||||||
where
|
where
|
||||||
T : AsMut<U>
|
T: AsMut<U>
|
||||||
{
|
{
|
||||||
fn as_mut(&mut self) -> &mut U
|
fn as_mut(&mut self) -> &mut U {
|
||||||
{
|
|
||||||
self.raw.as_mut()
|
self.raw.as_mut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, U> AsRef<U> for Raw<T>
|
impl<T, U> AsRef<U> for Raw<T>
|
||||||
where
|
where
|
||||||
T : AsRef<U>
|
T: AsRef<U>
|
||||||
{
|
{
|
||||||
fn as_ref(&self) -> &U
|
fn as_ref(&self) -> &U {
|
||||||
{
|
|
||||||
self.raw.as_ref()
|
self.raw.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T : Clone> Clone for Raw<T>
|
impl<T: Clone> Clone for Raw<T> {
|
||||||
{
|
fn clone(&self) -> Self {
|
||||||
fn clone(&self) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
raw: self.raw.clone(),
|
raw: self.raw.clone(),
|
||||||
mime: self.mime.clone()
|
mime: self.mime.clone()
|
||||||
|
@ -88,36 +81,19 @@ impl<T : Clone> Clone for Raw<T>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T : for<'a> From<&'a [u8]>> FromBody for Raw<T>
|
impl<T: for<'a> From<&'a [u8]>> FromBody for Raw<T> {
|
||||||
{
|
|
||||||
type Err = Infallible;
|
type Err = Infallible;
|
||||||
|
|
||||||
fn from_body(body : Bytes, mime : Mime) -> Result<Self, Self::Err>
|
fn from_body(body: Bytes, mime: Mime) -> Result<Self, Self::Err> {
|
||||||
{
|
|
||||||
Ok(Self::new(body.as_ref().into(), mime))
|
Ok(Self::new(body.as_ref().into(), mime))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> RequestBody for Raw<T>
|
impl<T> RequestBody for Raw<T> where Raw<T>: FromBody + ResourceType {}
|
||||||
where
|
|
||||||
Raw<T> : FromBody + ResourceType
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Into<Body>> ResourceResult for Raw<T>
|
#[cfg(feature = "openapi")]
|
||||||
where
|
impl<T> OpenapiType for Raw<T> {
|
||||||
Self : Send
|
fn schema() -> OpenapiSchema {
|
||||||
{
|
|
||||||
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")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -125,39 +101,61 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, E> ResourceResult for Result<Raw<T>, E>
|
impl<T: Into<Body>> IntoResponse for Raw<T>
|
||||||
where
|
where
|
||||||
Raw<T> : ResourceResult,
|
Self: Send
|
||||||
E : Display + IntoResponseError<Err = <Raw<T> as ResourceResult>::Err>
|
{
|
||||||
|
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;
|
type Err = E::Err;
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>>
|
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
|
||||||
{
|
|
||||||
match self {
|
match self {
|
||||||
Ok(raw) => raw.into_response(),
|
Ok(raw) => raw.into_response(),
|
||||||
Err(e) => handle_error(e)
|
Err(e) => handle_error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
fn schema() -> OpenapiSchema
|
impl<T, E> ResponseSchema for Result<Raw<T>, E>
|
||||||
{
|
where
|
||||||
<Raw<T> as ResourceResult>::schema()
|
Raw<T>: IntoResponseWithSchema,
|
||||||
|
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
|
||||||
|
{
|
||||||
|
fn schema() -> OpenapiSchema {
|
||||||
|
<Raw<T> as ResponseSchema>::schema()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test
|
mod test {
|
||||||
{
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use futures_executor::block_on;
|
use futures_executor::block_on;
|
||||||
use mime::TEXT_PLAIN;
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn raw_response()
|
fn raw_response() {
|
||||||
{
|
|
||||||
let msg = "Test";
|
let msg = "Test";
|
||||||
let raw = Raw::new(msg, TEXT_PLAIN);
|
let raw = Raw::new(msg, TEXT_PLAIN);
|
||||||
let res = block_on(raw.into_response()).expect("didn't expect error response");
|
let res = block_on(raw.into_response()).expect("didn't expect error response");
|
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]);
|
||||||
|
}
|
||||||
|
}
|
105
src/response/result.rs
Normal file
105
src/response/result.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
use super::{handle_error, IntoResponse, ResourceError};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
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: Display + Send + 'static;
|
||||||
|
|
||||||
|
fn into_response_error(self) -> Result<Response, Self::Err>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Error> IntoResponseError for E {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
|
fn into_response_error(self) -> Result<Response, Self::Err> {
|
||||||
|
let err: ResourceError = self.into();
|
||||||
|
Ok(Response::json(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
serde_json::to_string(&err)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, E> IntoResponse for Result<R, E>
|
||||||
|
where
|
||||||
|
R: ResponseBody,
|
||||||
|
E: Display + IntoResponseError<Err = serde_json::Error>
|
||||||
|
{
|
||||||
|
type Err = E::Err;
|
||||||
|
|
||||||
|
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
|
||||||
|
match self {
|
||||||
|
Ok(r) => Success::from(r).into_response(),
|
||||||
|
Err(e) => handle_error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::response::OrAllTypes;
|
||||||
|
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_ok() {
|
||||||
|
let ok: Result<Msg, MsgError> = Ok(Msg::default());
|
||||||
|
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(), br#"{"msg":""}"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn result_err() {
|
||||||
|
let err: Result<Msg, MsgError> = Err(MsgError::default());
|
||||||
|
let res = block_on(err.into_response()).expect("didn't expect error response");
|
||||||
|
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
||||||
|
assert_eq!(
|
||||||
|
res.full_body().unwrap(),
|
||||||
|
format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn success_accepts_json() {
|
||||||
|
assert!(<Result<Msg, MsgError>>::accepted_types()
|
||||||
|
.or_all_types()
|
||||||
|
.contains(&APPLICATION_JSON))
|
||||||
|
}
|
||||||
|
}
|
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,191 +0,0 @@
|
||||||
use crate::Response;
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::OpenapiSchema;
|
|
||||||
use futures_util::future::FutureExt;
|
|
||||||
use mime::{Mime, STAR_STAR};
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
future::Future,
|
|
||||||
fmt::{Debug, Display},
|
|
||||||
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 + '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(|| {
|
|
||||||
errorlog(&e);
|
|
||||||
e.into_response_error()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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,141 +0,0 @@
|
||||||
use super::{ResourceResult, handle_error};
|
|
||||||
use crate::{IntoResponseError, Response};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::{OpenapiSchema, OpenapiType};
|
|
||||||
use futures_util::{future, future::FutureExt};
|
|
||||||
use mime::Mime;
|
|
||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
future::Future,
|
|
||||||
pin::Pin
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
This is the return type of a resource that doesn't actually return something. It will result
|
|
||||||
in a _204 No Content_ answer by default. You don't need to use this type directly if using
|
|
||||||
the function attributes:
|
|
||||||
|
|
||||||
```
|
|
||||||
# #[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(MyResource)]
|
|
||||||
fn read_all(_state: &mut State) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct NoContent;
|
|
||||||
|
|
||||||
impl From<()> for NoContent
|
|
||||||
{
|
|
||||||
fn from(_ : ()) -> Self
|
|
||||||
{
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResourceResult 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the schema of the `()` type.
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
<()>::schema()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This will always be a _204 No Content_
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn default_status() -> crate::StatusCode
|
|
||||||
{
|
|
||||||
crate::StatusCode::NO_CONTENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> ResourceResult for Result<NoContent, E>
|
|
||||||
where
|
|
||||||
E : Display + IntoResponseError<Err = serde_json::Error>
|
|
||||||
{
|
|
||||||
type Err = serde_json::Error;
|
|
||||||
|
|
||||||
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, serde_json::Error>> + Send>>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(nc) => nc.into_response(),
|
|
||||||
Err(e) => handle_error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
NoContent::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[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 no_content_has_empty_response()
|
|
||||||
{
|
|
||||||
let no_content = NoContent::default();
|
|
||||||
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
|
||||||
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
|
||||||
assert_eq!(res.mime, None);
|
|
||||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_content_result()
|
|
||||||
{
|
|
||||||
let no_content : Result<NoContent, MsgError> = Ok(NoContent::default());
|
|
||||||
let res = block_on(no_content.into_response()).expect("didn't expect error response");
|
|
||||||
assert_eq!(res.status, StatusCode::NO_CONTENT);
|
|
||||||
assert_eq!(res.mime, None);
|
|
||||||
assert_eq!(res.full_body().unwrap(), &[] as &[u8]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
use super::{ResourceResult, handle_error, into_response_helper};
|
|
||||||
use crate::{
|
|
||||||
result::ResourceError,
|
|
||||||
Response, ResponseBody, StatusCode
|
|
||||||
};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::OpenapiSchema;
|
|
||||||
use futures_core::future::Future;
|
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
|
||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
fmt::Display,
|
|
||||||
pin::Pin
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait IntoResponseError
|
|
||||||
{
|
|
||||||
type Err : Error + Send + 'static;
|
|
||||||
|
|
||||||
fn into_response_error(self) -> Result<Response, Self::Err>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E : Error> IntoResponseError for E
|
|
||||||
{
|
|
||||||
type Err = serde_json::Error;
|
|
||||||
|
|
||||||
fn into_response_error(self) -> Result<Response, Self::Err>
|
|
||||||
{
|
|
||||||
let err : ResourceError = self.into();
|
|
||||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R, E> ResourceResult for Result<R, E>
|
|
||||||
where
|
|
||||||
R : ResponseBody,
|
|
||||||
E : Display + IntoResponseError<Err = serde_json::Error>
|
|
||||||
{
|
|
||||||
type Err = E::Err;
|
|
||||||
|
|
||||||
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)?))),
|
|
||||||
Err(e) => handle_error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
R::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test
|
|
||||||
{
|
|
||||||
use super::*;
|
|
||||||
use crate::result::OrAllTypes;
|
|
||||||
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_ok()
|
|
||||||
{
|
|
||||||
let ok : Result<Msg, MsgError> = Ok(Msg::default());
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn result_err()
|
|
||||||
{
|
|
||||||
let err : Result<Msg, MsgError> = Err(MsgError::default());
|
|
||||||
let res = block_on(err.into_response()).expect("didn't expect error response");
|
|
||||||
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
assert_eq!(res.mime, Some(APPLICATION_JSON));
|
|
||||||
assert_eq!(res.full_body().unwrap(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn success_accepts_json()
|
|
||||||
{
|
|
||||||
assert!(<Result<Msg, MsgError>>::accepted_types().or_all_types().contains(&APPLICATION_JSON))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,163 +0,0 @@
|
||||||
use super::{ResourceResult, into_response_helper};
|
|
||||||
use crate::{Response, ResponseBody};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::OpenapiSchema;
|
|
||||||
use gotham::hyper::StatusCode;
|
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
|
||||||
use std::{
|
|
||||||
fmt::Debug,
|
|
||||||
future::Future,
|
|
||||||
pin::Pin,
|
|
||||||
ops::{Deref, DerefMut}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
461
src/routing.rs
461
src/routing.rs
|
@ -1,110 +1,81 @@
|
||||||
use crate::{
|
|
||||||
resource::*,
|
|
||||||
result::{ResourceError, ResourceResult},
|
|
||||||
RequestBody,
|
|
||||||
Response,
|
|
||||||
StatusCode
|
|
||||||
};
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
use crate::CorsRoute;
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
use crate::openapi::{
|
use crate::openapi::{
|
||||||
builder::{OpenapiBuilder, OpenapiInfo},
|
builder::{OpenapiBuilder, OpenapiInfo},
|
||||||
router::OpenapiRouter
|
router::OpenapiRouter
|
||||||
};
|
};
|
||||||
|
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
|
||||||
use futures_util::{future, future::FutureExt};
|
#[cfg(feature = "cors")]
|
||||||
|
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
|
||||||
use gotham::{
|
use gotham::{
|
||||||
handler::{HandlerError, HandlerFuture, IntoHandlerError},
|
handler::HandlerError,
|
||||||
helpers::http::response::{create_empty_response, create_response},
|
helpers::http::response::{create_empty_response, create_response},
|
||||||
|
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method, StatusCode},
|
||||||
pipeline::chain::PipelineHandleChain,
|
pipeline::chain::PipelineHandleChain,
|
||||||
router::{
|
router::{
|
||||||
builder::*,
|
builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder},
|
||||||
non_match::RouteNonMatch,
|
non_match::RouteNonMatch,
|
||||||
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
|
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
|
||||||
},
|
},
|
||||||
state::{FromState, State}
|
state::{FromState, State}
|
||||||
};
|
};
|
||||||
use gotham::hyper::{
|
|
||||||
body::to_bytes,
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
Body,
|
|
||||||
HeaderMap,
|
|
||||||
Method
|
|
||||||
};
|
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
use std::{
|
#[cfg(feature = "openapi")]
|
||||||
future::Future,
|
use openapi_type::OpenapiType;
|
||||||
panic::RefUnwindSafe,
|
use std::{any::TypeId, panic::RefUnwindSafe};
|
||||||
pin::Pin
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Allow us to extract an id from a path.
|
/// Allow us to extract an id from a path.
|
||||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
struct PathExtractor<ID : RefUnwindSafe + Send + 'static>
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
{
|
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||||
id : ID
|
pub id: ID
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
|
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
|
||||||
/// router into one that will only allow RESTful resources, but record them and generate
|
/// router into one that will only allow RESTful resources, but record them and generate
|
||||||
/// an OpenAPI specification on request.
|
/// an OpenAPI specification on request.
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub trait WithOpenapi<D>
|
pub trait WithOpenapi<D> {
|
||||||
{
|
fn with_openapi<F>(&mut self, info: OpenapiInfo, block: F)
|
||||||
fn with_openapi<F>(&mut self, info : OpenapiInfo, block : F)
|
|
||||||
where
|
where
|
||||||
F : FnOnce(OpenapiRouter<'_, D>);
|
F: FnOnce(OpenapiRouter<'_, D>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
||||||
/// any RESTful `Resource` with a path.
|
/// any RESTful [Resource] with a path.
|
||||||
pub trait DrawResources
|
#[_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
|
/// This trait allows to draw routes within an resource. Use this only inside the
|
||||||
/// `Resource::setup` method.
|
/// [Resource::setup] method.
|
||||||
pub trait DrawResourceRoutes
|
#[_private_openapi_trait(DrawResourceRoutesWithSchema)]
|
||||||
{
|
pub trait DrawResourceRoutes {
|
||||||
fn read_all<Handler : ResourceReadAll>(&mut self);
|
#[openapi_bound("E: crate::EndpointWithSchema")]
|
||||||
|
#[non_openapi_bound("E: crate::Endpoint")]
|
||||||
fn read<Handler : ResourceRead>(&mut self);
|
fn endpoint<E: 'static>(&mut self);
|
||||||
|
|
||||||
fn search<Handler : ResourceSearch>(&mut self);
|
|
||||||
|
|
||||||
fn create<Handler : ResourceCreate>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static;
|
|
||||||
|
|
||||||
fn change_all<Handler : ResourceChangeAll>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static;
|
|
||||||
|
|
||||||
fn change<Handler : ResourceChange>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static;
|
|
||||||
|
|
||||||
fn remove_all<Handler : ResourceRemoveAll>(&mut self);
|
|
||||||
|
|
||||||
fn remove<Handler : ResourceRemove>(&mut self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn response_from(res : Response, state : &State) -> gotham::hyper::Response<Body>
|
fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
|
||||||
{
|
|
||||||
let mut r = create_empty_response(state, res.status);
|
let mut r = create_empty_response(state, res.status);
|
||||||
if let Some(mime) = res.mime
|
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);
|
let method = Method::borrow_from(state);
|
||||||
if method != Method::HEAD
|
if method != Method::HEAD {
|
||||||
{
|
|
||||||
*r.body_mut() = res.body;
|
*r.body_mut() = res.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,160 +85,60 @@ fn response_from(res : Response, state : &State) -> gotham::hyper::Response<Body
|
||||||
r
|
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
|
where
|
||||||
F : FnOnce(State) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
E: Endpoint,
|
||||||
R : ResourceResult
|
<E::Output as IntoResponse>::Err: Into<HandlerError>
|
||||||
{
|
{
|
||||||
let (state, res) = get_result(state).await;
|
trace!("entering endpoint_handler");
|
||||||
let res = res.into_response().await;
|
let placeholders = E::Placeholders::take_from(state);
|
||||||
match res {
|
// workaround for E::Placeholders and E::Param being the same type
|
||||||
Ok(res) => {
|
// when fixed remove `Clone` requirement on endpoint
|
||||||
let r = response_from(res, &state);
|
if TypeId::of::<E::Placeholders>() == TypeId::of::<E::Params>() {
|
||||||
Ok((state, r))
|
state.put(placeholders.clone());
|
||||||
},
|
|
||||||
Err(e) => Err((state, e.into_handler_error()))
|
|
||||||
}
|
}
|
||||||
}
|
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>)
|
let body = match E::needs_body() {
|
||||||
where
|
true => {
|
||||||
B : RequestBody,
|
let body = to_bytes(Body::take_from(state)).await?;
|
||||||
F : FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>>,
|
|
||||||
R : ResourceResult
|
|
||||||
{
|
|
||||||
let body = to_bytes(Body::take_from(&mut state)).await;
|
|
||||||
|
|
||||||
let body = match body {
|
let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) {
|
||||||
Ok(body) => body,
|
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||||
Err(e) => return (state, Err(e.into_handler_error()))
|
None => {
|
||||||
};
|
debug!("Missing Content-Type: Returning 415 Response");
|
||||||
|
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
|
match E::Body::from_body(body, content_type) {
|
||||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
Ok(body) => Some(body),
|
||||||
None => {
|
Err(e) => {
|
||||||
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
debug!("Invalid Body: Returning 400 Response");
|
||||||
return (state, Ok(res))
|
let error: ResourceError = e.into();
|
||||||
}
|
let json = serde_json::to_string(&error)?;
|
||||||
};
|
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||||
|
return Ok(res);
|
||||||
let res = {
|
}
|
||||||
let body = match B::from_body(body, content_type) {
|
|
||||||
Ok(body) => body,
|
|
||||||
Err(e) => {
|
|
||||||
let error : ResourceError = e.into();
|
|
||||||
let res = match serde_json::to_string(&error) {
|
|
||||||
Ok(json) => {
|
|
||||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
|
||||||
Ok(res)
|
|
||||||
},
|
|
||||||
Err(e) => Err(e.into_handler_error())
|
|
||||||
};
|
|
||||||
return (state, res)
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
get_result(state, body)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (state, res) = res.await;
|
|
||||||
let res = res.into_response().await;
|
|
||||||
|
|
||||||
let res = match res {
|
|
||||||
Ok(res) => {
|
|
||||||
let r = response_from(res, &state);
|
|
||||||
Ok(r)
|
|
||||||
},
|
},
|
||||||
Err(e) => Err(e.into_handler_error())
|
false => None
|
||||||
};
|
};
|
||||||
(state, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_with_body<B, F, R>(state : State, get_result : F) -> Pin<Box<HandlerFuture>>
|
let out = E::handle(state, placeholders, params, body).await;
|
||||||
where
|
let res = out.into_response().await.map_err(Into::into)?;
|
||||||
B : RequestBody + 'static,
|
debug!("Returning response {:?}", res);
|
||||||
F : FnOnce(State, B) -> Pin<Box<dyn Future<Output = (State, R)> + Send>> + Send + 'static,
|
Ok(response_from(res, state))
|
||||||
R : ResourceResult + Send + 'static
|
|
||||||
{
|
|
||||||
body_to_res(state, get_result)
|
|
||||||
.then(|(state, res)| match res {
|
|
||||||
Ok(ok) => future::ok((state, ok)),
|
|
||||||
Err(err) => future::err((state, err))
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_all_handler<Handler : ResourceReadAll>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
{
|
|
||||||
to_handler_future(state, |state| Handler::read_all(state)).boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_handler<Handler : ResourceRead>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
{
|
|
||||||
let id = {
|
|
||||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
|
||||||
path.id.clone()
|
|
||||||
};
|
|
||||||
to_handler_future(state, |state| Handler::read(state, id)).boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_handler<Handler : ResourceSearch>(mut state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
{
|
|
||||||
let query = Handler::Query::take_from(&mut state);
|
|
||||||
to_handler_future(state, |state| Handler::search(state, query)).boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_handler<Handler : ResourceCreate>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_all_handler<Handler : ResourceChangeAll>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change_all(state, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_handler<Handler : ResourceChange>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
where
|
|
||||||
Handler::Res : 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let id = {
|
|
||||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
|
||||||
path.id.clone()
|
|
||||||
};
|
|
||||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::change(state, id, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_all_handler<Handler : ResourceRemoveAll>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
{
|
|
||||||
to_handler_future(state, |state| Handler::remove_all(state)).boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_handler<Handler : ResourceRemove>(state : State) -> Pin<Box<HandlerFuture>>
|
|
||||||
{
|
|
||||||
let id = {
|
|
||||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
|
||||||
path.id.clone()
|
|
||||||
};
|
|
||||||
to_handler_future(state, |state| Handler::remove(state, id)).boxed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct MaybeMatchAcceptHeader
|
struct MaybeMatchAcceptHeader {
|
||||||
{
|
matcher: Option<AcceptHeaderRouteMatcher>
|
||||||
matcher : Option<AcceptHeaderRouteMatcher>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RouteMatcher for MaybeMatchAcceptHeader
|
impl RouteMatcher for MaybeMatchAcceptHeader {
|
||||||
{
|
fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
|
||||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
|
||||||
{
|
|
||||||
match &self.matcher {
|
match &self.matcher {
|
||||||
Some(matcher) => matcher.is_match(state),
|
Some(matcher) => matcher.is_match(state),
|
||||||
None => Ok(())
|
None => Ok(())
|
||||||
|
@ -275,10 +146,8 @@ impl RouteMatcher for MaybeMatchAcceptHeader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
|
impl MaybeMatchAcceptHeader {
|
||||||
{
|
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||||
fn from(types : Option<Vec<Mime>>) -> Self
|
|
||||||
{
|
|
||||||
let types = match types {
|
let types = match types {
|
||||||
Some(types) if types.is_empty() => None,
|
Some(types) if types.is_empty() => None,
|
||||||
types => types
|
types => types
|
||||||
|
@ -289,16 +158,19 @@ impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
||||||
struct MaybeMatchContentTypeHeader
|
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||||
{
|
Self::new(types)
|
||||||
matcher : Option<ContentTypeHeaderRouteMatcher>
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RouteMatcher for MaybeMatchContentTypeHeader
|
#[derive(Clone)]
|
||||||
{
|
struct MaybeMatchContentTypeHeader {
|
||||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
matcher: Option<ContentTypeHeaderRouteMatcher>
|
||||||
{
|
}
|
||||||
|
|
||||||
|
impl RouteMatcher for MaybeMatchContentTypeHeader {
|
||||||
|
fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
|
||||||
match &self.matcher {
|
match &self.matcher {
|
||||||
Some(matcher) => matcher.is_match(state),
|
Some(matcher) => matcher.is_match(state),
|
||||||
None => Ok(())
|
None => Ok(())
|
||||||
|
@ -306,28 +178,31 @@ impl RouteMatcher for MaybeMatchContentTypeHeader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader
|
impl MaybeMatchContentTypeHeader {
|
||||||
{
|
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||||
fn from(types : Option<Vec<Mime>>) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
Self {
|
||||||
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
|
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
||||||
|
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||||
|
Self::new(types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! implDrawResourceRoutes {
|
macro_rules! implDrawResourceRoutes {
|
||||||
($implType:ident) => {
|
($implType:ident) => {
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
|
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn with_openapi<F>(&mut self, info : OpenapiInfo, block : F)
|
fn with_openapi<F>(&mut self, info: OpenapiInfo, block: F)
|
||||||
where
|
where
|
||||||
F : FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>)
|
F: FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>)
|
||||||
{
|
{
|
||||||
let router = OpenapiRouter {
|
let router = OpenapiRouter {
|
||||||
router: self,
|
router: self,
|
||||||
|
@ -340,117 +215,41 @@ macro_rules! implDrawResourceRoutes {
|
||||||
|
|
||||||
impl<'a, C, P> DrawResources for $implType<'a, C, P>
|
impl<'a, C, P> DrawResources for $implType<'a, C, P>
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn resource<R : Resource>(&mut self, path : &str)
|
fn resource<R: Resource>(&mut self, path: &str) {
|
||||||
{
|
|
||||||
R::setup((self, path));
|
R::setup((self, path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters
|
|
||||||
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
||||||
where
|
where
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
fn read_all<Handler : ResourceReadAll>(&mut self)
|
fn endpoint<E: Endpoint + 'static>(&mut self) {
|
||||||
{
|
let uri = format!("{}/{}", self.1, E::uri());
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
debug!("Registering endpoint for {}", uri);
|
||||||
self.0.get(&self.1)
|
self.0.associate(&uri, |assoc| {
|
||||||
.extend_route_matcher(matcher)
|
assoc
|
||||||
.to(|state| read_all_handler::<Handler>(state));
|
.request(vec![E::http_method()])
|
||||||
}
|
.add_route_matcher(MaybeMatchAcceptHeader::new(E::Output::accepted_types()))
|
||||||
|
.with_path_extractor::<E::Placeholders>()
|
||||||
|
.with_query_string_extractor::<E::Params>()
|
||||||
|
.to_async_borrowing(endpoint_handler::<E>);
|
||||||
|
|
||||||
fn read<Handler : ResourceRead>(&mut self)
|
#[cfg(feature = "cors")]
|
||||||
{
|
if E::http_method() != Method::GET {
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
assoc
|
||||||
self.0.get(&format!("{}/:id", self.1))
|
.options()
|
||||||
.extend_route_matcher(matcher)
|
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
.to(crate::cors::cors_preflight_handler);
|
||||||
.to(|state| read_handler::<Handler>(state));
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
fn search<Handler : ResourceSearch>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.get(&format!("{}/search", self.1))
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_query_string_extractor::<Handler::Query>()
|
|
||||||
.to(|state| search_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create<Handler : ResourceCreate>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : Send + 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
self.0.post(&self.1)
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.to(|state| create_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&self.1, Method::POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_all<Handler : ResourceChangeAll>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : Send + 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
self.0.put(&self.1)
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.to(|state| change_all_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&self.1, Method::PUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change<Handler : ResourceChange>(&mut self)
|
|
||||||
where
|
|
||||||
Handler::Res : Send + 'static,
|
|
||||||
Handler::Body : 'static
|
|
||||||
{
|
|
||||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
let path = format!("{}/:id", self.1);
|
|
||||||
self.0.put(&path)
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| change_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&path, Method::PUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_all<Handler : ResourceRemoveAll>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.delete(&self.1)
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.to(|state| remove_all_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&self.1, Method::DELETE);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove<Handler : ResourceRemove>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let path = format!("{}/:id", self.1);
|
|
||||||
self.0.delete(&path)
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| remove_handler::<Handler>(state));
|
|
||||||
#[cfg(feature = "cors")]
|
|
||||||
self.0.cors(&path, Method::POST);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
implDrawResourceRoutes!(RouterBuilder);
|
implDrawResourceRoutes!(RouterBuilder);
|
||||||
|
|
102
src/types.rs
102
src/types.rs
|
@ -1,54 +1,39 @@
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::OpenapiType;
|
|
||||||
|
|
||||||
use gotham::hyper::body::Bytes;
|
use gotham::hyper::body::Bytes;
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{
|
use std::error::Error;
|
||||||
error::Error,
|
|
||||||
panic::RefUnwindSafe
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
#[cfg(not(feature = "openapi"))]
|
||||||
pub trait ResourceType
|
pub trait ResourceType {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
#[cfg(not(feature = "openapi"))]
|
||||||
impl<T> ResourceType for T
|
impl<T> ResourceType for T {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
pub trait ResourceType : OpenapiType
|
pub trait ResourceType: OpenapiType {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
#[cfg(feature = "openapi")]
|
||||||
impl<T : OpenapiType> ResourceType for T
|
impl<T: OpenapiType> ResourceType for T {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// A type that can be used inside a response body. Implemented for every type that is
|
/// A type that can be used inside a response body. Implemented for every type that is
|
||||||
/// serializable with serde. If the `openapi` feature is used, it must also be of type
|
/// serializable with serde. If the `openapi` feature is used, it must also be of type
|
||||||
/// `OpenapiType`.
|
/// [OpenapiType].
|
||||||
pub trait ResponseBody : ResourceType + Serialize
|
///
|
||||||
{
|
/// [OpenapiType]: trait.OpenapiType.html
|
||||||
}
|
pub trait ResponseBody: ResourceType + Serialize {}
|
||||||
|
|
||||||
impl<T : ResourceType + Serialize> ResponseBody for T
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
impl<T: ResourceType + Serialize> ResponseBody for T {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
This trait should be implemented for every type that can be built from an HTTP request body
|
This trait should be implemented for every type that can be built from an HTTP request body
|
||||||
plus its media type. For most use cases it is sufficient to derive this trait, you usually
|
plus its media type.
|
||||||
don't need to manually implement this. Therefore, make sure that the first variable of
|
|
||||||
your struct can be built from [`Bytes`], and the second one can be build from [`Mime`].
|
For most use cases it is sufficient to derive this trait, you usually don't need to manually
|
||||||
If you have any additional variables, they need to be `Default`. This is an example of
|
implement this. Therefore, make sure that the first variable of your struct can be built from
|
||||||
such a struct:
|
[Bytes], and the second one can be build from [Mime]. If you have any additional variables, they
|
||||||
|
need to be [Default]. This is an example of such a struct:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate gotham_restful;
|
# #[macro_use] extern crate gotham_restful;
|
||||||
|
@ -60,39 +45,32 @@ struct RawImage {
|
||||||
content_type: Mime
|
content_type: Mime
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[`Bytes`]: ../bytes/struct.Bytes.html
|
|
||||||
[`Mime`]: ../mime/struct.Mime.html
|
|
||||||
*/
|
*/
|
||||||
pub trait FromBody : Sized
|
pub trait FromBody: Sized {
|
||||||
{
|
|
||||||
/// The error type returned by the conversion if it was unsuccessfull. When using the derive
|
/// The error type returned by the conversion if it was unsuccessfull. When using the derive
|
||||||
/// macro, there is no way to trigger an error, so `Infallible` is used here. However, this
|
/// macro, there is no way to trigger an error, so [std::convert::Infallible] is used here.
|
||||||
/// might change in the future.
|
/// However, this might change in the future.
|
||||||
type Err : Error;
|
type Err: Error;
|
||||||
|
|
||||||
/// Perform the conversion.
|
/// Perform the conversion.
|
||||||
fn from_body(body : Bytes, content_type : Mime) -> Result<Self, Self::Err>;
|
fn from_body(body: Bytes, content_type: Mime) -> Result<Self, Self::Err>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T : DeserializeOwned> FromBody for T
|
impl<T: DeserializeOwned> FromBody for T {
|
||||||
{
|
|
||||||
type Err = serde_json::Error;
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
fn from_body(body : Bytes, _content_type : Mime) -> Result<Self, Self::Err>
|
fn from_body(body: Bytes, _content_type: Mime) -> Result<Self, Self::Err> {
|
||||||
{
|
|
||||||
serde_json::from_slice(&body)
|
serde_json::from_slice(&body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A type that can be used inside a request body. Implemented for every type that is deserializable
|
A type that can be used inside a request body. Implemented for every type that is deserializable
|
||||||
with serde. If the `openapi` feature is used, it must also be of type [`OpenapiType`].
|
with serde. If the `openapi` feature is used, it must also be of type [OpenapiType].
|
||||||
|
|
||||||
If you want a non-deserializable type to be used as a request body, e.g. because you'd like to
|
If you want a non-deserializable type to be used as a request body, e.g. because you'd like to
|
||||||
get the raw data, you can derive it for your own type. All you need is to have a type implementing
|
get the raw data, you can derive it for your own type. All you need is to have a type implementing
|
||||||
[`FromBody`] and optionally a list of supported media types:
|
[FromBody] and optionally a list of supported media types:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate gotham_restful;
|
# #[macro_use] extern crate gotham_restful;
|
||||||
|
@ -105,33 +83,17 @@ struct RawImage {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[`FromBody`]: trait.FromBody.html
|
[OpenapiType]: trait.OpenapiType.html
|
||||||
[`OpenapiType`]: trait.OpenapiType.html
|
|
||||||
*/
|
*/
|
||||||
pub trait RequestBody : ResourceType + FromBody
|
pub trait RequestBody: ResourceType + FromBody {
|
||||||
{
|
|
||||||
/// Return all types that are supported as content types. Use `None` if all types are supported.
|
/// Return all types that are supported as content types. Use `None` if all types are supported.
|
||||||
fn supported_types() -> Option<Vec<Mime>>
|
fn supported_types() -> Option<Vec<Mime>> {
|
||||||
{
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T : ResourceType + DeserializeOwned> RequestBody for T
|
impl<T: ResourceType + DeserializeOwned> RequestBody for T {
|
||||||
{
|
fn supported_types() -> Option<Vec<Mime>> {
|
||||||
fn supported_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
Some(vec![APPLICATION_JSON])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A type than can be used as a parameter to a resource method. Implemented for every type
|
|
||||||
/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of
|
|
||||||
/// type `OpenapiType`.
|
|
||||||
pub trait ResourceID : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync> ResourceID for T
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,106 +1,133 @@
|
||||||
#[macro_use] extern crate gotham_derive;
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
hyper::{HeaderMap, Method},
|
||||||
router::builder::*,
|
router::builder::*,
|
||||||
|
state::State,
|
||||||
test::TestServer
|
test::TestServer
|
||||||
};
|
};
|
||||||
use gotham_restful::*;
|
use gotham_restful::*;
|
||||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
mod util { include!("util/mod.rs"); }
|
mod util {
|
||||||
use util::{test_get_response, test_post_response, test_put_response, test_delete_response};
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
|
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[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;
|
struct FooResource;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct FooBody
|
struct FooBody {
|
||||||
{
|
data: String
|
||||||
data : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct FooSearch
|
struct FooSearch {
|
||||||
{
|
query: String
|
||||||
query : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
async fn read_all() -> Raw<&'static [u8]>
|
async fn read_all() -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
async fn read(_id : u64) -> Raw<&'static [u8]>
|
async fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||||
#[search(FooResource)]
|
#[search]
|
||||||
async fn search(_body : FooSearch) -> Raw<&'static [u8]>
|
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||||
#[create(FooResource)]
|
#[create]
|
||||||
async fn create(_body : FooBody) -> Raw<&'static [u8]>
|
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||||
#[change_all(FooResource)]
|
#[change_all]
|
||||||
async fn change_all(_body : FooBody) -> Raw<&'static [u8]>
|
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||||
#[change(FooResource)]
|
#[change]
|
||||||
async fn change(_id : u64, _body : FooBody) -> Raw<&'static [u8]>
|
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||||
#[remove_all(FooResource)]
|
#[remove_all]
|
||||||
async fn remove_all() -> Raw<&'static [u8]>
|
async fn remove_all() -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||||
#[remove(FooResource)]
|
#[remove]
|
||||||
async fn remove(_id : u64) -> Raw<&'static [u8]>
|
async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]
|
#[test]
|
||||||
fn async_methods()
|
fn async_methods() {
|
||||||
{
|
let _ = pretty_env_logger::try_init_timed();
|
||||||
|
|
||||||
let server = TestServer::new(build_simple_router(|router| {
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
router.resource::<FooResource>("foo");
|
router.resource::<FooResource>("foo");
|
||||||
})).unwrap();
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE);
|
test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE);
|
||||||
test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE);
|
test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE);
|
||||||
test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE);
|
test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE);
|
||||||
test_post_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CREATE_RESPONSE);
|
test_post_response(
|
||||||
test_put_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_ALL_RESPONSE);
|
&server,
|
||||||
test_put_response(&server, "http://localhost/foo/1", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_RESPONSE);
|
"http://localhost/foo",
|
||||||
|
r#"{"data":"hello world"}"#,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
CREATE_RESPONSE
|
||||||
|
);
|
||||||
|
test_put_response(
|
||||||
|
&server,
|
||||||
|
"http://localhost/foo",
|
||||||
|
r#"{"data":"hello world"}"#,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
CHANGE_ALL_RESPONSE
|
||||||
|
);
|
||||||
|
test_put_response(
|
||||||
|
&server,
|
||||||
|
"http://localhost/foo/1",
|
||||||
|
r#"{"data":"hello world"}"#,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
CHANGE_RESPONSE
|
||||||
|
);
|
||||||
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
|
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
|
||||||
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
|
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
|
||||||
|
test_get_response(&server, "http://localhost/foo/state_test", STATE_TEST_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,124 +5,285 @@ use gotham::{
|
||||||
router::builder::*,
|
router::builder::*,
|
||||||
test::{Server, TestRequest, TestServer}
|
test::{Server, TestRequest, TestServer}
|
||||||
};
|
};
|
||||||
use gotham_restful::{CorsConfig, DrawResources, Origin, Raw, Resource, change_all, read_all};
|
use gotham_restful::{
|
||||||
use itertools::Itertools;
|
change_all,
|
||||||
|
cors::{Headers, Origin},
|
||||||
|
read_all, CorsConfig, DrawResources, Raw, Resource
|
||||||
|
};
|
||||||
use mime::TEXT_PLAIN;
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all, change_all)]
|
#[resource(read_all, change_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all()
|
fn read_all() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[change_all(FooResource)]
|
#[change_all]
|
||||||
fn change_all(_body : Raw<Vec<u8>>)
|
fn change_all(_body: Raw<Vec<u8>>) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_server(cfg : CorsConfig) -> TestServer
|
fn test_server(cfg: CorsConfig) -> TestServer {
|
||||||
{
|
|
||||||
let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build());
|
let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build());
|
||||||
TestServer::new(build_router(chain, pipeline, |router| {
|
TestServer::new(build_router(chain, pipeline, |router| router.resource::<FooResource>("/foo"))).unwrap()
|
||||||
router.resource::<FooResource>("/foo")
|
|
||||||
})).unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_response<TS, C>(req : TestRequest<TS, C>, origin : Option<&str>, vary : Option<&str>, credentials : bool)
|
fn test_response<TS, C>(req: TestRequest<TS, C>, origin: Option<&str>, vary: Option<&str>, credentials: bool)
|
||||||
where
|
where
|
||||||
TS : Server + 'static,
|
TS: Server + 'static,
|
||||||
C : Connect + Clone + Send + Sync + 'static
|
C: Connect + Clone + Send + Sync + 'static
|
||||||
{
|
{
|
||||||
let res = req.with_header(ORIGIN, "http://example.org".parse().unwrap()).perform().unwrap();
|
let res = req
|
||||||
|
.with_header(ORIGIN, "http://example.org".parse().unwrap())
|
||||||
|
.perform()
|
||||||
|
.unwrap();
|
||||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||||
let headers = res.headers();
|
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).and_then(|value| value.to_str().ok()).as_deref(), origin);
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.as_deref(),
|
||||||
|
origin
|
||||||
|
);
|
||||||
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), vary);
|
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), vary);
|
||||||
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_CREDENTIALS).and_then(|value| value.to_str().ok()).map(|value| value == "true").unwrap_or(false), credentials);
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.get(ACCESS_CONTROL_ALLOW_CREDENTIALS)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.map(|value| value == "true")
|
||||||
|
.unwrap_or(false),
|
||||||
|
credentials
|
||||||
|
);
|
||||||
assert!(headers.get(ACCESS_CONTROL_MAX_AGE).is_none());
|
assert!(headers.get(ACCESS_CONTROL_MAX_AGE).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_preflight(server : &TestServer, method : &str, origin : Option<&str>, vary : &str, credentials : bool, max_age : u64)
|
fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary: &str, credentials: bool, max_age: u64) {
|
||||||
{
|
let res = server
|
||||||
let res = server.client().options("http://example.org/foo")
|
.client()
|
||||||
|
.options("http://example.org/foo")
|
||||||
.with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap())
|
.with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap())
|
||||||
.with_header(ORIGIN, "http://example.org".parse().unwrap())
|
.with_header(ORIGIN, "http://example.org".parse().unwrap())
|
||||||
.perform().unwrap();
|
.perform()
|
||||||
|
.unwrap();
|
||||||
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||||
let headers = res.headers();
|
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).and_then(|value| value.to_str().ok()).as_deref(), Some(method));
|
assert_eq!(
|
||||||
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_ORIGIN).and_then(|value| value.to_str().ok()).as_deref(), origin);
|
headers
|
||||||
|
.get(ACCESS_CONTROL_ALLOW_METHODS)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.as_deref(),
|
||||||
|
Some(method)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.get(ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.as_deref(),
|
||||||
|
origin
|
||||||
|
);
|
||||||
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary));
|
assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary));
|
||||||
assert_eq!(headers.get(ACCESS_CONTROL_ALLOW_CREDENTIALS).and_then(|value| value.to_str().ok()).map(|value| value == "true").unwrap_or(false), credentials);
|
assert_eq!(
|
||||||
assert_eq!(headers.get(ACCESS_CONTROL_MAX_AGE).and_then(|value| value.to_str().ok()).and_then(|value| value.parse().ok()), Some(max_age));
|
headers
|
||||||
|
.get(ACCESS_CONTROL_ALLOW_CREDENTIALS)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.map(|value| value == "true")
|
||||||
|
.unwrap_or(false),
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
headers
|
||||||
|
.get(ACCESS_CONTROL_MAX_AGE)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.and_then(|value| value.parse().ok()),
|
||||||
|
Some(max_age)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]
|
#[test]
|
||||||
fn cors_origin_none()
|
fn cors_origin_none() {
|
||||||
{
|
|
||||||
let cfg = Default::default();
|
let cfg = Default::default();
|
||||||
let server = test_server(cfg);
|
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(server.client().get("http://example.org/foo"), None, None, false);
|
||||||
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false);
|
test_response(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cors_origin_star()
|
fn cors_origin_star() {
|
||||||
{
|
|
||||||
let cfg = CorsConfig {
|
let cfg = CorsConfig {
|
||||||
origin: Origin::Star,
|
origin: Origin::Star,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let server = test_server(cfg);
|
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(server.client().get("http://example.org/foo"), Some("*"), None, false);
|
||||||
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("*"), None, false);
|
test_response(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
Some("*"),
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cors_origin_single()
|
fn cors_origin_single() {
|
||||||
{
|
|
||||||
let cfg = CorsConfig {
|
let cfg = CorsConfig {
|
||||||
origin: Origin::Single("https://foo.com".to_owned()),
|
origin: Origin::Single("https://foo.com".to_owned()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let server = test_server(cfg);
|
let server = test_server(cfg);
|
||||||
|
|
||||||
test_preflight(&server, "PUT", Some("https://foo.com"), "Access-Control-Request-Method", false, 0);
|
test_preflight(
|
||||||
|
&server,
|
||||||
|
"PUT",
|
||||||
|
Some("https://foo.com"),
|
||||||
|
"access-control-request-method",
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
test_response(server.client().get("http://example.org/foo"), Some("https://foo.com"), None, false);
|
test_response(
|
||||||
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("https://foo.com"), None, false);
|
server.client().get("http://example.org/foo"),
|
||||||
|
Some("https://foo.com"),
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
test_response(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
Some("https://foo.com"),
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cors_origin_copy()
|
fn cors_origin_copy() {
|
||||||
{
|
|
||||||
let cfg = CorsConfig {
|
let cfg = CorsConfig {
|
||||||
origin: Origin::Copy,
|
origin: Origin::Copy,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let server = test_server(cfg);
|
let server = test_server(cfg);
|
||||||
|
|
||||||
test_preflight(&server, "PUT", Some("http://example.org"), "Access-Control-Request-Method,Origin", false, 0);
|
test_preflight(
|
||||||
|
&server,
|
||||||
|
"PUT",
|
||||||
|
Some("http://example.org"),
|
||||||
|
"access-control-request-method,origin",
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
test_response(server.client().get("http://example.org/foo"), Some("http://example.org"), Some("Origin"), false);
|
test_response(
|
||||||
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("http://example.org"), Some("Origin"), false);
|
server.client().get("http://example.org/foo"),
|
||||||
|
Some("http://example.org"),
|
||||||
|
Some("origin"),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
test_response(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
Some("http://example.org"),
|
||||||
|
Some("origin"),
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cors_credentials()
|
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 {
|
let cfg = CorsConfig {
|
||||||
origin: Origin::None,
|
origin: Origin::None,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
@ -130,15 +291,19 @@ fn cors_credentials()
|
||||||
};
|
};
|
||||||
let server = test_server(cfg);
|
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(server.client().get("http://example.org/foo"), None, None, true);
|
||||||
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, true);
|
test_response(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cors_max_age()
|
fn cors_max_age() {
|
||||||
{
|
|
||||||
let cfg = CorsConfig {
|
let cfg = CorsConfig {
|
||||||
origin: Origin::None,
|
origin: Origin::None,
|
||||||
max_age: 31536000,
|
max_age: 31536000,
|
||||||
|
@ -146,8 +311,13 @@ fn cors_max_age()
|
||||||
};
|
};
|
||||||
let server = test_server(cfg);
|
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(server.client().get("http://example.org/foo"), None, None, false);
|
||||||
test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false);
|
test_response(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
use gotham::{
|
use gotham::{hyper::header::CONTENT_TYPE, router::builder::*, test::TestServer};
|
||||||
hyper::header::CONTENT_TYPE,
|
|
||||||
router::builder::*,
|
|
||||||
test::TestServer
|
|
||||||
};
|
|
||||||
use gotham_restful::*;
|
use gotham_restful::*;
|
||||||
use mime::TEXT_PLAIN;
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
|
const RESPONSE: &[u8] = b"This is the only valid response.";
|
||||||
const RESPONSE : &[u8] = b"This is the only valid response.";
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(create)]
|
#[resource(create)]
|
||||||
|
@ -20,24 +15,25 @@ struct Foo {
|
||||||
content_type: Mime
|
content_type: Mime
|
||||||
}
|
}
|
||||||
|
|
||||||
#[create(FooResource)]
|
#[create]
|
||||||
fn create(body : Foo) -> Raw<Vec<u8>> {
|
fn create(body: Foo) -> Raw<Vec<u8>> {
|
||||||
Raw::new(body.content, body.content_type)
|
Raw::new(body.content, body.content_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_request_body()
|
fn custom_request_body() {
|
||||||
{
|
|
||||||
let server = TestServer::new(build_simple_router(|router| {
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
router.resource::<FooResource>("foo");
|
router.resource::<FooResource>("foo");
|
||||||
})).unwrap();
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let res = server.client()
|
let res = server
|
||||||
|
.client()
|
||||||
.post("http://localhost/foo", RESPONSE, TEXT_PLAIN)
|
.post("http://localhost/foo", RESPONSE, TEXT_PLAIN)
|
||||||
.perform().unwrap();
|
.perform()
|
||||||
|
.unwrap();
|
||||||
assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain");
|
assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain");
|
||||||
let res = res.read_body().unwrap();
|
let res = res.read_body().unwrap();
|
||||||
let body : &[u8] = res.as_ref();
|
let body: &[u8] = res.as_ref();
|
||||||
assert_eq!(body, RESPONSE);
|
assert_eq!(body, RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,56 @@
|
||||||
},
|
},
|
||||||
"openapi": "3.0.2",
|
"openapi": "3.0.2",
|
||||||
"paths": {
|
"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}": {
|
"/img/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getImage",
|
"operationId": "getImage",
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))]
|
#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))]
|
||||||
|
|
||||||
#[macro_use] extern crate gotham_derive;
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
hyper::Method,
|
||||||
pipeline::{new_pipeline, single::single_pipeline},
|
pipeline::{new_pipeline, single::single_pipeline},
|
||||||
router::builder::*,
|
router::builder::*,
|
||||||
test::TestServer
|
test::TestServer
|
||||||
|
@ -13,86 +15,91 @@ use mime::IMAGE_PNG;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod util { include!("util/mod.rs"); }
|
mod util {
|
||||||
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
use util::{test_get_response, test_openapi_response};
|
use util::{test_get_response, test_openapi_response};
|
||||||
|
|
||||||
|
|
||||||
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read, change)]
|
#[resource(get_image, set_image)]
|
||||||
struct ImageResource;
|
struct ImageResource;
|
||||||
|
|
||||||
#[derive(FromBody, RequestBody)]
|
#[derive(FromBody, RequestBody)]
|
||||||
#[supported_types(IMAGE_PNG)]
|
#[supported_types(IMAGE_PNG)]
|
||||||
struct Image(Vec<u8>);
|
struct Image(Vec<u8>);
|
||||||
|
|
||||||
#[read(ImageResource, operation_id = "getImage")]
|
#[read(operation_id = "getImage")]
|
||||||
fn get_image(_id : u64) -> Raw<&'static [u8]>
|
fn get_image(_id: u64) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[change(ImageResource, operation_id = "setImage")]
|
#[change(operation_id = "setImage")]
|
||||||
fn set_image(_id : u64, _image : Image)
|
fn set_image(_id: u64, _image: Image) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read, search)]
|
#[resource(read_secret, search_secret)]
|
||||||
struct SecretResource;
|
struct SecretResource;
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
struct AuthData
|
struct AuthData {
|
||||||
{
|
sub: String,
|
||||||
sub : String,
|
iat: u64,
|
||||||
iat : u64,
|
exp: u64
|
||||||
exp : u64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthStatus = gotham_restful::AuthStatus<AuthData>;
|
type AuthStatus = gotham_restful::AuthStatus<AuthData>;
|
||||||
|
|
||||||
#[derive(OpenapiType, Serialize)]
|
#[derive(OpenapiType, Serialize)]
|
||||||
struct Secret
|
struct Secret {
|
||||||
{
|
code: f32
|
||||||
code : f32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(OpenapiType, Serialize)]
|
#[derive(OpenapiType, Serialize)]
|
||||||
struct Secrets
|
struct Secrets {
|
||||||
{
|
secrets: Vec<Secret>
|
||||||
secrets : Vec<Secret>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
|
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
|
||||||
struct SecretQuery
|
struct SecretQuery {
|
||||||
{
|
date: NaiveDate,
|
||||||
date : NaiveDate,
|
hour: Option<u16>,
|
||||||
hour : Option<u16>,
|
minute: Option<u16>
|
||||||
minute : Option<u16>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[read(SecretResource)]
|
#[read]
|
||||||
fn read_secret(auth : AuthStatus, _id : NaiveDateTime) -> AuthSuccess<Secret>
|
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
|
||||||
{
|
|
||||||
auth.ok()?;
|
auth.ok()?;
|
||||||
Ok(Secret { code: 4.2 })
|
Ok(Secret { code: 4.2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[search(SecretResource)]
|
#[search]
|
||||||
fn search_secret(auth : AuthStatus, _query : SecretQuery) -> AuthSuccess<Secrets>
|
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
|
||||||
{
|
|
||||||
auth.ok()?;
|
auth.ok()?;
|
||||||
Ok(Secrets {
|
Ok(Secrets {
|
||||||
secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }]
|
secrets: vec![Secret { code: 4.2 }, Secret { code: 3.14 }]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn openapi_supports_scope()
|
fn openapi_specification() {
|
||||||
{
|
|
||||||
let info = OpenapiInfo {
|
let info = OpenapiInfo {
|
||||||
title: "This is just a test".to_owned(),
|
title: "This is just a test".to_owned(),
|
||||||
version: "1.2.3".to_owned(),
|
version: "1.2.3".to_owned(),
|
||||||
|
@ -107,10 +114,12 @@ fn openapi_supports_scope()
|
||||||
let server = TestServer::new(build_router(chain, pipelines, |router| {
|
let server = TestServer::new(build_router(chain, pipelines, |router| {
|
||||||
router.with_openapi(info, |mut router| {
|
router.with_openapi(info, |mut router| {
|
||||||
router.resource::<ImageResource>("img");
|
router.resource::<ImageResource>("img");
|
||||||
router.get_openapi("openapi");
|
|
||||||
router.resource::<SecretResource>("secret");
|
router.resource::<SecretResource>("secret");
|
||||||
|
router.resource::<CustomResource>("custom");
|
||||||
|
router.get_openapi("openapi");
|
||||||
});
|
});
|
||||||
})).unwrap();
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json");
|
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,27 @@
|
||||||
#![cfg(feature = "openapi")]
|
#![cfg(feature = "openapi")]
|
||||||
use gotham::{
|
use gotham::{router::builder::*, test::TestServer};
|
||||||
router::builder::*,
|
|
||||||
test::TestServer
|
|
||||||
};
|
|
||||||
use gotham_restful::*;
|
use gotham_restful::*;
|
||||||
use mime::TEXT_PLAIN;
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod util { include!("util/mod.rs"); }
|
mod util {
|
||||||
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
use util::{test_get_response, test_openapi_response};
|
use util::{test_get_response, test_openapi_response};
|
||||||
|
|
||||||
|
const RESPONSE: &[u8] = b"This is the only valid response.";
|
||||||
const RESPONSE : &[u8] = b"This is the only valid response.";
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() -> Raw<&'static [u8]>
|
fn read_all() -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(RESPONSE, TEXT_PLAIN)
|
Raw::new(RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openapi_supports_scope()
|
fn openapi_supports_scope() {
|
||||||
{
|
|
||||||
let info = OpenapiInfo {
|
let info = OpenapiInfo {
|
||||||
title: "Test".to_owned(),
|
title: "Test".to_owned(),
|
||||||
version: "1.2.3".to_owned(),
|
version: "1.2.3".to_owned(),
|
||||||
|
@ -44,7 +39,8 @@ fn openapi_supports_scope()
|
||||||
});
|
});
|
||||||
router.resource::<FooResource>("foo4");
|
router.resource::<FooResource>("foo4");
|
||||||
});
|
});
|
||||||
})).unwrap();
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_get_response(&server, "http://localhost/foo1", RESPONSE);
|
test_get_response(&server, "http://localhost/foo1", RESPONSE);
|
||||||
test_get_response(&server, "http://localhost/bar/foo2", RESPONSE);
|
test_get_response(&server, "http://localhost/bar/foo2", RESPONSE);
|
||||||
|
|
37
tests/resource_error.rs
Normal file
37
tests/resource_error.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use gotham_restful::ResourceError;
|
||||||
|
|
||||||
|
#[derive(ResourceError)]
|
||||||
|
enum Error {
|
||||||
|
#[display("I/O Error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[status(INTERNAL_SERVER_ERROR)]
|
||||||
|
#[display("Internal Server Error: {0}")]
|
||||||
|
InternalServerError(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
mod resource_error {
|
||||||
|
use super::Error;
|
||||||
|
use gotham::hyper::StatusCode;
|
||||||
|
use gotham_restful::IntoResponseError;
|
||||||
|
use mime::APPLICATION_JSON;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn internal_server_error() {
|
||||||
|
let err = Error::InternalServerError("Brocken".to_owned());
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,17 @@
|
||||||
#[macro_use] extern crate gotham_derive;
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
use gotham::{
|
use gotham::{router::builder::*, test::TestServer};
|
||||||
router::builder::*,
|
|
||||||
test::TestServer
|
|
||||||
};
|
|
||||||
use gotham_restful::*;
|
use gotham_restful::*;
|
||||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
mod util { include!("util/mod.rs"); }
|
mod util {
|
||||||
use util::{test_get_response, test_post_response, test_put_response, test_delete_response};
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
|
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[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)]
|
||||||
|
@ -19,88 +20,98 @@ struct FooResource;
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct FooBody
|
struct FooBody {
|
||||||
{
|
data: String
|
||||||
data : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct FooSearch
|
struct FooSearch {
|
||||||
{
|
query: String
|
||||||
query : String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||||
#[read_all(FooResource)]
|
#[read_all]
|
||||||
fn read_all() -> Raw<&'static [u8]>
|
fn read_all() -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||||
#[read(FooResource)]
|
#[read]
|
||||||
fn read(_id : u64) -> Raw<&'static [u8]>
|
fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||||
#[search(FooResource)]
|
#[search]
|
||||||
fn search(_body : FooSearch) -> Raw<&'static [u8]>
|
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||||
#[create(FooResource)]
|
#[create]
|
||||||
fn create(_body : FooBody) -> Raw<&'static [u8]>
|
fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||||
#[change_all(FooResource)]
|
#[change_all]
|
||||||
fn change_all(_body : FooBody) -> Raw<&'static [u8]>
|
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||||
#[change(FooResource)]
|
#[change]
|
||||||
fn change(_id : u64, _body : FooBody) -> Raw<&'static [u8]>
|
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||||
#[remove_all(FooResource)]
|
#[remove_all]
|
||||||
fn remove_all() -> Raw<&'static [u8]>
|
fn remove_all() -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||||
#[remove(FooResource)]
|
#[remove]
|
||||||
fn remove(_id : u64) -> Raw<&'static [u8]>
|
fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
{
|
|
||||||
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_methods()
|
fn sync_methods() {
|
||||||
{
|
let _ = pretty_env_logger::try_init_timed();
|
||||||
|
|
||||||
let server = TestServer::new(build_simple_router(|router| {
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
router.resource::<FooResource>("foo");
|
router.resource::<FooResource>("foo");
|
||||||
})).unwrap();
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE);
|
test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE);
|
||||||
test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE);
|
test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE);
|
||||||
test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE);
|
test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE);
|
||||||
test_post_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CREATE_RESPONSE);
|
test_post_response(
|
||||||
test_put_response(&server, "http://localhost/foo", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_ALL_RESPONSE);
|
&server,
|
||||||
test_put_response(&server, "http://localhost/foo/1", r#"{"data":"hello world"}"#, APPLICATION_JSON, CHANGE_RESPONSE);
|
"http://localhost/foo",
|
||||||
|
r#"{"data":"hello world"}"#,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
CREATE_RESPONSE
|
||||||
|
);
|
||||||
|
test_put_response(
|
||||||
|
&server,
|
||||||
|
"http://localhost/foo",
|
||||||
|
r#"{"data":"hello world"}"#,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
CHANGE_ALL_RESPONSE
|
||||||
|
);
|
||||||
|
test_put_response(
|
||||||
|
&server,
|
||||||
|
"http://localhost/foo/1",
|
||||||
|
r#"{"data":"hello world"}"#,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
CHANGE_RESPONSE
|
||||||
|
);
|
||||||
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
|
test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE);
|
||||||
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
|
test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,29 +2,9 @@ use trybuild::TestCases;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn trybuild_ui()
|
fn trybuild_ui() {
|
||||||
{
|
|
||||||
let t = TestCases::new();
|
let t = TestCases::new();
|
||||||
|
t.compile_fail("tests/ui/endpoint/*.rs");
|
||||||
// always enabled
|
t.compile_fail("tests/ui/from_body/*.rs");
|
||||||
t.compile_fail("tests/ui/from_body_enum.rs");
|
t.compile_fail("tests/ui/resource/*.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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
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)]
|
#[derive(Resource)]
|
||||||
#[resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct FooResource;
|
struct FooResource;
|
||||||
|
|
||||||
#[read_all(FooResource)]
|
#[read_all(FooResource)]
|
||||||
fn read_all(self)
|
fn read_all() {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main()
|
fn main() {}
|
||||||
{
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue