mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-04-19 22:44:38 +00:00
Compare commits
176 commits
Author | SHA1 | Date | |
---|---|---|---|
7bac379e05 | |||
2dd3f3e21a | |||
e206ab10eb | |||
a5257608e3 | |||
9c7f681e3d | |||
63567f5480 | |||
3a3f743369 | |||
ebea39fe0d | |||
eecd192458 | |||
2a35e044db | |||
a57f1c097d | |||
5f60599c41 | |||
43d3a1cd89 | |||
667009bd22 | |||
d9c7f4135f | |||
90870e3b6a | |||
2251c29d7b | |||
09dee5a673 | |||
fabcbc4e78 | |||
960ba0e8bc | |||
bb6f5b0fdd | |||
![]() |
31f92c07cd | ||
28ae4dfdee | |||
![]() |
7ed98c82e8 | ||
e7ef6bdf5a | |||
666514c8e2 | |||
![]() |
7de11cdae1 | ||
c640efcb88 | |||
30edd349ed | |||
90fc17e57d | |||
3b95b9f495 | |||
1bd398c7ee | |||
8b73701405 | |||
9e65540cd8 | |||
44e2f0317c | |||
![]() |
af28e0d916 | ||
![]() |
441a42c75e | ||
cf0223473f | |||
f2bcc8438f | |||
681ef5d894 | |||
70914d107b | |||
![]() |
5261aa9931 | ||
002cfb1b4d | |||
![]() |
b807ae2796 | ||
0ac0f0f504 | |||
0251c03ceb | |||
75cd7e2c96 | |||
edd8bb618d | |||
daea3ba9ec | |||
b7a1193333 | |||
44f3c9fe84 | |||
3600a115d0 | |||
388bf8b49c | |||
6ee382242b | |||
813c12614f | |||
766bc9d17d | |||
b005346e54 | |||
141e5ac2d7 | |||
2b8796b9c9 | |||
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 | |||
f72b9ac797 | |||
912f030bfd | |||
0b06528742 | |||
c1cb0e692a | |||
8321b63982 | |||
e5e9cd5d3c | |||
81803fd54a | |||
7268cc0567 | |||
b39b30694e | |||
955715eea6 | |||
dc26e9a02e | |||
94abc75268 | |||
20818b0f95 | |||
4ff5a8d7e4 | |||
![]() |
604494651d | ||
74ef0af512 | |||
f20c768d02 | |||
748bf65d3e | |||
40c90e6b4a | |||
b9002bd70d | |||
6680887b84 | |||
9ed24c9bcb | |||
e2eb9b0fcc | |||
b1b9858da4 | |||
e05f9bb963 | |||
4bf0bd7b09 | |||
ea80689db2 | |||
f8181bcb7e | |||
e470f060e3 | |||
b1801f2486 | |||
5587ded60d | |||
52679ad29d | |||
4cd2474d90 | |||
bccefa8248 | |||
e5f13792c6 | |||
aa9fa0f457 | |||
e7e55514a2 | |||
022edede62 | |||
![]() |
d8c4215cc2 | ||
a1acc06f6d | |||
cc86d3396c | |||
3de130e104 | |||
110ef2be7a | |||
7ef964b0a0 | |||
328ebf821e | |||
992d9be195 | |||
5e5e3aaf9d | |||
da30f34d97 | |||
f7157dcf62 | |||
101e94b900 | |||
![]() |
0d95ca4abb | ||
![]() |
d754d6044d | ||
8593e133b7 | |||
a36993f615 | |||
cd7cf07318 | |||
e013af8e18 | |||
45eac21726 | |||
9fd0bceaf4 | |||
01f818e268 | |||
b4eaeca01c | |||
96317cdfb7 | |||
![]() |
805df80971 | ||
![]() |
4ce53bc361 | ||
d08d9bea8c | |||
147ea980bf | |||
ad6e3dd00d | |||
876f44ceff | |||
f70865d246 | |||
b6006797f4 | |||
8834f3f64b | |||
1e607bbcc9 | |||
fdc34fc296 | |||
45cad64923 | |||
523d01d443 | |||
310d7f79d5 | |||
63e6eb9b32 | |||
a493071ff8 | |||
40e6d1bc03 | |||
694b45ea60 | |||
89f6494b51 | |||
427c836f52 | |||
d7282786b1 | |||
06e6c93a46 | |||
a8ae939019 | |||
ede0d75161 | |||
f425f21ff3 |
145 changed files with 8663 additions and 4160 deletions
126
.gitlab-ci.yml
126
.gitlab-ci.yml
|
@ -1,59 +1,143 @@
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
- build
|
||||||
- publish
|
- publish
|
||||||
|
|
||||||
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
|
image: rust:slim
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo test --workspace --lib
|
- cargo check --manifest-path example/Cargo.toml
|
||||||
- cargo test --workspace --doc
|
|
||||||
cache:
|
cache:
|
||||||
|
key: cargo-stable-example
|
||||||
paths:
|
paths:
|
||||||
- cargo/
|
- cargo/
|
||||||
- target/
|
- target/
|
||||||
|
|
||||||
test-all:
|
test-default:
|
||||||
stage: test
|
stage: test
|
||||||
image: msrd0/rust:alpine-tarpaulin
|
image: rust:1.49-slim
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
script:
|
script:
|
||||||
- cargo test --workspace --all-features --doc
|
- cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild
|
||||||
- cargo tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'gotham_restful_derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v
|
- cargo test
|
||||||
|
cache:
|
||||||
|
key: cargo-1-49-default
|
||||||
|
paths:
|
||||||
|
- cargo/
|
||||||
|
- target/
|
||||||
|
|
||||||
|
test-full:
|
||||||
|
stage: test
|
||||||
|
image: rust:1.49-slim
|
||||||
|
before_script:
|
||||||
|
- apt update -y
|
||||||
|
- apt install -y --no-install-recommends libpq-dev
|
||||||
|
- cargo -V
|
||||||
|
script:
|
||||||
|
- cargo test --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-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 -r gotham_restful -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
|
||||||
|
|
||||||
publish:
|
rustfmt:
|
||||||
stage: publish
|
stage: test
|
||||||
image: msrd0/rust:alpine
|
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:
|
||||||
|
stage: build
|
||||||
|
image: rust:slim
|
||||||
before_script:
|
before_script:
|
||||||
- cargo -V
|
- cargo -V
|
||||||
- cargo login $CRATES_IO_TOKEN
|
|
||||||
script:
|
script:
|
||||||
- cd gotham_restful_derive
|
- cargo doc --no-default-features --features full
|
||||||
- cargo publish
|
artifacts:
|
||||||
- sleep 10s
|
paths:
|
||||||
- cd ../gotham_restful
|
- target/doc/
|
||||||
- cargo publish
|
cache:
|
||||||
- cd ..
|
key: cargo-stable-doc
|
||||||
|
paths:
|
||||||
|
- cargo/
|
||||||
|
- target/
|
||||||
|
|
||||||
|
pages:
|
||||||
|
stage: publish
|
||||||
|
image: busybox
|
||||||
|
script:
|
||||||
|
- mv target/doc public
|
||||||
|
- 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:
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
only:
|
only:
|
||||||
- tags
|
- master
|
||||||
|
|
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
|
80
Cargo.toml
80
Cargo.toml
|
@ -1,13 +1,77 @@
|
||||||
# -*- eval: (cargo-minor-mode 1) -*-
|
# -*- eval: (cargo-minor-mode 1) -*-
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"]
|
||||||
"gotham_restful",
|
|
||||||
"gotham_restful_derive",
|
[package]
|
||||||
"example"
|
name = "gotham_restful"
|
||||||
]
|
version = "0.3.0-dev"
|
||||||
|
authors = ["Dominic Meiser <git@msrd0.de>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "RESTful additions for the gotham web framework"
|
||||||
|
keywords = ["gotham", "rest", "restful", "web", "http"]
|
||||||
|
categories = ["web-programming", "web-programming::http-server"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://gitlab.com/msrd0/gotham-restful"
|
||||||
|
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||||
|
|
||||||
|
[badges]
|
||||||
|
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
futures-core = "0.3.7"
|
||||||
|
futures-util = "0.3.7"
|
||||||
|
gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false }
|
||||||
|
gotham_derive = "0.5.0"
|
||||||
|
gotham_restful_derive = "0.3.0-dev"
|
||||||
|
log = "0.4.8"
|
||||||
|
mime = "0.3.16"
|
||||||
|
serde = { version = "1.0.110", features = ["derive"] }
|
||||||
|
serde_json = "1.0.58"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
# non-feature optional dependencies
|
||||||
|
base64 = { version = "0.13.0", optional = true }
|
||||||
|
cookie = { version = "0.15", optional = true }
|
||||||
|
gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", optional = true }
|
||||||
|
indexmap = { version = "1.3.2", optional = true }
|
||||||
|
indoc = { version = "1.0", optional = true }
|
||||||
|
jsonwebtoken = { version = "7.1.0", optional = true }
|
||||||
|
once_cell = { version = "1.5", optional = true }
|
||||||
|
openapiv3 = { version = "=0.3.2", optional = true }
|
||||||
|
openapi_type = { version = "0.1.0-dev", optional = true }
|
||||||
|
regex = { version = "1.4", optional = true }
|
||||||
|
sha2 = { version = "0.9.3", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
diesel = { version = "1.4.4", features = ["postgres"] }
|
||||||
|
futures-executor = "0.3.5"
|
||||||
|
paste = "1.0"
|
||||||
|
pretty_env_logger = "0.4"
|
||||||
|
tokio = { version = "1.0", features = ["time"], default-features = false }
|
||||||
|
thiserror = "1.0.18"
|
||||||
|
trybuild = "1.0.27"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["cors", "errorlog", "without-openapi"]
|
||||||
|
full = ["auth", "cors", "database", "errorlog", "openapi"]
|
||||||
|
|
||||||
|
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
||||||
|
cors = []
|
||||||
|
database = ["gotham_restful_derive/database", "gotham_middleware_diesel"]
|
||||||
|
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]
|
||||||
|
no-default-features = true
|
||||||
|
features = ["full"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
gotham_restful = { path = "./gotham_restful" }
|
gotham_restful = { path = "." }
|
||||||
gotham_restful_derive = { path = "./gotham_restful_derive" }
|
gotham_restful_derive = { path = "./derive" }
|
||||||
openapiv3 = { git = "https://github.com/glademiller/openapiv3", rev = "4c3bd95c966a3f9d59bb494c3d8e30c5c3068bdb" }
|
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)
|
|
||||||
|
|
108
README.md
108
README.md
|
@ -1,108 +1,4 @@
|
||||||
<div align="center">
|
# Moved to GitHub
|
||||||
<h1>gotham-restful</h1>
|
|
||||||
</div>
|
|
||||||
<div align="center">
|
|
||||||
<a href="https://gitlab.com/msrd0/gotham-restful/-/commits/master">
|
|
||||||
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://gitlab.com/msrd0/gotham-restful/-/commits/master">
|
|
||||||
<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://www.rust-lang.org/en-US/">
|
|
||||||
<img alt="Build with Rust" src="https://img.shields.io/badge/Made%20with-Rust-orange.svg"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html">
|
|
||||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.40+-yellow.svg"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to
|
This project has moved to GitHub: https://github.com/msrd0/gotham_restful
|
||||||
have several RESTful resources that can be added to the gotham router. This crate will take care
|
|
||||||
of everything else, like parsing path/query parameters, request bodies, and writing response
|
|
||||||
bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)serializing. If you
|
|
||||||
enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful
|
|
||||||
resources.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
A basic server with only one resource, handling a simple `GET` request, could look like this:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
/// Our RESTful Resource.
|
|
||||||
#[derive(Resource)]
|
|
||||||
#[rest_resource(read_all)]
|
|
||||||
struct UsersResource;
|
|
||||||
|
|
||||||
/// Our return type.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
struct User {
|
|
||||||
id: i64,
|
|
||||||
username: String,
|
|
||||||
email: String
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our handler method.
|
|
||||||
#[rest_read_all(UsersResource)]
|
|
||||||
fn read_all(_state: &mut State) -> Success<Vec<User>> {
|
|
||||||
vec![User {
|
|
||||||
id: 1,
|
|
||||||
username: "h4ck3r".to_string(),
|
|
||||||
email: "h4ck3r@example.org".to_string()
|
|
||||||
}].into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our main method.
|
|
||||||
fn main() {
|
|
||||||
gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
|
||||||
route.resource::<UsersResource>("users");
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Uploads and Downloads can also be handled, but you need to specify the mime type manually:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Resource)]
|
|
||||||
#[rest_resource(create)]
|
|
||||||
struct ImageResource;
|
|
||||||
|
|
||||||
#[derive(FromBody, RequestBody)]
|
|
||||||
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
|
||||||
struct RawImage(Vec<u8>);
|
|
||||||
|
|
||||||
#[rest_create(ImageResource)]
|
|
||||||
fn create(_state : &mut State, body : RawImage) -> Raw<Vec<u8>> {
|
|
||||||
Raw::new(body.0, mime::APPLICATION_OCTET_STREAM)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Look at the [example] for more methods and usage with the `openapi` feature.
|
|
||||||
|
|
||||||
## Known Issues
|
|
||||||
|
|
||||||
These are currently known major issues. For a complete list please see
|
|
||||||
[the issue tracker](https://gitlab.com/msrd0/gotham-restful/issues).
|
|
||||||
If you encounter any issues that aren't yet reported, please report them
|
|
||||||
[here](https://gitlab.com/msrd0/gotham-restful/issues/new).
|
|
||||||
|
|
||||||
- Enabling the `openapi` feature might break code ([#4](https://gitlab.com/msrd0/gotham-restful/issues/4))
|
|
||||||
- For `chrono`'s `DateTime` types, the format is `date-time` instead of `datetime` ([openapiv3#14](https://github.com/glademiller/openapiv3/pull/14))
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
|
|
||||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
|
||||||
[gotham]: https://gotham.rs/
|
|
||||||
[serde]: https://github.com/serde-rs/serde#serde-----
|
|
||||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
|
||||||
|
|
69
README.tpl
69
README.tpl
|
@ -1,26 +1,61 @@
|
||||||
<div align="center">
|
<br/>
|
||||||
<h1>gotham-restful</h1>
|
<div>
|
||||||
</div>
|
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
|
||||||
<div align="center">
|
|
||||||
<a href="https://gitlab.com/msrd0/gotham-restful/-/commits/master">
|
|
||||||
<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://gitlab.com/msrd0/gotham-restful/-/commits/master">
|
<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">
|
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
|
||||||
<img alt="crates.io" src="https://img.shields.io/crates/v/gotham_restful.svg"/>
|
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://docs.rs/crate/gotham_restful">
|
<a href="https://blog.rust-lang.org/2020/12/31/Rust-1.49.0.html">
|
||||||
<img alt="docs.rs" src="https://docs.rs/gotham_restful/badge.svg"/>
|
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.49+-orange.svg"/>
|
||||||
</a>
|
|
||||||
<a href="https://www.rust-lang.org/en-US/">
|
|
||||||
<img alt="Build with Rust" src="https://img.shields.io/badge/Made%20with-Rust-orange.svg"/>
|
|
||||||
</a>
|
|
||||||
<a href="https://blog.rust-lang.org/2019/12/19/Rust-1.40.0.html">
|
|
||||||
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.40+-yellow.svg"/>
|
|
||||||
</a>
|
</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>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
This repository contains the following crates:
|
||||||
|
|
||||||
|
- **gotham_restful**
|
||||||
|
[](https://crates.io/crates/gotham_restful)
|
||||||
|
[](https://docs.rs/gotham_restful)
|
||||||
|
- **gotham_restful_derive**
|
||||||
|
[](https://crates.io/crates/gotham_restful_derive)
|
||||||
|
[](https://docs.rs/gotham_restful_derive)
|
||||||
|
- **openapi_type**
|
||||||
|
[](https://crates.io/crates/openapi_type)
|
||||||
|
[](https://docs.rs/crate/openapi_type)
|
||||||
|
- **openapi_type_derive**
|
||||||
|
[](https://crates.io/crates/openapi_type_derive)
|
||||||
|
[](https://docs.rs/crate/openapi_type_derive)
|
||||||
|
|
||||||
|
# gotham-restful
|
||||||
|
|
||||||
{{readme}}
|
{{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.0.3"
|
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 Gotham - Derive"
|
description = "Derive macros for gotham_restful"
|
||||||
keywords = ["gotham", "rest", "restful", "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"
|
||||||
proc-macro2 = "1.0.8"
|
paste = "1.0"
|
||||||
quote = "1.0.2"
|
proc-macro2 = "1.0.13"
|
||||||
syn = { version = "1.0.14", features = ["extra-traits", "full"] }
|
quote = "1.0.6"
|
||||||
|
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
|
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
|
||||||
|
})
|
||||||
|
}
|
122
derive/src/from_body.rs
Normal file
122
derive/src/from_body.rs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use std::cmp::min;
|
||||||
|
use syn::{spanned::Spanned, Data, DeriveInput, Error, Field, Fields, Ident, Result, Type};
|
||||||
|
|
||||||
|
struct ParsedFields {
|
||||||
|
fields: Vec<(Ident, Type)>,
|
||||||
|
named: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParsedFields {
|
||||||
|
fn from_named<I>(fields: I) -> Self
|
||||||
|
where
|
||||||
|
I: Iterator<Item = Field>
|
||||||
|
{
|
||||||
|
let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect();
|
||||||
|
Self { fields, named: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_unnamed<I>(fields: I) -> Self
|
||||||
|
where
|
||||||
|
I: Iterator<Item = Field>
|
||||||
|
{
|
||||||
|
let fields = fields
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, field)| (format_ident!("arg{}", i), field.ty))
|
||||||
|
.collect();
|
||||||
|
Self { fields, named: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_unit() -> Self {
|
||||||
|
Self {
|
||||||
|
fields: Vec::new(),
|
||||||
|
named: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_from_body(input: DeriveInput) -> Result<TokenStream> {
|
||||||
|
let krate = super::krate();
|
||||||
|
let ident = input.ident;
|
||||||
|
let generics = input.generics;
|
||||||
|
|
||||||
|
let strukt = match input.data {
|
||||||
|
Data::Enum(inum) => Err(inum.enum_token.span()),
|
||||||
|
Data::Struct(strukt) => Ok(strukt),
|
||||||
|
Data::Union(uni) => Err(uni.union_token.span())
|
||||||
|
}
|
||||||
|
.map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?;
|
||||||
|
|
||||||
|
let fields = match strukt.fields {
|
||||||
|
Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()),
|
||||||
|
Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter()),
|
||||||
|
Fields::Unit => ParsedFields::from_unit()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut where_clause = quote!();
|
||||||
|
let mut block = quote!();
|
||||||
|
let mut body_ident = format_ident!("_body");
|
||||||
|
let mut type_ident = format_ident!("_type");
|
||||||
|
|
||||||
|
if let Some(body_field) = fields.fields.get(0) {
|
||||||
|
body_ident = body_field.0.clone();
|
||||||
|
let body_ty = &body_field.1;
|
||||||
|
where_clause = quote! {
|
||||||
|
#where_clause
|
||||||
|
#body_ty : for<'a> From<&'a [u8]>,
|
||||||
|
};
|
||||||
|
block = quote! {
|
||||||
|
#block
|
||||||
|
let #body_ident : &[u8] = &#body_ident;
|
||||||
|
let #body_ident : #body_ty = #body_ident.into();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(type_field) = fields.fields.get(1) {
|
||||||
|
type_ident = type_field.0.clone();
|
||||||
|
let type_ty = &type_field.1;
|
||||||
|
where_clause = quote! {
|
||||||
|
#where_clause
|
||||||
|
#type_ty : From<#krate::Mime>,
|
||||||
|
};
|
||||||
|
block = quote! {
|
||||||
|
#block
|
||||||
|
let #type_ident : #type_ty = #type_ident.into();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for field in &fields.fields[min(2, fields.fields.len())..] {
|
||||||
|
let field_ident = &field.0;
|
||||||
|
let field_ty = &field.1;
|
||||||
|
where_clause = quote! {
|
||||||
|
#where_clause
|
||||||
|
#field_ty : Default,
|
||||||
|
};
|
||||||
|
block = quote! {
|
||||||
|
#block
|
||||||
|
let #field_ident : #field_ty = Default::default();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let field_names: Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect();
|
||||||
|
let ctor = if fields.named {
|
||||||
|
quote!(Self { #(#field_names),* })
|
||||||
|
} else {
|
||||||
|
quote!(Self ( #(#field_names),* ))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
impl #generics #krate::FromBody for #ident #generics
|
||||||
|
where #where_clause
|
||||||
|
{
|
||||||
|
type Err = ::std::convert::Infallible;
|
||||||
|
|
||||||
|
fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result<Self, ::std::convert::Infallible>
|
||||||
|
{
|
||||||
|
#block
|
||||||
|
Ok(#ctor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
129
derive/src/lib.rs
Normal file
129
derive/src/lib.rs
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||||
|
#![deny(broken_intra_doc_links)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Result};
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
mod endpoint;
|
||||||
|
use endpoint::{expand_endpoint, EndpointType};
|
||||||
|
|
||||||
|
mod from_body;
|
||||||
|
use from_body::expand_from_body;
|
||||||
|
|
||||||
|
mod request_body;
|
||||||
|
use request_body::expand_request_body;
|
||||||
|
|
||||||
|
mod resource;
|
||||||
|
use resource::expand_resource;
|
||||||
|
|
||||||
|
mod resource_error;
|
||||||
|
use resource_error::expand_resource_error;
|
||||||
|
|
||||||
|
mod private_openapi_trait;
|
||||||
|
use private_openapi_trait::expand_private_openapi_trait;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn print_tokens(tokens: TokenStream2) -> TokenStream {
|
||||||
|
// eprintln!("{}", tokens);
|
||||||
|
tokens.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn expand_derive<F>(input: TokenStream, expand: F) -> TokenStream
|
||||||
|
where
|
||||||
|
F: FnOnce(DeriveInput) -> Result<TokenStream2>
|
||||||
|
{
|
||||||
|
print_tokens(expand(parse_macro_input!(input)).unwrap_or_else(|err| err.to_compile_error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn expand_macro<F, A, I>(attrs: TokenStream, item: TokenStream, expand: F) -> TokenStream
|
||||||
|
where
|
||||||
|
F: FnOnce(A, I) -> Result<TokenStream2>,
|
||||||
|
A: ParseMacroInput,
|
||||||
|
I: ParseMacroInput
|
||||||
|
{
|
||||||
|
print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)).unwrap_or_else(|err| err.to_compile_error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn krate() -> TokenStream2 {
|
||||||
|
quote!(::gotham_restful)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(FromBody)]
|
||||||
|
pub fn derive_from_body(input: TokenStream) -> TokenStream {
|
||||||
|
expand_derive(input, expand_from_body)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(RequestBody, attributes(supported_types))]
|
||||||
|
pub fn derive_request_body(input: TokenStream) -> TokenStream {
|
||||||
|
expand_derive(input, expand_request_body)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(Resource, attributes(resource))]
|
||||||
|
pub fn derive_resource(input: TokenStream) -> TokenStream {
|
||||||
|
expand_derive(input, expand_resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(ResourceError, attributes(display, from, status))]
|
||||||
|
pub fn derive_resource_error(input: TokenStream) -> TokenStream {
|
||||||
|
expand_derive(input, expand_resource_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::custom(), attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, 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)
|
||||||
|
}
|
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
|
||||||
|
})
|
||||||
|
}
|
97
derive/src/request_body.rs
Normal file
97
derive/src/request_body.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use crate::util::CollectToResult;
|
||||||
|
use proc_macro2::{Ident, TokenStream};
|
||||||
|
use quote::quote;
|
||||||
|
use std::iter;
|
||||||
|
use syn::{
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
punctuated::Punctuated,
|
||||||
|
spanned::Spanned,
|
||||||
|
DeriveInput, Error, Generics, Path, Result, Token
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MimeList(Punctuated<Path, Token![,]>);
|
||||||
|
|
||||||
|
impl Parse for MimeList {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
let list = Punctuated::parse_separated_nonempty(&input)?;
|
||||||
|
Ok(Self(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "openapi"))]
|
||||||
|
fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream {
|
||||||
|
quote!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream {
|
||||||
|
let krate = super::krate();
|
||||||
|
let openapi = quote!(#krate::private::openapi);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #generics #krate::private::OpenapiType for #ident #generics
|
||||||
|
{
|
||||||
|
fn schema() -> #krate::private::OpenapiSchema
|
||||||
|
{
|
||||||
|
#krate::private::OpenapiSchema::new(
|
||||||
|
#openapi::SchemaKind::Type(
|
||||||
|
#openapi::Type::String(
|
||||||
|
#openapi::StringType {
|
||||||
|
format: #openapi::VariantOrUnknownOrEmpty::Item(
|
||||||
|
#openapi::StringFormat::Binary
|
||||||
|
),
|
||||||
|
.. ::std::default::Default::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_request_body(input: DeriveInput) -> Result<TokenStream> {
|
||||||
|
let krate = super::krate();
|
||||||
|
let ident = input.ident;
|
||||||
|
let generics = input.generics;
|
||||||
|
|
||||||
|
let types = input
|
||||||
|
.attrs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|attr| {
|
||||||
|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())
|
||||||
|
})
|
||||||
|
.flat_map(|attr| {
|
||||||
|
let span = attr.span();
|
||||||
|
attr.parse_args::<MimeList>()
|
||||||
|
.map(|list| Box::new(list.0.into_iter().map(Ok)) as Box<dyn Iterator<Item = Result<Path>>>)
|
||||||
|
.unwrap_or_else(|mut err| {
|
||||||
|
err.combine(Error::new(
|
||||||
|
span,
|
||||||
|
"Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]"
|
||||||
|
));
|
||||||
|
Box::new(iter::once(Err(err)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect_to_result()?;
|
||||||
|
|
||||||
|
let types = match types {
|
||||||
|
ref types if types.is_empty() => quote!(None),
|
||||||
|
types => quote!(Some(vec![#(#types),*]))
|
||||||
|
};
|
||||||
|
|
||||||
|
let impl_openapi_type = impl_openapi_type(&ident, &generics);
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
impl #generics #krate::RequestBody for #ident #generics
|
||||||
|
where #ident #generics : #krate::FromBody
|
||||||
|
{
|
||||||
|
fn supported_types() -> Option<Vec<#krate::Mime>>
|
||||||
|
{
|
||||||
|
#types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#impl_openapi_type
|
||||||
|
})
|
||||||
|
}
|
70
derive/src/resource.rs
Normal file
70
derive/src/resource.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use crate::{
|
||||||
|
endpoint::endpoint_ident,
|
||||||
|
util::{CollectToResult, PathEndsWith}
|
||||||
|
};
|
||||||
|
use proc_macro2::{Ident, TokenStream};
|
||||||
|
use quote::quote;
|
||||||
|
use std::iter;
|
||||||
|
use syn::{
|
||||||
|
parenthesized,
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
punctuated::Punctuated,
|
||||||
|
DeriveInput, Result, Token
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MethodList(Punctuated<Ident, Token![,]>);
|
||||||
|
|
||||||
|
impl Parse for MethodList {
|
||||||
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
||||||
|
let content;
|
||||||
|
let _paren = parenthesized!(content in input);
|
||||||
|
let list = Punctuated::parse_separated_nonempty(&content)?;
|
||||||
|
Ok(Self(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_resource(input: DeriveInput) -> Result<TokenStream> {
|
||||||
|
let krate = super::krate();
|
||||||
|
let ident = input.ident;
|
||||||
|
|
||||||
|
let methods = input
|
||||||
|
.attrs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|attr| attr.path.ends_with("resource"))
|
||||||
|
.map(|attr| syn::parse2(attr.tokens).map(|m: MethodList| m.0.into_iter()))
|
||||||
|
.flat_map(|list| match list {
|
||||||
|
Ok(iter) => Box::new(iter.map(|method| {
|
||||||
|
let ident = endpoint_ident(&method);
|
||||||
|
Ok(quote!(route.endpoint::<#ident>();))
|
||||||
|
})) as Box<dyn Iterator<Item = Result<TokenStream>>>,
|
||||||
|
Err(err) => Box::new(iter::once(Err(err)))
|
||||||
|
})
|
||||||
|
.collect_to_result()?;
|
||||||
|
|
||||||
|
let non_openapi_impl = quote! {
|
||||||
|
impl #krate::Resource for #ident
|
||||||
|
{
|
||||||
|
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
||||||
|
{
|
||||||
|
#(#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
|
||||||
|
})
|
||||||
|
}
|
331
derive/src/resource_error.rs
Normal file
331
derive/src/resource_error.rs
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
use crate::util::{remove_parens, CollectToResult};
|
||||||
|
use proc_macro2::{Ident, TokenStream};
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use std::iter;
|
||||||
|
use syn::{
|
||||||
|
spanned::Spanned, Attribute, Data, DeriveInput, Error, Fields, GenericParam, LitStr, Path, PathSegment, Result, Type,
|
||||||
|
Variant
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ErrorVariantField {
|
||||||
|
attrs: Vec<Attribute>,
|
||||||
|
ident: Ident,
|
||||||
|
ty: Type
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ErrorVariant {
|
||||||
|
ident: Ident,
|
||||||
|
status: Option<Path>,
|
||||||
|
is_named: bool,
|
||||||
|
fields: Vec<ErrorVariantField>,
|
||||||
|
from_ty: Option<(usize, Type)>,
|
||||||
|
display: Option<LitStr>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_variant(variant: Variant) -> Result<ErrorVariant> {
|
||||||
|
let status =
|
||||||
|
match variant.attrs.iter().find(|attr| {
|
||||||
|
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()))?),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut is_named = false;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
match variant.fields {
|
||||||
|
Fields::Named(named) => {
|
||||||
|
is_named = true;
|
||||||
|
for field in named.named {
|
||||||
|
let span = field.span();
|
||||||
|
fields.push(ErrorVariantField {
|
||||||
|
attrs: field.attrs,
|
||||||
|
ident: field
|
||||||
|
.ident
|
||||||
|
.ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?,
|
||||||
|
ty: field.ty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Fields::Unnamed(unnamed) => {
|
||||||
|
for (i, field) in unnamed.unnamed.into_iter().enumerate() {
|
||||||
|
fields.push(ErrorVariantField {
|
||||||
|
attrs: field.attrs,
|
||||||
|
ident: format_ident!("arg{}", i),
|
||||||
|
ty: field.ty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Fields::Unit => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let from_ty = fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.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()));
|
||||||
|
|
||||||
|
let display = match variant.attrs.iter().find(|attr| {
|
||||||
|
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()))?),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ErrorVariant {
|
||||||
|
ident: variant.ident,
|
||||||
|
status,
|
||||||
|
is_named,
|
||||||
|
fields,
|
||||||
|
from_ty,
|
||||||
|
display
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_segment(name: &str) -> PathSegment {
|
||||||
|
PathSegment {
|
||||||
|
ident: format_ident!("{}", name),
|
||||||
|
arguments: Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorVariant {
|
||||||
|
fn fields_pat(&self) -> TokenStream {
|
||||||
|
let mut fields = self.fields.iter().map(|field| &field.ident).peekable();
|
||||||
|
if fields.peek().is_none() {
|
||||||
|
quote!()
|
||||||
|
} else if self.is_named {
|
||||||
|
quote!( { #( #fields ),* } )
|
||||||
|
} else {
|
||||||
|
quote!( ( #( #fields ),* ) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_display_match_arm(&self, formatter_ident: &Ident, enum_ident: &Ident) -> Result<TokenStream> {
|
||||||
|
let ident = &self.ident;
|
||||||
|
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
|
||||||
|
let display_str = display.value();
|
||||||
|
let mut params: Vec<&str> = Vec::new();
|
||||||
|
let len = display_str.len();
|
||||||
|
let mut start = len;
|
||||||
|
let mut iter = display_str.chars().enumerate().peekable();
|
||||||
|
while let Some((i, c)) = iter.next() {
|
||||||
|
// we found a new opening brace
|
||||||
|
if start == len && c == '{' {
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
// we found a duplicate opening brace
|
||||||
|
else if start == i && c == '{' {
|
||||||
|
start = len;
|
||||||
|
}
|
||||||
|
// we found a closing brace
|
||||||
|
else if start < i && c == '}' {
|
||||||
|
match iter.peek() {
|
||||||
|
Some((_, '}')) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
display.span(),
|
||||||
|
"Error parsing format string: curly braces not allowed inside parameter name"
|
||||||
|
))
|
||||||
|
},
|
||||||
|
_ => params.push(&display_str[start..i])
|
||||||
|
};
|
||||||
|
start = len;
|
||||||
|
}
|
||||||
|
// we found a closing brace without content
|
||||||
|
else if start == i && c == '}' {
|
||||||
|
return Err(Error::new(
|
||||||
|
display.span(),
|
||||||
|
"Error parsing format string: parameter name must not be empty"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start != len {
|
||||||
|
return Err(Error::new(
|
||||||
|
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 fields_pat = self.fields_pat();
|
||||||
|
Ok(quote! {
|
||||||
|
#enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_match_arm(self, krate: &TokenStream, enum_ident: &Ident) -> Result<TokenStream> {
|
||||||
|
let ident = &self.ident;
|
||||||
|
let fields_pat = self.fields_pat();
|
||||||
|
let status = self.status.map(|status| {
|
||||||
|
// the status might be relative to StatusCode, so let's fix that
|
||||||
|
if status.leading_colon.is_none() && status.segments.len() < 2 {
|
||||||
|
let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK"));
|
||||||
|
Path {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// the response will come directly from the from_ty if present
|
||||||
|
let res = match (self.from_ty, status) {
|
||||||
|
(Some((from_index, _)), None) => {
|
||||||
|
let from_field = &self.fields[from_index].ident;
|
||||||
|
quote!(#from_field.into_response_error())
|
||||||
|
},
|
||||||
|
(Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")),
|
||||||
|
(None, Some(status)) => quote!(Ok(#krate::Response::new(
|
||||||
|
{ #status }.into(),
|
||||||
|
#krate::gotham::hyper::Body::empty(),
|
||||||
|
None
|
||||||
|
))),
|
||||||
|
(None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant"))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#enum_ident::#ident #fields_pat => #res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn were(&self) -> Option<TokenStream> {
|
||||||
|
match self.from_ty.as_ref() {
|
||||||
|
Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )),
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_resource_error(input: DeriveInput) -> Result<TokenStream> {
|
||||||
|
let krate = super::krate();
|
||||||
|
let ident = input.ident;
|
||||||
|
let generics = input.generics;
|
||||||
|
|
||||||
|
let inum = match input.data {
|
||||||
|
Data::Enum(inum) => Ok(inum),
|
||||||
|
Data::Struct(strukt) => Err(strukt.struct_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(process_variant).collect_to_result()?;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
GenericParam::Type(ty) => {
|
||||||
|
let ident = &ty.ident;
|
||||||
|
Some(quote!(#ident : ::std::fmt::Display))
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
});
|
||||||
|
let formatter_ident = format_ident!("resource_error_display_formatter");
|
||||||
|
let match_arms = variants
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.to_display_match_arm(&formatter_ident, &ident))
|
||||||
|
.collect_to_result()?;
|
||||||
|
Some(quote! {
|
||||||
|
impl #generics ::std::fmt::Display for #ident #generics
|
||||||
|
where #( #were ),*
|
||||||
|
{
|
||||||
|
fn fmt(&self, #formatter_ident: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
#( #match_arms ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut from_impls: Vec<TokenStream> = Vec::new();
|
||||||
|
|
||||||
|
for var in &variants {
|
||||||
|
let var_ident = &var.ident;
|
||||||
|
let (from_index, from_ty) = match var.from_ty.as_ref() {
|
||||||
|
Some(f) => f,
|
||||||
|
None => continue
|
||||||
|
};
|
||||||
|
let from_ident = &var.fields[*from_index].ident;
|
||||||
|
|
||||||
|
let fields_pat = var.fields_pat();
|
||||||
|
let fields_where = var
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| i != from_index)
|
||||||
|
.map(|(_, field)| {
|
||||||
|
let ty = &field.ty;
|
||||||
|
quote!( #ty : Default )
|
||||||
|
})
|
||||||
|
.chain(iter::once(quote!( #from_ty : ::std::error::Error )));
|
||||||
|
let fields_let = var
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| i != from_index)
|
||||||
|
.map(|(_, field)| {
|
||||||
|
let id = &field.ident;
|
||||||
|
let ty = &field.ty;
|
||||||
|
quote!( let #id : #ty = Default::default(); )
|
||||||
|
});
|
||||||
|
|
||||||
|
from_impls.push(quote! {
|
||||||
|
impl #generics ::std::convert::From<#from_ty> for #ident #generics
|
||||||
|
where #( #fields_where ),*
|
||||||
|
{
|
||||||
|
fn from(#from_ident : #from_ty) -> Self
|
||||||
|
{
|
||||||
|
#( #fields_let )*
|
||||||
|
Self::#var_ident #fields_pat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let were = variants.iter().filter_map(|variant| variant.were()).collect::<Vec<_>>();
|
||||||
|
let variants = variants
|
||||||
|
.into_iter()
|
||||||
|
.map(|variant| variant.into_match_arm(&krate, &ident))
|
||||||
|
.collect_to_result()?;
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#display_impl
|
||||||
|
|
||||||
|
impl #generics #krate::IntoResponseError for #ident #generics
|
||||||
|
where #( #were ),*
|
||||||
|
{
|
||||||
|
type Err = #krate::private::serde_json::Error;
|
||||||
|
|
||||||
|
fn into_response_error(self) -> Result<#krate::Response, Self::Err>
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
#( #variants ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#( #from_impls )*
|
||||||
|
})
|
||||||
|
}
|
74
derive/src/util.rs
Normal file
74
derive/src/util.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use proc_macro2::{Delimiter, TokenStream, TokenTree};
|
||||||
|
use std::iter;
|
||||||
|
use syn::{Error, Lit, LitBool, LitStr, Path, Result};
|
||||||
|
|
||||||
|
pub(crate) trait CollectToResult {
|
||||||
|
type Item;
|
||||||
|
|
||||||
|
fn collect_to_result(self) -> Result<Vec<Self::Item>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Item, I> CollectToResult for I
|
||||||
|
where
|
||||||
|
I: Iterator<Item = Result<Item>>
|
||||||
|
{
|
||||||
|
type Item = Item;
|
||||||
|
|
||||||
|
fn collect_to_result(self) -> Result<Vec<Item>> {
|
||||||
|
self.fold(Ok(Vec::new()), |res, code| match (code, res) {
|
||||||
|
(Ok(code), Ok(mut codes)) => {
|
||||||
|
codes.push(code);
|
||||||
|
Ok(codes)
|
||||||
|
},
|
||||||
|
(Ok(_), Err(errors)) => 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExpectLit for Lit {
|
||||||
|
fn expect_bool(self) -> Result<LitBool> {
|
||||||
|
match self {
|
||||||
|
Self::Bool(bool) => Ok(bool),
|
||||||
|
_ => Err(Error::new(self.span(), "Expected boolean literal"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_str(self) -> Result<LitStr> {
|
||||||
|
match self {
|
||||||
|
Self::Str(str) => Ok(str),
|
||||||
|
_ => Err(Error::new(self.span(), "Expected string literal"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait PathEndsWith {
|
||||||
|
fn ends_with(&self, s: &str) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathEndsWith for Path {
|
||||||
|
fn ends_with(&self, s: &str) -> bool {
|
||||||
|
self.segments.last().map(|segment| segment.ident.to_string()).as_deref() == Some(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_parens(input: TokenStream) -> TokenStream {
|
||||||
|
let iter = input.into_iter().flat_map(|tt| {
|
||||||
|
if let TokenTree::Group(group) = &tt {
|
||||||
|
if group.delimiter() == Delimiter::Parenthesis {
|
||||||
|
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box::new(iter::once(tt))
|
||||||
|
});
|
||||||
|
iter.collect()
|
||||||
|
}
|
|
@ -2,28 +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"
|
fake = "2.2.2"
|
||||||
gotham = "0.4"
|
gotham = { version = "0.5.0", default-features = false }
|
||||||
gotham_derive = "0.4"
|
gotham_derive = "0.5.0"
|
||||||
gotham_restful = { version = "0.0.4", features = ["auth", "openapi"] }
|
gotham_restful = { version = "0.2.0", features = ["auth", "cors", "openapi"], default-features = false }
|
||||||
hyper = "0.12"
|
log = "0.4.8"
|
||||||
log = "0.4"
|
pretty_env_logger = "0.4"
|
||||||
log4rs = { version = "0.8", features = ["console_appender"], default-features = false }
|
serde = "1.0.110"
|
||||||
serde = "1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
fake = "2.2"
|
|
||||||
log = "0.4"
|
|
||||||
log4rs = { version = "0.8", features = ["console_appender"], default-features = false }
|
|
||||||
|
|
|
@ -1,43 +1,34 @@
|
||||||
#[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::{
|
||||||
|
hyper::header::CONTENT_TYPE,
|
||||||
middleware::logger::RequestLogger,
|
middleware::logger::RequestLogger,
|
||||||
pipeline::{new_pipeline, single::single_pipeline},
|
pipeline::{new_pipeline, single::single_pipeline},
|
||||||
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)]
|
||||||
#[rest_resource(ReadAll, Read, Search, Create, DeleteAll, Delete, Update, UpdateAll)]
|
#[resource(read_all, read, search, create, update_all, update, remove, remove_all)]
|
||||||
struct Users
|
struct Users {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[rest_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_read_all(Users)]
|
#[read_all]
|
||||||
fn read_all(_state : &mut State) -> 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 }))
|
||||||
|
@ -45,102 +36,95 @@ fn read_all(_state : &mut State) -> Success<Vec<Option<User>>>
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_read(Users)]
|
#[read]
|
||||||
fn read(_state : &mut State, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_search(Users)]
|
#[search]
|
||||||
fn search(_state : &mut State, query : User) -> Success<User>
|
fn search(query: User) -> Success<User> {
|
||||||
{
|
|
||||||
query.into()
|
query.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_create(Users)]
|
#[create]
|
||||||
fn create(_state : &mut State, body : User)
|
fn create(body: User) {
|
||||||
{
|
|
||||||
info!("Created User: {}", body.username);
|
info!("Created User: {}", body.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_update_all(Users)]
|
#[change_all]
|
||||||
fn update_all(_state : &mut State, 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>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_update(Users)]
|
#[change]
|
||||||
fn update(_state : &mut State, id : u64, body : User)
|
fn update(id: u64, body: User) {
|
||||||
{
|
|
||||||
info!("Change User {} to {}", id, body.username);
|
info!("Change User {} to {}", id, body.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_delete_all(Users)]
|
#[remove_all]
|
||||||
fn delete_all(_state : &mut State)
|
fn remove_all() {
|
||||||
{
|
|
||||||
info!("Delete all Users");
|
info!("Delete all Users");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_delete(Users)]
|
#[remove]
|
||||||
fn delete(_state : &mut State, id : u64)
|
fn remove(id: u64) {
|
||||||
{
|
|
||||||
info!("Delete User {}", id);
|
info!("Delete User {}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_read_all(Auth)]
|
#[read_all]
|
||||||
fn auth_read_all(auth : AuthStatus<()>) -> AuthResult<Success<String>>
|
fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess<String> {
|
||||||
{
|
match auth {
|
||||||
let str : Success<String> = match auth {
|
AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)),
|
||||||
AuthStatus::Authenticated(data) => format!("{:?}", data).into(),
|
_ => Err(Forbidden)
|
||||||
_ => return AuthErr
|
}
|
||||||
};
|
|
||||||
str.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
let cors = CorsConfig {
|
||||||
.appender(
|
origin: Origin::Copy,
|
||||||
Appender::builder()
|
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||||
.build("stdout", Box::new(
|
credentials: true,
|
||||||
ConsoleAppender::builder()
|
..Default::default()
|
||||||
.encoder(Box::new(encoder))
|
};
|
||||||
.build()
|
|
||||||
)))
|
|
||||||
.build(Root::builder().appender("stdout").build(LevelFilter::Info))
|
|
||||||
.unwrap();
|
|
||||||
log4rs::init_config(config).unwrap();
|
|
||||||
|
|
||||||
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)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
gotham::start(ADDR, build_router(chain, pipelines, |route| {
|
gotham::start(
|
||||||
route.with_openapi("Users Example".to_owned(), "0.0.1".to_owned(), format!("http://{}", ADDR), |mut route| {
|
ADDR,
|
||||||
route.resource::<Users>("users");
|
build_router(chain, pipelines, |route| {
|
||||||
route.resource::<Auth>("auth");
|
let info = OpenapiInfo {
|
||||||
route.get_openapi("openapi");
|
title: "Users Example".to_owned(),
|
||||||
});
|
version: "0.0.1".to_owned(),
|
||||||
}));
|
urls: vec![format!("http://{}", ADDR)]
|
||||||
|
};
|
||||||
|
route.with_openapi(info, |mut route| {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
# -*- eval: (cargo-minor-mode 1) -*-
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "gotham_restful"
|
|
||||||
version = "0.0.4"
|
|
||||||
authors = ["Dominic Meiser <git@msrd0.de>"]
|
|
||||||
edition = "2018"
|
|
||||||
description = "RESTful additions for Gotham"
|
|
||||||
keywords = ["gotham", "rest", "restful"]
|
|
||||||
license = "EPL-2.0 OR Apache-2.0"
|
|
||||||
readme = "README.md"
|
|
||||||
repository = "https://gitlab.com/msrd0/gotham-restful"
|
|
||||||
|
|
||||||
[badges]
|
|
||||||
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
base64 = { version = ">=0.10.1, <0.12", optional = true }
|
|
||||||
chrono = { version = "0.4.10", optional = true }
|
|
||||||
cookie = { version = "0.12", optional = true }
|
|
||||||
futures = "0.1.29"
|
|
||||||
gotham = "0.4"
|
|
||||||
gotham_derive = "0.4"
|
|
||||||
gotham_middleware_diesel = { version = "0.1", optional = true }
|
|
||||||
gotham_restful_derive = { version = "0.0.3" }
|
|
||||||
hyper = "0.12.35"
|
|
||||||
indexmap = { version = "1.3.0", optional = true }
|
|
||||||
jsonwebtoken = { version = "6.0.1", optional = true }
|
|
||||||
log = { version = "0.4.8", optional = true }
|
|
||||||
mime = "0.3.16"
|
|
||||||
openapiv3 = { version = "0.3", optional = true }
|
|
||||||
serde = { version = "1.0.104", features = ["derive"] }
|
|
||||||
serde_json = "1.0.45"
|
|
||||||
uuid = { version = ">= 0.1, < 0.9", optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
paste = "0.1.10"
|
|
||||||
thiserror = "1"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"]
|
|
||||||
errorlog = []
|
|
||||||
database = ["gotham_restful_derive/database", "gotham_middleware_diesel"]
|
|
||||||
openapi = ["gotham_restful_derive/openapi", "indexmap", "log", "openapiv3"]
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE-Apache
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE-EPL
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE.md
|
|
|
@ -1 +0,0 @@
|
||||||
../README.md
|
|
|
@ -1,181 +0,0 @@
|
||||||
/*!
|
|
||||||
This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to
|
|
||||||
have several RESTful resources that can be added to the gotham router. This crate will take care
|
|
||||||
of everything else, like parsing path/query parameters, request bodies, and writing response
|
|
||||||
bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)serializing. If you
|
|
||||||
enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful
|
|
||||||
resources.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
A basic server with only one resource, handling a simple `GET` request, could look like this:
|
|
||||||
|
|
||||||
```rust,no_run
|
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
|
||||||
# use gotham::{router::builder::*, state::State};
|
|
||||||
# use gotham_restful::{DrawResources, Resource, Success};
|
|
||||||
# use serde::{Deserialize, Serialize};
|
|
||||||
/// Our RESTful Resource.
|
|
||||||
#[derive(Resource)]
|
|
||||||
#[rest_resource(read_all)]
|
|
||||||
struct UsersResource;
|
|
||||||
|
|
||||||
/// Our return type.
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
# #[derive(OpenapiType)]
|
|
||||||
struct User {
|
|
||||||
id: i64,
|
|
||||||
username: String,
|
|
||||||
email: String
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our handler method.
|
|
||||||
#[rest_read_all(UsersResource)]
|
|
||||||
fn read_all(_state: &mut State) -> Success<Vec<User>> {
|
|
||||||
vec![User {
|
|
||||||
id: 1,
|
|
||||||
username: "h4ck3r".to_string(),
|
|
||||||
email: "h4ck3r@example.org".to_string()
|
|
||||||
}].into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our main method.
|
|
||||||
fn main() {
|
|
||||||
gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
|
||||||
route.resource::<UsersResource>("users");
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Uploads and Downloads can also be handled, but you need to specify the mime type manually:
|
|
||||||
|
|
||||||
```rust,no_run
|
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
|
||||||
# use gotham::{router::builder::*, state::State};
|
|
||||||
# use gotham_restful::{DrawResources, Raw, Resource, Success};
|
|
||||||
# use serde::{Deserialize, Serialize};
|
|
||||||
#[derive(Resource)]
|
|
||||||
#[rest_resource(create)]
|
|
||||||
struct ImageResource;
|
|
||||||
|
|
||||||
#[derive(FromBody, RequestBody)]
|
|
||||||
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
|
||||||
struct RawImage(Vec<u8>);
|
|
||||||
|
|
||||||
#[rest_create(ImageResource)]
|
|
||||||
fn create(_state : &mut State, body : RawImage) -> Raw<Vec<u8>> {
|
|
||||||
Raw::new(body.0, mime::APPLICATION_OCTET_STREAM)
|
|
||||||
}
|
|
||||||
# fn main() {
|
|
||||||
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
|
||||||
# route.resource::<ImageResource>("image");
|
|
||||||
# }));
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
Look at the [example] for more methods and usage with the `openapi` feature.
|
|
||||||
|
|
||||||
# Known Issues
|
|
||||||
|
|
||||||
These are currently known major issues. For a complete list please see
|
|
||||||
[the issue tracker](https://gitlab.com/msrd0/gotham-restful/issues).
|
|
||||||
If you encounter any issues that aren't yet reported, please report them
|
|
||||||
[here](https://gitlab.com/msrd0/gotham-restful/issues/new).
|
|
||||||
|
|
||||||
- Enabling the `openapi` feature might break code ([#4](https://gitlab.com/msrd0/gotham-restful/issues/4))
|
|
||||||
- For `chrono`'s `DateTime` types, the format is `date-time` instead of `datetime` ([openapiv3#14](https://github.com/glademiller/openapiv3/pull/14))
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
|
||||||
[gotham]: https://gotham.rs/
|
|
||||||
[serde]: https://github.com/serde-rs/serde#serde-----
|
|
||||||
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
|
||||||
*/
|
|
||||||
|
|
||||||
// weird proc macro issue
|
|
||||||
extern crate self as gotham_restful;
|
|
||||||
|
|
||||||
#[macro_use] extern crate gotham_derive;
|
|
||||||
#[macro_use] extern crate serde;
|
|
||||||
|
|
||||||
#[doc(no_inline)]
|
|
||||||
pub use hyper::{header::HeaderName, Chunk, StatusCode};
|
|
||||||
#[doc(no_inline)]
|
|
||||||
pub use mime::Mime;
|
|
||||||
|
|
||||||
pub use gotham_restful_derive::*;
|
|
||||||
|
|
||||||
/// Not public API
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub mod export
|
|
||||||
{
|
|
||||||
pub use futures::future::Future;
|
|
||||||
pub use gotham::state::{FromState, State};
|
|
||||||
|
|
||||||
#[cfg(feature = "database")]
|
|
||||||
pub use gotham_middleware_diesel::Repo;
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub use indexmap::IndexMap;
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub use openapiv3 as openapi;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
|
||||||
mod auth;
|
|
||||||
#[cfg(feature = "auth")]
|
|
||||||
pub use auth::{
|
|
||||||
AuthHandler,
|
|
||||||
AuthMiddleware,
|
|
||||||
AuthSource,
|
|
||||||
AuthStatus,
|
|
||||||
AuthValidation,
|
|
||||||
StaticAuthHandler
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
mod openapi;
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub use openapi::{
|
|
||||||
router::{GetOpenapi, OpenapiRouter},
|
|
||||||
types::{OpenapiSchema, OpenapiType}
|
|
||||||
};
|
|
||||||
|
|
||||||
mod resource;
|
|
||||||
pub use resource::{
|
|
||||||
Resource,
|
|
||||||
ResourceMethod,
|
|
||||||
ResourceReadAll,
|
|
||||||
ResourceRead,
|
|
||||||
ResourceSearch,
|
|
||||||
ResourceCreate,
|
|
||||||
ResourceUpdateAll,
|
|
||||||
ResourceUpdate,
|
|
||||||
ResourceDeleteAll,
|
|
||||||
ResourceDelete
|
|
||||||
};
|
|
||||||
|
|
||||||
mod result;
|
|
||||||
pub use result::{
|
|
||||||
AuthResult,
|
|
||||||
AuthResult::AuthErr,
|
|
||||||
NoContent,
|
|
||||||
Raw,
|
|
||||||
ResourceResult,
|
|
||||||
Response,
|
|
||||||
Success
|
|
||||||
};
|
|
||||||
|
|
||||||
mod routing;
|
|
||||||
pub use routing::{DrawResources, DrawResourceRoutes};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub use routing::WithOpenapi;
|
|
||||||
|
|
||||||
mod types;
|
|
||||||
pub use types::*;
|
|
|
@ -1,3 +0,0 @@
|
||||||
|
|
||||||
pub mod router;
|
|
||||||
pub mod types;
|
|
|
@ -1,557 +0,0 @@
|
||||||
use crate::{
|
|
||||||
resource::*,
|
|
||||||
result::*,
|
|
||||||
routing::*,
|
|
||||||
OpenapiSchema,
|
|
||||||
OpenapiType,
|
|
||||||
RequestBody
|
|
||||||
};
|
|
||||||
use futures::future::ok;
|
|
||||||
use gotham::{
|
|
||||||
handler::{Handler, HandlerFuture, NewHandler},
|
|
||||||
helpers::http::response::create_response,
|
|
||||||
pipeline::chain::PipelineHandleChain,
|
|
||||||
router::builder::*,
|
|
||||||
state::State
|
|
||||||
};
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use log::error;
|
|
||||||
use mime::{Mime, APPLICATION_JSON, STAR_STAR, TEXT_PLAIN};
|
|
||||||
use openapiv3::{
|
|
||||||
APIKeyLocation, Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
|
|
||||||
ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema,
|
|
||||||
SchemaKind, SecurityScheme, Server, StatusCode, Type
|
|
||||||
};
|
|
||||||
use std::panic::RefUnwindSafe;
|
|
||||||
|
|
||||||
/**
|
|
||||||
This type is required to build routes while adding them to the generated OpenAPI Spec at the
|
|
||||||
same time. There is no need to use this type directly. See [`WithOpenapi`] on how to do this.
|
|
||||||
|
|
||||||
[`WithOpenapi`]: trait.WithOpenapi.html
|
|
||||||
*/
|
|
||||||
pub struct OpenapiRouter(OpenAPI);
|
|
||||||
|
|
||||||
impl OpenapiRouter
|
|
||||||
{
|
|
||||||
pub fn new(title : String, version : String, url : String) -> Self
|
|
||||||
{
|
|
||||||
Self(OpenAPI {
|
|
||||||
openapi: "3.0.2".to_string(),
|
|
||||||
info: openapiv3::Info {
|
|
||||||
title, version,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
servers: vec![Server {
|
|
||||||
url,
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
fn remove_path(&mut self, path : &str) -> PathItem
|
|
||||||
{
|
|
||||||
match self.0.paths.swap_remove(path) {
|
|
||||||
Some(Item(item)) => item,
|
|
||||||
_ => PathItem::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem)
|
|
||||||
{
|
|
||||||
self.0.paths.insert(path.to_string(), Item(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema)
|
|
||||||
{
|
|
||||||
self.add_schema_dependencies(&mut schema.dependencies);
|
|
||||||
|
|
||||||
match &mut self.0.components {
|
|
||||||
Some(comp) => {
|
|
||||||
comp.schemas.insert(name, Item(schema.into_schema()));
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
let mut comp = Components::default();
|
|
||||||
comp.schemas.insert(name, Item(schema.into_schema()));
|
|
||||||
self.0.components = Some(comp);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>)
|
|
||||||
{
|
|
||||||
let keys : Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
|
|
||||||
for dep in keys
|
|
||||||
{
|
|
||||||
let dep_schema = dependencies.swap_remove(&dep);
|
|
||||||
if let Some(dep_schema) = dep_schema
|
|
||||||
{
|
|
||||||
self.add_schema_impl(dep, dep_schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_schema<T : OpenapiType>(&mut self) -> ReferenceOr<Schema>
|
|
||||||
{
|
|
||||||
let mut schema = T::schema();
|
|
||||||
match schema.name.clone() {
|
|
||||||
Some(name) => {
|
|
||||||
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
|
|
||||||
self.add_schema_impl(name, schema);
|
|
||||||
reference
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
self.add_schema_dependencies(&mut schema.dependencies);
|
|
||||||
Item(schema.into_schema())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct OpenapiHandler(OpenAPI);
|
|
||||||
|
|
||||||
impl OpenapiHandler
|
|
||||||
{
|
|
||||||
fn new(openapi : &OpenapiRouter) -> Self
|
|
||||||
{
|
|
||||||
Self(openapi.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NewHandler for OpenapiHandler
|
|
||||||
{
|
|
||||||
type Instance = Self;
|
|
||||||
|
|
||||||
fn new_handler(&self) -> gotham::error::Result<Self::Instance>
|
|
||||||
{
|
|
||||||
Ok(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
|
||||||
const SECURITY_NAME : &'static str = "authToken";
|
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
|
||||||
fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>>
|
|
||||||
{
|
|
||||||
use crate::AuthSource;
|
|
||||||
use gotham::state::FromState;
|
|
||||||
|
|
||||||
let source = match AuthSource::try_borrow_from(state) {
|
|
||||||
Some(source) => source,
|
|
||||||
None => return Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let security_scheme = match source {
|
|
||||||
AuthSource::Cookie(name) => SecurityScheme::APIKey {
|
|
||||||
location: APIKeyLocation::Cookie,
|
|
||||||
name: name.to_string()
|
|
||||||
},
|
|
||||||
AuthSource::Header(name) => SecurityScheme::APIKey {
|
|
||||||
location: APIKeyLocation::Header,
|
|
||||||
name: name.to_string()
|
|
||||||
},
|
|
||||||
AuthSource::AuthorizationHeader => SecurityScheme::HTTP {
|
|
||||||
scheme: "bearer".to_owned(),
|
|
||||||
bearer_format: Some("JWT".to_owned())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut security_schemes : IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default();
|
|
||||||
security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme));
|
|
||||||
|
|
||||||
security_schemes
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "auth"))]
|
|
||||||
fn get_security(state : &mut State) -> (Vec<SecurityRequirement>, IndexMap<String, ReferenceOr<SecurityScheme>>)
|
|
||||||
{
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Handler for OpenapiHandler
|
|
||||||
{
|
|
||||||
fn handle(self, mut state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
let mut openapi = self.0;
|
|
||||||
let security_schemes = get_security(&mut state);
|
|
||||||
let mut components = openapi.components.unwrap_or_default();
|
|
||||||
components.security_schemes = security_schemes;
|
|
||||||
openapi.components = Some(components);
|
|
||||||
|
|
||||||
match serde_json::to_string(&openapi) {
|
|
||||||
Ok(body) => {
|
|
||||||
let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body);
|
|
||||||
Box::new(ok((state, res)))
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to handle OpenAPI request due to error: {}", e);
|
|
||||||
let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
|
||||||
Box::new(ok((state, res)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
|
|
||||||
pub trait GetOpenapi
|
|
||||||
{
|
|
||||||
fn get_openapi(&mut self, path : &str);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
content.insert(ty.to_string(), MediaType {
|
|
||||||
schema: Some(schema.clone()),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
content
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct OperationParams<'a>
|
|
||||||
{
|
|
||||||
path_params : Vec<&'a str>,
|
|
||||||
query_params : Option<OpenapiSchema>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> OperationParams<'a>
|
|
||||||
{
|
|
||||||
fn new(path_params : Vec<&'a str>, query_params : Option<OpenapiSchema>) -> Self
|
|
||||||
{
|
|
||||||
Self { path_params, query_params }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_path_params(path_params : Vec<&'a str>) -> Self
|
|
||||||
{
|
|
||||||
Self::new(path_params, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_query_params(query_params : OpenapiSchema) -> Self
|
|
||||||
{
|
|
||||||
Self::new(Vec::new(), Some(query_params))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_path_params(&self, params : &mut Vec<ReferenceOr<Parameter>>)
|
|
||||||
{
|
|
||||||
for param in &self.path_params
|
|
||||||
{
|
|
||||||
params.push(Item(Parameter::Path {
|
|
||||||
parameter_data: ParameterData {
|
|
||||||
name: param.to_string(),
|
|
||||||
description: None,
|
|
||||||
required: true,
|
|
||||||
deprecated: None,
|
|
||||||
format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())),
|
|
||||||
example: None,
|
|
||||||
examples: IndexMap::new()
|
|
||||||
},
|
|
||||||
style: Default::default(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_query_params(self, params : &mut Vec<ReferenceOr<Parameter>>)
|
|
||||||
{
|
|
||||||
let query_params = match self.query_params {
|
|
||||||
Some(qp) => qp.schema,
|
|
||||||
None => return
|
|
||||||
};
|
|
||||||
let query_params = match query_params {
|
|
||||||
SchemaKind::Type(Type::Object(ty)) => ty,
|
|
||||||
_ => panic!("Query Parameters needs to be a plain struct")
|
|
||||||
};
|
|
||||||
for (name, schema) in query_params.properties
|
|
||||||
{
|
|
||||||
let required = query_params.required.contains(&name);
|
|
||||||
params.push(Item(Parameter::Query {
|
|
||||||
parameter_data: ParameterData {
|
|
||||||
name,
|
|
||||||
description: None,
|
|
||||||
required,
|
|
||||||
deprecated: None,
|
|
||||||
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
|
||||||
example: None,
|
|
||||||
examples: IndexMap::new()
|
|
||||||
},
|
|
||||||
allow_reserved: false,
|
|
||||||
style: Default::default(),
|
|
||||||
allow_empty_value: None
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_params(self) -> Vec<ReferenceOr<Parameter>>
|
|
||||||
{
|
|
||||||
let mut params : Vec<ReferenceOr<Parameter>> = Vec::new();
|
|
||||||
self.add_path_params(&mut params);
|
|
||||||
self.add_query_params(&mut params);
|
|
||||||
params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_operation(
|
|
||||||
operation_id : Option<String>,
|
|
||||||
default_status : hyper::StatusCode,
|
|
||||||
accepted_types : Option<Vec<Mime>>,
|
|
||||||
schema : ReferenceOr<Schema>,
|
|
||||||
params : OperationParams,
|
|
||||||
body_schema : Option<ReferenceOr<Schema>>,
|
|
||||||
supported_types : Option<Vec<Mime>>,
|
|
||||||
requires_auth : bool
|
|
||||||
) -> Operation
|
|
||||||
{
|
|
||||||
let content = schema_to_content(accepted_types.unwrap_or_else(|| vec![STAR_STAR]), schema);
|
|
||||||
|
|
||||||
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
|
|
||||||
responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response {
|
|
||||||
description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(),
|
|
||||||
headers: IndexMap::new(),
|
|
||||||
content,
|
|
||||||
links: IndexMap::new()
|
|
||||||
}));
|
|
||||||
|
|
||||||
let request_body = body_schema.map(|schema| Item(OARequestBody {
|
|
||||||
description: None,
|
|
||||||
content: schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema),
|
|
||||||
required: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
let mut security = Vec::new();
|
|
||||||
if requires_auth
|
|
||||||
{
|
|
||||||
let mut sec = IndexMap::new();
|
|
||||||
sec.insert(SECURITY_NAME.to_owned(), Vec::new());
|
|
||||||
security.push(sec);
|
|
||||||
}
|
|
||||||
|
|
||||||
Operation {
|
|
||||||
tags: Vec::new(),
|
|
||||||
operation_id,
|
|
||||||
parameters: params.into_params(),
|
|
||||||
request_body,
|
|
||||||
responses: Responses {
|
|
||||||
default: None,
|
|
||||||
responses
|
|
||||||
},
|
|
||||||
deprecated: false,
|
|
||||||
security,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! implOpenapiRouter {
|
|
||||||
($implType:ident) => {
|
|
||||||
|
|
||||||
impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiRouter)
|
|
||||||
where
|
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn get_openapi(&mut self, path : &str)
|
|
||||||
{
|
|
||||||
self.0.get(path).to_new_handler(OpenapiHandler::new(&self.1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiRouter)
|
|
||||||
where
|
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn resource<R : Resource>(&mut self, path : &str)
|
|
||||||
{
|
|
||||||
R::setup((self, path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiRouter), &str)
|
|
||||||
where
|
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn read_all<Handler : ResourceReadAll>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("/{}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).read_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<Handler : ResourceRead>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("/{}/{{id}}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).read::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search<Handler : ResourceSearch>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("/{}/search", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&self.1);
|
|
||||||
item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_query_params(Handler::Query::schema()), None, None, Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).search::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create<Handler : ResourceCreate>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).1.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("/{}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.post = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).create::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_all<Handler : ResourceUpdateAll>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).1.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("/{}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).update_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update<Handler : ResourceUpdate>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
let body_schema = (self.0).1.add_schema::<Handler::Body>();
|
|
||||||
|
|
||||||
let path = format!("/{}/{{id}}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).update::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_all<Handler : ResourceDeleteAll>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("/{}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).delete_all::<Handler>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete<Handler : ResourceDelete>(&mut self)
|
|
||||||
{
|
|
||||||
let schema = (self.0).1.add_schema::<Handler::Res>();
|
|
||||||
|
|
||||||
let path = format!("/{}/{{id}}", &self.1);
|
|
||||||
let mut item = (self.0).1.remove_path(&path);
|
|
||||||
item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth()));
|
|
||||||
(self.0).1.add_path(path, item);
|
|
||||||
|
|
||||||
(&mut *(self.0).0, self.1).delete::<Handler>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implOpenapiRouter!(RouterBuilder);
|
|
||||||
implOpenapiRouter!(ScopeBuilder);
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test
|
|
||||||
{
|
|
||||||
use crate::ResourceResult;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[derive(OpenapiType)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
struct QueryParams
|
|
||||||
{
|
|
||||||
id : isize
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn params_empty()
|
|
||||||
{
|
|
||||||
let op_params = OperationParams::default();
|
|
||||||
let params = op_params.into_params();
|
|
||||||
assert!(params.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn params_from_path_params()
|
|
||||||
{
|
|
||||||
let name = "id";
|
|
||||||
let op_params = OperationParams::from_path_params(vec![name]);
|
|
||||||
let params = op_params.into_params();
|
|
||||||
let json = serde_json::to_string(¶ms).unwrap();
|
|
||||||
assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}}]"#, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn params_from_query_params()
|
|
||||||
{
|
|
||||||
let op_params = OperationParams::from_query_params(QueryParams::schema());
|
|
||||||
let params = op_params.into_params();
|
|
||||||
let json = serde_json::to_string(¶ms).unwrap();
|
|
||||||
assert_eq!(json, r#"[{"in":"query","name":"id","required":true,"schema":{"type":"integer"},"style":"form"}]"#);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn params_both()
|
|
||||||
{
|
|
||||||
let name = "id";
|
|
||||||
let op_params = OperationParams::new(vec![name], Some(QueryParams::schema()));
|
|
||||||
let params = op_params.into_params();
|
|
||||||
let json = serde_json::to_string(¶ms).unwrap();
|
|
||||||
assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_content_schema_to_content()
|
|
||||||
{
|
|
||||||
let types = NoContent::accepted_types();
|
|
||||||
let schema = <NoContent as OpenapiType>::schema();
|
|
||||||
let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema()));
|
|
||||||
assert!(content.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn raw_schema_to_content()
|
|
||||||
{
|
|
||||||
let types = Raw::<&str>::accepted_types();
|
|
||||||
let schema = <Raw<&str> as OpenapiType>::schema();
|
|
||||||
let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema()));
|
|
||||||
assert_eq!(content.len(), 1);
|
|
||||||
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
|
||||||
assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,384 +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, HashSet};
|
|
||||||
|
|
||||||
/**
|
|
||||||
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> OpenapiType for HashSet<T>
|
|
||||||
{
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
<Vec<T> as OpenapiType>::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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:snake $(_ $generic:snake)*>]()
|
|
||||||
{
|
|
||||||
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!(Value => r#"{"nullable":true}"#);
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType};
|
|
||||||
use gotham::{
|
|
||||||
extractor::QueryStringExtractor,
|
|
||||||
state::State
|
|
||||||
};
|
|
||||||
use hyper::Body;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use std::panic::RefUnwindSafe;
|
|
||||||
|
|
||||||
/// This trait must be implemented by every RESTful Resource. It will
|
|
||||||
/// allow you to register the different methods for this Resource.
|
|
||||||
pub trait Resource
|
|
||||||
{
|
|
||||||
/// The name of this resource. Must be unique.
|
|
||||||
fn name() -> String;
|
|
||||||
|
|
||||||
/// Setup all routes of this resource. Take a look at the rest_resource!
|
|
||||||
/// macro if you don't feel like caring yourself.
|
|
||||||
fn setup<D : DrawResourceRoutes>(route : D);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ResourceMethod
|
|
||||||
{
|
|
||||||
type Res : ResourceResult;
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn operation_id() -> Option<String>
|
|
||||||
{
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wants_auth() -> bool
|
|
||||||
{
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a GET request on the Resource root.
|
|
||||||
pub trait ResourceReadAll : ResourceMethod
|
|
||||||
{
|
|
||||||
fn read_all(state : &mut State) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a GET request on the Resource with an id.
|
|
||||||
pub trait ResourceRead : ResourceMethod
|
|
||||||
{
|
|
||||||
type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static;
|
|
||||||
|
|
||||||
fn read(state : &mut State, id : Self::ID) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a GET request on the Resource with additional search parameters.
|
|
||||||
pub trait ResourceSearch : ResourceMethod
|
|
||||||
{
|
|
||||||
type Query : ResourceType + QueryStringExtractor<Body> + Sync;
|
|
||||||
|
|
||||||
fn search(state : &mut State, query : Self::Query) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a POST request on the Resource root.
|
|
||||||
pub trait ResourceCreate : ResourceMethod
|
|
||||||
{
|
|
||||||
type Body : RequestBody;
|
|
||||||
|
|
||||||
fn create(state : &mut State, body : Self::Body) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a PUT request on the Resource root.
|
|
||||||
pub trait ResourceUpdateAll : ResourceMethod
|
|
||||||
{
|
|
||||||
type Body : RequestBody;
|
|
||||||
|
|
||||||
fn update_all(state : &mut State, body : Self::Body) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a PUT request on the Resource with an id.
|
|
||||||
pub trait ResourceUpdate : ResourceMethod
|
|
||||||
{
|
|
||||||
type Body : RequestBody;
|
|
||||||
type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static;
|
|
||||||
|
|
||||||
fn update(state : &mut State, id : Self::ID, body : Self::Body) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a DELETE request on the Resource root.
|
|
||||||
pub trait ResourceDeleteAll : ResourceMethod
|
|
||||||
{
|
|
||||||
fn delete_all(state : &mut State) -> Self::Res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a DELETE request on the Resource with an id.
|
|
||||||
pub trait ResourceDelete : ResourceMethod
|
|
||||||
{
|
|
||||||
type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static;
|
|
||||||
|
|
||||||
fn delete(state : &mut State, id : Self::ID) -> Self::Res;
|
|
||||||
}
|
|
|
@ -1,588 +0,0 @@
|
||||||
use crate::{ResponseBody, StatusCode};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::{OpenapiSchema, OpenapiType};
|
|
||||||
use hyper::Body;
|
|
||||||
#[cfg(feature = "errorlog")]
|
|
||||||
use log::error;
|
|
||||||
use mime::{Mime, APPLICATION_JSON, STAR_STAR};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde_json::error::Error as SerdeJsonError;
|
|
||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
fmt::Debug
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A response, used to create the final gotham response from.
|
|
||||||
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)]
|
|
||||||
fn full_body(self) -> Vec<u8>
|
|
||||||
{
|
|
||||||
use futures::{future::Future, stream::Stream};
|
|
||||||
|
|
||||||
let bytes : &[u8] = &self.body.concat2().wait().unwrap().into_bytes();
|
|
||||||
bytes.to_vec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait provided to convert a resource's result to json.
|
|
||||||
pub trait ResourceResult
|
|
||||||
{
|
|
||||||
/// Turn this into a response that can be returned to the browser. This api will likely
|
|
||||||
/// change in the future.
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>;
|
|
||||||
|
|
||||||
/// 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() -> StatusCode
|
|
||||||
{
|
|
||||||
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 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 : std::fmt::Display>(e : E)
|
|
||||||
{
|
|
||||||
error!("The handler encountered an error: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "errorlog"))]
|
|
||||||
fn errorlog<E>(_e : E) {}
|
|
||||||
|
|
||||||
impl<R : ResponseBody, E : Error> ResourceResult for Result<R, E>
|
|
||||||
{
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
Ok(match self {
|
|
||||||
Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?),
|
|
||||||
Err(e) => {
|
|
||||||
errorlog(&e);
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
R::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
This can be returned from a resource when there is no cause of an error. For 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)]
|
|
||||||
# struct MyResource;
|
|
||||||
#
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
# #[derive(OpenapiType)]
|
|
||||||
struct MyResponse {
|
|
||||||
message: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rest_read_all(MyResource)]
|
|
||||||
fn read_all(_state: &mut State) -> Success<MyResponse> {
|
|
||||||
let res = MyResponse { message: "I'm always happy".to_string() };
|
|
||||||
res.into()
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
pub struct Success<T>(T);
|
|
||||||
|
|
||||||
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 : Debug> Debug for Success<T>
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Success({:?})", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResponseBody> ResourceResult for Success<T>
|
|
||||||
{
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
T::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
This return type can be used to map another `ResourceResult` that can only be returned if the
|
|
||||||
client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can
|
|
||||||
look something like this (assuming the `auth` feature is enabled):
|
|
||||||
|
|
||||||
```
|
|
||||||
# #[macro_use] extern crate gotham_restful_derive;
|
|
||||||
# mod doc_tests_are_broken {
|
|
||||||
# use gotham::state::State;
|
|
||||||
# use gotham_restful::*;
|
|
||||||
# use serde::Deserialize;
|
|
||||||
#
|
|
||||||
# #[derive(Resource)]
|
|
||||||
# struct MyResource;
|
|
||||||
#
|
|
||||||
# #[derive(Clone, Deserialize)]
|
|
||||||
# struct MyAuthData { exp : u64 }
|
|
||||||
#
|
|
||||||
#[rest_read_all(MyResource)]
|
|
||||||
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent> {
|
|
||||||
let auth_data = match auth {
|
|
||||||
AuthStatus::Authenticated(data) => data,
|
|
||||||
_ => return AuthErr
|
|
||||||
};
|
|
||||||
// do something
|
|
||||||
NoContent::default().into()
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
pub enum AuthResult<T>
|
|
||||||
{
|
|
||||||
Ok(T),
|
|
||||||
AuthErr
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> AuthResult<T>
|
|
||||||
{
|
|
||||||
pub fn is_ok(&self) -> bool
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::Ok(_) => true,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unwrap(self) -> T
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::Ok(data) => data,
|
|
||||||
_ => panic!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<T> for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn from(t : T) -> Self
|
|
||||||
{
|
|
||||||
Self::Ok(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Clone> Clone for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::Ok(t) => Self::Ok(t.clone()),
|
|
||||||
Self::AuthErr => Self::AuthErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Debug> Debug for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Ok(t) => write!(f, "Ok({:?})", t),
|
|
||||||
Self::AuthErr => write!(f, "AuthErr")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResourceResult> ResourceResult for AuthResult<T>
|
|
||||||
{
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
match self
|
|
||||||
{
|
|
||||||
Self::Ok(res) => res.into_response(),
|
|
||||||
Self::AuthErr => Ok(Response::forbidden())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
T::accepted_types()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
T::schema()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn default_status() -> StatusCode
|
|
||||||
{
|
|
||||||
T::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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)]
|
|
||||||
# struct MyResource;
|
|
||||||
#
|
|
||||||
#[rest_read_all(MyResource)]
|
|
||||||
fn read_all(_state: &mut State) {
|
|
||||||
// do something
|
|
||||||
}
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct NoContent;
|
|
||||||
|
|
||||||
impl From<()> for NoContent
|
|
||||||
{
|
|
||||||
fn from(_ : ()) -> Self
|
|
||||||
{
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResourceResult for NoContent
|
|
||||||
{
|
|
||||||
/// This will always be a _204 No Content_ together with an empty string.
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
Ok(Response::no_content())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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() -> StatusCode
|
|
||||||
{
|
|
||||||
StatusCode::NO_CONTENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E : Error> ResourceResult for Result<NoContent, E>
|
|
||||||
{
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(nc) => nc.into_response(),
|
|
||||||
Err(e) => {
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
<NoContent as ResourceResult>::schema()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn default_status() -> StatusCode
|
|
||||||
{
|
|
||||||
NoContent::default_status()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Raw<T>
|
|
||||||
{
|
|
||||||
pub raw : T,
|
|
||||||
pub mime : Mime
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Raw<T>
|
|
||||||
{
|
|
||||||
pub fn new(raw : T, mime : Mime) -> Self
|
|
||||||
{
|
|
||||||
Self { raw, mime }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Clone> Clone for Raw<T>
|
|
||||||
{
|
|
||||||
fn clone(&self) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
raw: self.raw.clone(),
|
|
||||||
mime: self.mime.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Debug> Debug for Raw<T>
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Raw({:?}, {:?})", self.raw, self.mime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : Into<Body>> ResourceResult for Raw<T>
|
|
||||||
{
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
Ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![STAR_STAR])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
|
||||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
|
||||||
..Default::default()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E : Error> ResourceResult for Result<Raw<T>, E>
|
|
||||||
where
|
|
||||||
Raw<T> : ResourceResult
|
|
||||||
{
|
|
||||||
fn into_response(self) -> Result<Response, SerdeJsonError>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Ok(raw) => raw.into_response(),
|
|
||||||
Err(e) => {
|
|
||||||
let err : ResourceError = e.into();
|
|
||||||
Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn accepted_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
<Raw<T> as ResourceResult>::accepted_types()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn schema() -> OpenapiSchema
|
|
||||||
{
|
|
||||||
<Raw<T> as ResourceResult>::schema()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test
|
|
||||||
{
|
|
||||||
use super::*;
|
|
||||||
use mime::TEXT_PLAIN;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
|
||||||
struct Msg
|
|
||||||
{
|
|
||||||
msg : String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Error)]
|
|
||||||
#[error("An Error")]
|
|
||||||
struct MsgError;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resource_result_ok()
|
|
||||||
{
|
|
||||||
let ok : Result<Msg, MsgError> = Ok(Msg::default());
|
|
||||||
let res = 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(), r#"{"msg":""}"#.as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resource_result_err()
|
|
||||||
{
|
|
||||||
let err : Result<Msg, MsgError> = Err(MsgError::default());
|
|
||||||
let res = 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(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn success_always_successfull()
|
|
||||||
{
|
|
||||||
let success : Success<Msg> = Msg::default().into();
|
|
||||||
let res = 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(), r#"{"msg":""}"#.as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_content_has_empty_response()
|
|
||||||
{
|
|
||||||
let no_content = NoContent::default();
|
|
||||||
let res = 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(), &[] as &[u8]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_content_result()
|
|
||||||
{
|
|
||||||
let no_content : Result<NoContent, MsgError> = Ok(NoContent::default());
|
|
||||||
let res = 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(), &[] as &[u8]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn raw_response()
|
|
||||||
{
|
|
||||||
let msg = "Test";
|
|
||||||
let raw = Raw::new(msg, TEXT_PLAIN);
|
|
||||||
let res = raw.into_response().expect("didn't expect error response");
|
|
||||||
assert_eq!(res.status, StatusCode::OK);
|
|
||||||
assert_eq!(res.mime, Some(TEXT_PLAIN));
|
|
||||||
assert_eq!(res.full_body(), msg.as_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,377 +0,0 @@
|
||||||
use crate::{
|
|
||||||
resource::*,
|
|
||||||
result::{ResourceError, ResourceResult, Response},
|
|
||||||
RequestBody,
|
|
||||||
StatusCode
|
|
||||||
};
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::OpenapiRouter;
|
|
||||||
|
|
||||||
use futures::{
|
|
||||||
future::{Future, err, ok},
|
|
||||||
stream::Stream
|
|
||||||
};
|
|
||||||
use gotham::{
|
|
||||||
handler::{HandlerFuture, IntoHandlerError},
|
|
||||||
helpers::http::response::{create_empty_response, create_response},
|
|
||||||
pipeline::chain::PipelineHandleChain,
|
|
||||||
router::{
|
|
||||||
builder::*,
|
|
||||||
non_match::RouteNonMatch,
|
|
||||||
route::matcher::{
|
|
||||||
content_type::ContentTypeHeaderRouteMatcher,
|
|
||||||
AcceptHeaderRouteMatcher,
|
|
||||||
RouteMatcher
|
|
||||||
}
|
|
||||||
},
|
|
||||||
state::{FromState, State}
|
|
||||||
};
|
|
||||||
use hyper::{
|
|
||||||
header::CONTENT_TYPE,
|
|
||||||
Body,
|
|
||||||
HeaderMap,
|
|
||||||
Method
|
|
||||||
};
|
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
|
||||||
use std::panic::RefUnwindSafe;
|
|
||||||
|
|
||||||
/// Allow us to extract an id from a path.
|
|
||||||
#[derive(Deserialize, StateData, StaticResponseExtender)]
|
|
||||||
struct PathExtractor<ID : RefUnwindSafe + Send + 'static>
|
|
||||||
{
|
|
||||||
id : ID
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// an OpenAPI specification on request.
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub trait WithOpenapi<D>
|
|
||||||
{
|
|
||||||
fn with_openapi<F>(&mut self, title : String, version : String, server_url : String, block : F)
|
|
||||||
where
|
|
||||||
F : FnOnce((&mut D, &mut OpenapiRouter));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
|
||||||
/// any RESTful `Resource` with a path.
|
|
||||||
pub trait DrawResources
|
|
||||||
{
|
|
||||||
fn resource<R : Resource>(&mut self, path : &str);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This trait allows to draw routes within an resource. Use this only inside the
|
|
||||||
/// `Resource::setup` method.
|
|
||||||
pub trait DrawResourceRoutes
|
|
||||||
{
|
|
||||||
fn read_all<Handler : ResourceReadAll>(&mut self);
|
|
||||||
fn read<Handler : ResourceRead>(&mut self);
|
|
||||||
fn search<Handler : ResourceSearch>(&mut self);
|
|
||||||
fn create<Handler : ResourceCreate>(&mut self);
|
|
||||||
fn update_all<Handler : ResourceUpdateAll>(&mut self);
|
|
||||||
fn update<Handler : ResourceUpdate>(&mut self);
|
|
||||||
fn delete_all<Handler : ResourceDeleteAll>(&mut self);
|
|
||||||
fn delete<Handler : ResourceDelete>(&mut self);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn response_from(res : Response, state : &State) -> hyper::Response<Body>
|
|
||||||
{
|
|
||||||
let mut r = create_empty_response(state, res.status);
|
|
||||||
if let Some(mime) = res.mime
|
|
||||||
{
|
|
||||||
r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap());
|
|
||||||
}
|
|
||||||
if Method::borrow_from(state) != Method::HEAD
|
|
||||||
{
|
|
||||||
*r.body_mut() = res.body;
|
|
||||||
}
|
|
||||||
r
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_handler_future<F, R>(mut state : State, get_result : F) -> Box<HandlerFuture>
|
|
||||||
where
|
|
||||||
F : FnOnce(&mut State) -> R,
|
|
||||||
R : ResourceResult
|
|
||||||
{
|
|
||||||
let res = get_result(&mut state).into_response();
|
|
||||||
match res {
|
|
||||||
Ok(res) => {
|
|
||||||
let r = response_from(res, &state);
|
|
||||||
Box::new(ok((state, r)))
|
|
||||||
},
|
|
||||||
Err(e) => Box::new(err((state, e.into_handler_error())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_with_body<Body, F, R>(mut state : State, get_result : F) -> Box<HandlerFuture>
|
|
||||||
where
|
|
||||||
Body : RequestBody,
|
|
||||||
F : FnOnce(&mut State, Body) -> R + Send + 'static,
|
|
||||||
R : ResourceResult
|
|
||||||
{
|
|
||||||
let f = hyper::Body::take_from(&mut state)
|
|
||||||
.concat2()
|
|
||||||
.then(|body| {
|
|
||||||
|
|
||||||
let body = match body {
|
|
||||||
Ok(body) => body,
|
|
||||||
Err(e) => return err((state, e.into_handler_error()))
|
|
||||||
};
|
|
||||||
|
|
||||||
let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) {
|
|
||||||
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
|
||||||
None => {
|
|
||||||
let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
|
||||||
return ok((state, res))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let body = match Body::from_body(body, content_type) {
|
|
||||||
Ok(body) => body,
|
|
||||||
Err(e) => return {
|
|
||||||
let error : ResourceError = e.into();
|
|
||||||
match serde_json::to_string(&error) {
|
|
||||||
Ok(json) => {
|
|
||||||
let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
|
||||||
ok((state, res))
|
|
||||||
},
|
|
||||||
Err(e) => err((state, e.into_handler_error()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = get_result(&mut state, body).into_response();
|
|
||||||
match res {
|
|
||||||
Ok(res) => {
|
|
||||||
let r = response_from(res, &state);
|
|
||||||
ok((state, r))
|
|
||||||
},
|
|
||||||
Err(e) => err((state, e.into_handler_error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Box::new(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_all_handler<Handler : ResourceReadAll>(state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
to_handler_future(state, |state| Handler::read_all(state))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_handler<Handler : ResourceRead>(state : State) -> 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_handler<Handler : ResourceSearch>(mut state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
let query = Handler::Query::take_from(&mut state);
|
|
||||||
to_handler_future(state, |state| Handler::search(state, query))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_handler<Handler : ResourceCreate>(state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::create(state, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_all_handler<Handler : ResourceUpdateAll>(state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::update_all(state, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_handler<Handler : ResourceUpdate>(state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
let id = {
|
|
||||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
|
||||||
path.id.clone()
|
|
||||||
};
|
|
||||||
handle_with_body::<Handler::Body, _, _>(state, |state, body| Handler::update(state, id, body))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_all_handler<Handler : ResourceDeleteAll>(state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
to_handler_future(state, |state| Handler::delete_all(state))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_handler<Handler : ResourceDelete>(state : State) -> Box<HandlerFuture>
|
|
||||||
{
|
|
||||||
let id = {
|
|
||||||
let path : &PathExtractor<Handler::ID> = PathExtractor::borrow_from(&state);
|
|
||||||
path.id.clone()
|
|
||||||
};
|
|
||||||
to_handler_future(state, |state| Handler::delete(state, id))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MaybeMatchAcceptHeader
|
|
||||||
{
|
|
||||||
matcher : Option<AcceptHeaderRouteMatcher>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouteMatcher for MaybeMatchAcceptHeader
|
|
||||||
{
|
|
||||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
|
||||||
{
|
|
||||||
match &self.matcher {
|
|
||||||
Some(matcher) => matcher.is_match(state),
|
|
||||||
None => Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader
|
|
||||||
{
|
|
||||||
fn from(types : Option<Vec<Mime>>) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
matcher: types.map(AcceptHeaderRouteMatcher::new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct MaybeMatchContentTypeHeader
|
|
||||||
{
|
|
||||||
matcher : Option<ContentTypeHeaderRouteMatcher>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouteMatcher for MaybeMatchContentTypeHeader
|
|
||||||
{
|
|
||||||
fn is_match(&self, state : &State) -> Result<(), RouteNonMatch>
|
|
||||||
{
|
|
||||||
match &self.matcher {
|
|
||||||
Some(matcher) => matcher.is_match(state),
|
|
||||||
None => Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader
|
|
||||||
{
|
|
||||||
fn from(types : Option<Vec<Mime>>) -> Self
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
matcher: types.map(ContentTypeHeaderRouteMatcher::new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! implDrawResourceRoutes {
|
|
||||||
($implType:ident) => {
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
|
|
||||||
where
|
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn with_openapi<F>(&mut self, title : String, version : String, server_url : String, block : F)
|
|
||||||
where
|
|
||||||
F : FnOnce((&mut Self, &mut OpenapiRouter))
|
|
||||||
{
|
|
||||||
let mut router = OpenapiRouter::new(title, version, server_url);
|
|
||||||
block((self, &mut router));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, C, P> DrawResources for $implType<'a, C, P>
|
|
||||||
where
|
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn resource<R : Resource>(&mut self, path : &str)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
where
|
|
||||||
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
||||||
P : RefUnwindSafe + Send + Sync + 'static
|
|
||||||
{
|
|
||||||
fn read_all<Handler : ResourceReadAll>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.get(&self.1)
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.to(|state| read_all_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read<Handler : ResourceRead>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.get(&format!("{}/:id", self.1))
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| read_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search<Handler : ResourceSearch>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.get(&format!("{}/search", self.1))
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_query_string_extractor::<Handler::Query>()
|
|
||||||
.to(|state| search_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create<Handler : ResourceCreate>(&mut self)
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_all<Handler : ResourceUpdateAll>(&mut self)
|
|
||||||
{
|
|
||||||
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| update_all_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update<Handler : ResourceUpdate>(&mut self)
|
|
||||||
{
|
|
||||||
let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into();
|
|
||||||
self.0.put(&format!("{}/:id", self.1))
|
|
||||||
.extend_route_matcher(accept_matcher)
|
|
||||||
.extend_route_matcher(content_matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| update_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_all<Handler : ResourceDeleteAll>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.delete(&self.1)
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.to(|state| delete_all_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete<Handler : ResourceDelete>(&mut self)
|
|
||||||
{
|
|
||||||
let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into();
|
|
||||||
self.0.delete(&format!("{}/:id", self.1))
|
|
||||||
.extend_route_matcher(matcher)
|
|
||||||
.with_path_extractor::<PathExtractor<Handler::ID>>()
|
|
||||||
.to(|state| delete_handler::<Handler>(state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implDrawResourceRoutes!(RouterBuilder);
|
|
||||||
implDrawResourceRoutes!(ScopeBuilder);
|
|
|
@ -1,80 +0,0 @@
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
use crate::OpenapiType;
|
|
||||||
use crate::result::ResourceError;
|
|
||||||
|
|
||||||
use hyper::Chunk;
|
|
||||||
use mime::{Mime, APPLICATION_JSON};
|
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
|
||||||
pub trait ResourceType
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
|
||||||
impl<T> ResourceType for T
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
pub trait ResourceType : OpenapiType
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
impl<T : OpenapiType> ResourceType for T
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// `OpenapiType`.
|
|
||||||
pub trait ResponseBody : ResourceType + Serialize
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResourceType + Serialize> ResponseBody for T
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// This trait must be implemented by every type that can be used as a request body. It allows
|
|
||||||
/// to create the type from a hyper body chunk and it's content type.
|
|
||||||
pub trait FromBody : Sized
|
|
||||||
{
|
|
||||||
type Err : Into<ResourceError>;
|
|
||||||
|
|
||||||
/// Create the request body from a raw body and the content type.
|
|
||||||
fn from_body(body : Chunk, content_type : Mime) -> Result<Self, Self::Err>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : DeserializeOwned> FromBody for T
|
|
||||||
{
|
|
||||||
type Err = serde_json::Error;
|
|
||||||
|
|
||||||
fn from_body(body : Chunk, _content_type : Mime) -> Result<Self, Self::Err>
|
|
||||||
{
|
|
||||||
serde_json::from_slice(&body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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`.
|
|
||||||
pub trait RequestBody : ResourceType + FromBody
|
|
||||||
{
|
|
||||||
/// Return all types that are supported as content types.
|
|
||||||
fn supported_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T : ResourceType + DeserializeOwned> RequestBody for T
|
|
||||||
{
|
|
||||||
fn supported_types() -> Option<Vec<Mime>>
|
|
||||||
{
|
|
||||||
Some(vec![APPLICATION_JSON])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE-Apache
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE-EPL
|
|
|
@ -1 +0,0 @@
|
||||||
../LICENSE.md
|
|
|
@ -1,68 +0,0 @@
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{
|
|
||||||
spanned::Spanned,
|
|
||||||
Error,
|
|
||||||
Fields,
|
|
||||||
ItemStruct,
|
|
||||||
parse_macro_input
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn expand_from_body(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
expand(tokens)
|
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand(tokens : TokenStream) -> Result<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
let input = parse_macro_input::parse::<ItemStruct>(tokens)?;
|
|
||||||
let ident = input.ident;
|
|
||||||
let generics = input.generics;
|
|
||||||
|
|
||||||
let (were, body) = match input.fields {
|
|
||||||
Fields::Named(named) => {
|
|
||||||
let fields = named.named;
|
|
||||||
match fields.len() {
|
|
||||||
0 => (quote!(), quote!(Self{})),
|
|
||||||
1 => {
|
|
||||||
let field = fields.first().unwrap();
|
|
||||||
let field_ident = field.ident.as_ref().unwrap();
|
|
||||||
let field_ty = &field.ty;
|
|
||||||
(quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self { #field_ident: body.into() }))
|
|
||||||
},
|
|
||||||
_ => return Err(Error::new(fields.into_iter().nth(1).unwrap().span(), "FromBody can only be derived for structs with at most one field"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Fields::Unnamed(unnamed) => {
|
|
||||||
let fields = unnamed.unnamed;
|
|
||||||
match fields.len() {
|
|
||||||
0 => (quote!(), quote!(Self{})),
|
|
||||||
1 => {
|
|
||||||
let field = fields.first().unwrap();
|
|
||||||
let field_ty = &field.ty;
|
|
||||||
(quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self(body.into())))
|
|
||||||
},
|
|
||||||
_ => return Err(Error::new(fields.into_iter().nth(1).unwrap().span(), "FromBody can only be derived for structs with at most one field"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Fields::Unit => (quote!(), quote!(Self{}))
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(quote! {
|
|
||||||
impl #generics #krate::FromBody for #ident #generics
|
|
||||||
#were
|
|
||||||
{
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_body(body : #krate::Chunk, _content_type : #krate::Mime) -> Result<Self, Self::Err>
|
|
||||||
{
|
|
||||||
let body : &[u8] = &body;
|
|
||||||
Ok(#body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
extern crate proc_macro;
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::quote;
|
|
||||||
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
mod from_body;
|
|
||||||
use from_body::expand_from_body;
|
|
||||||
mod method;
|
|
||||||
use method::{expand_method, Method};
|
|
||||||
mod request_body;
|
|
||||||
use request_body::expand_request_body;
|
|
||||||
mod resource;
|
|
||||||
use resource::expand_resource;
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
mod openapi_type;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn print_tokens(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
//eprintln!("{}", tokens);
|
|
||||||
tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
fn krate() -> TokenStream2
|
|
||||||
{
|
|
||||||
quote!(::gotham_restful)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(FromBody)]
|
|
||||||
pub fn derive_from_body(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_from_body(tokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
#[proc_macro_derive(OpenapiType, attributes(openapi))]
|
|
||||||
pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(openapi_type::expand(tokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(RequestBody, attributes(supported_types))]
|
|
||||||
pub fn derive_request_body(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_request_body(tokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_derive(Resource, attributes(rest_resource))]
|
|
||||||
pub fn derive_resource(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_resource(tokens))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::ReadAll, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_read(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::Read, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_search(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::Search, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_create(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::Create, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_update_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::UpdateAll, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_update(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::Update, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_delete_all(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::DeleteAll, attr, item))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
|
||||||
pub fn rest_delete(attr : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
print_tokens(expand_method(Method::Delete, attr, item))
|
|
||||||
}
|
|
|
@ -1,459 +0,0 @@
|
||||||
use crate::util::CollectToResult;
|
|
||||||
use heck::{CamelCase, SnakeCase};
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
|
|
||||||
use quote::{format_ident, quote};
|
|
||||||
use syn::{
|
|
||||||
spanned::Spanned,
|
|
||||||
Attribute,
|
|
||||||
AttributeArgs,
|
|
||||||
Error,
|
|
||||||
FnArg,
|
|
||||||
ItemFn,
|
|
||||||
Lit,
|
|
||||||
LitBool,
|
|
||||||
Meta,
|
|
||||||
NestedMeta,
|
|
||||||
PatType,
|
|
||||||
ReturnType,
|
|
||||||
Type,
|
|
||||||
parse_macro_input
|
|
||||||
};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
pub enum Method
|
|
||||||
{
|
|
||||||
ReadAll,
|
|
||||||
Read,
|
|
||||||
Search,
|
|
||||||
Create,
|
|
||||||
UpdateAll,
|
|
||||||
Update,
|
|
||||||
DeleteAll,
|
|
||||||
Delete
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Method
|
|
||||||
{
|
|
||||||
type Err = String;
|
|
||||||
fn from_str(str : &str) -> Result<Self, Self::Err>
|
|
||||||
{
|
|
||||||
match str {
|
|
||||||
"ReadAll" | "read_all" => Ok(Self::ReadAll),
|
|
||||||
"Read" | "read" => Ok(Self::Read),
|
|
||||||
"Search" | "search" => Ok(Self::Search),
|
|
||||||
"Create" | "create" => Ok(Self::Create),
|
|
||||||
"UpdateAll" | "update_all" => Ok(Self::UpdateAll),
|
|
||||||
"Update" | "update" => Ok(Self::Update),
|
|
||||||
"DeleteAll" | "delete_all" => Ok(Self::DeleteAll),
|
|
||||||
"Delete" | "delete" => Ok(Self::Delete),
|
|
||||||
_ => Err(format!("Unknown method: `{}'", str))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Method
|
|
||||||
{
|
|
||||||
pub fn type_names(&self) -> Vec<&'static str>
|
|
||||||
{
|
|
||||||
use Method::*;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
ReadAll => vec![],
|
|
||||||
Read => vec!["ID"],
|
|
||||||
Search => vec!["Query"],
|
|
||||||
Create => vec!["Body"],
|
|
||||||
UpdateAll => vec!["Body"],
|
|
||||||
Update => vec!["ID", "Body"],
|
|
||||||
DeleteAll => vec![],
|
|
||||||
Delete => vec!["ID"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trait_ident(&self) -> Ident
|
|
||||||
{
|
|
||||||
use Method::*;
|
|
||||||
|
|
||||||
let name = match self {
|
|
||||||
ReadAll => "ReadAll",
|
|
||||||
Read => "Read",
|
|
||||||
Search => "Search",
|
|
||||||
Create => "Create",
|
|
||||||
UpdateAll => "UpdateAll",
|
|
||||||
Update => "Update",
|
|
||||||
DeleteAll => "DeleteAll",
|
|
||||||
Delete => "Delete"
|
|
||||||
};
|
|
||||||
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",
|
|
||||||
UpdateAll => "update_all",
|
|
||||||
Update => "update",
|
|
||||||
DeleteAll => "delete_all",
|
|
||||||
Delete => "delete"
|
|
||||||
};
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MethodArgumentType
|
|
||||||
{
|
|
||||||
StateRef,
|
|
||||||
StateMutRef,
|
|
||||||
MethodArg(Type),
|
|
||||||
DatabaseConnection(Type),
|
|
||||||
AuthStatus(Type),
|
|
||||||
AuthStatusRef(Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MethodArgumentType
|
|
||||||
{
|
|
||||||
fn is_method_arg(&self) -> bool
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::MethodArg(_) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_database_conn(&self) -> bool
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::DatabaseConnection(_) => true,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_auth_status(&self) -> bool
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::AuthStatus(_) | Self::AuthStatusRef(_) => true,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn quote_ty(&self) -> Option<TokenStream2>
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::MethodArg(ty) => Some(quote!(#ty)),
|
|
||||||
Self::DatabaseConnection(ty) => Some(quote!(#ty)),
|
|
||||||
Self::AuthStatus(ty) => Some(quote!(#ty)),
|
|
||||||
Self::AuthStatusRef(ty) => Some(quote!(#ty)),
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MethodArgument
|
|
||||||
{
|
|
||||||
ident : Ident,
|
|
||||||
ident_span : Span,
|
|
||||||
ty : MethodArgumentType
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Spanned for MethodArgument
|
|
||||||
{
|
|
||||||
fn span(&self) -> Span
|
|
||||||
{
|
|
||||||
self.ident_span
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn interpret_arg_ty(index : usize, attrs : &[Attribute], name : &str, ty : Type) -> Result<MethodArgumentType, Error>
|
|
||||||
{
|
|
||||||
let attr = attrs.into_iter()
|
|
||||||
.filter(|arg| arg.path.segments.iter().filter(|path| &path.ident.to_string() == "rest_arg").nth(0).is_some())
|
|
||||||
.nth(0)
|
|
||||||
.map(|arg| arg.tokens.to_string());
|
|
||||||
|
|
||||||
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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if index == 0
|
|
||||||
{
|
|
||||||
return match ty {
|
|
||||||
Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }),
|
|
||||||
_ => Err(Error::new(ty.span(), "The first argument, unless some feature is used, has to be a (mutable) reference to gotham::state::State"))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(MethodArgumentType::MethodArg(ty))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn interpret_arg(index : usize, arg : &PatType) -> Result<MethodArgument, Error>
|
|
||||||
{
|
|
||||||
let pat = &arg.pat;
|
|
||||||
let ident = format_ident!("arg{}", index);
|
|
||||||
let orig_name = quote!(#pat);
|
|
||||||
let ty = interpret_arg_ty(index, &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 : &AttributeArgs) -> TokenStream2
|
|
||||||
{
|
|
||||||
let mut operation_id : Option<&Lit> = None;
|
|
||||||
for meta in attrs
|
|
||||||
{
|
|
||||||
match meta {
|
|
||||||
NestedMeta::Meta(Meta::NameValue(kv)) => {
|
|
||||||
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(_ : &AttributeArgs) -> TokenStream2
|
|
||||||
{
|
|
||||||
quote!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_wants_auth(attrs : &AttributeArgs, default : bool) -> TokenStream2
|
|
||||||
{
|
|
||||||
let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() });
|
|
||||||
let mut wants_auth = &default_lit;
|
|
||||||
for meta in attrs
|
|
||||||
{
|
|
||||||
match meta {
|
|
||||||
NestedMeta::Meta(Meta::NameValue(kv)) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
|
|
||||||
// parse attributes
|
|
||||||
let mut method_attrs = parse_macro_input::parse::<AttributeArgs>(attrs)?;
|
|
||||||
let resource_path = match method_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 = parse_macro_input::parse::<ItemFn>(item)?;
|
|
||||||
let fun_ident = &fun.sig.ident;
|
|
||||||
let fun_vis = &fun.vis;
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
// 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<TokenStream2> = 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<TokenStream2> = 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!(#state_ident : &mut #krate::export::State));
|
|
||||||
|
|
||||||
// extract the arguments to pass over to the supplied method
|
|
||||||
let args_pass : Vec<TokenStream2> = args.iter().map(|arg| match (&arg.ty, &arg.ident) {
|
|
||||||
(MethodArgumentType::StateRef, _) => quote!(#state_ident),
|
|
||||||
(MethodArgumentType::StateMutRef, _) => quote!(#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),*));
|
|
||||||
if is_no_content
|
|
||||||
{
|
|
||||||
block = quote!(#block; Default::default())
|
|
||||||
}
|
|
||||||
if let Some(arg) = args.iter().filter(|arg| (*arg).ty.is_database_conn()).nth(0)
|
|
||||||
{
|
|
||||||
let conn_ty = arg.ty.quote_ty();
|
|
||||||
block = quote! {
|
|
||||||
let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone();
|
|
||||||
#repo_ident.run::<_, #ret, ()>(move |#conn_ident| {
|
|
||||||
Ok({#block})
|
|
||||||
}).wait().unwrap()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(arg) = args.iter().filter(|arg| (*arg).ty.is_auth_status()).nth(0)
|
|
||||||
{
|
|
||||||
let auth_ty = arg.ty.quote_ty();
|
|
||||||
block = quote! {
|
|
||||||
let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(#state_ident).clone();
|
|
||||||
#block
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(&method_attrs);
|
|
||||||
let wants_auth = expand_wants_auth(&method_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),*) -> #ret
|
|
||||||
{
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use #krate::export::{Future, FromState};
|
|
||||||
|
|
||||||
#block
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[deny(dead_code)]
|
|
||||||
pub fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D)
|
|
||||||
{
|
|
||||||
route.#method_ident::<#handler_ident>();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_method(method : Method, attrs : TokenStream, item : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
expand(method, attrs, item)
|
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
|
||||||
.into()
|
|
||||||
}
|
|
|
@ -1,310 +0,0 @@
|
||||||
use crate::util::CollectToResult;
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::{
|
|
||||||
Delimiter,
|
|
||||||
TokenStream as TokenStream2,
|
|
||||||
TokenTree
|
|
||||||
};
|
|
||||||
use quote::quote;
|
|
||||||
use std::{iter, iter::FromIterator};
|
|
||||||
use syn::{
|
|
||||||
spanned::Spanned,
|
|
||||||
Attribute,
|
|
||||||
AttributeArgs,
|
|
||||||
Error,
|
|
||||||
Field,
|
|
||||||
Fields,
|
|
||||||
Generics,
|
|
||||||
GenericParam,
|
|
||||||
Item,
|
|
||||||
ItemEnum,
|
|
||||||
ItemStruct,
|
|
||||||
Lit,
|
|
||||||
Meta,
|
|
||||||
NestedMeta,
|
|
||||||
Variant,
|
|
||||||
parse_macro_input
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn expand(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
let input = parse_macro_input!(tokens as Item);
|
|
||||||
|
|
||||||
let output = match input {
|
|
||||||
Item::Enum(item) => expand_enum(item),
|
|
||||||
Item::Struct(item) => expand_struct(item),
|
|
||||||
_ => Err(Error::new(input.span(), "derive(OpenapiType) not supported for this context"))
|
|
||||||
};
|
|
||||||
output
|
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_where(generics : &Generics) -> TokenStream2
|
|
||||||
{
|
|
||||||
if generics.params.is_empty()
|
|
||||||
{
|
|
||||||
quote!()
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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, Error>
|
|
||||||
{
|
|
||||||
match lit {
|
|
||||||
Lit::Str(str) => Ok(str.value()),
|
|
||||||
_ => Err(Error::new(lit.span(), "Expected string literal"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_bool(lit : &Lit) -> Result<bool, Error>
|
|
||||||
{
|
|
||||||
match lit {
|
|
||||||
Lit::Bool(bool) => Ok(bool.value),
|
|
||||||
_ => Err(Error::new(lit.span(), "Expected bool"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_parens(input : TokenStream2) -> TokenStream2
|
|
||||||
{
|
|
||||||
let iter = input.into_iter().flat_map(|tt| {
|
|
||||||
if let TokenTree::Group(group) = &tt
|
|
||||||
{
|
|
||||||
if group.delimiter() == Delimiter::Parenthesis
|
|
||||||
{
|
|
||||||
return Box::new(group.stream().into_iter()) as Box<dyn Iterator<Item = TokenTree>>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box::new(iter::once(tt))
|
|
||||||
});
|
|
||||||
let output = TokenStream2::from_iter(iter);
|
|
||||||
output
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_attributes(input : &[Attribute]) -> Result<Attrs, Error>
|
|
||||||
{
|
|
||||||
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());
|
|
||||||
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<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
if variant.fields != Fields::Unit
|
|
||||||
{
|
|
||||||
return Err(Error::new(variant.span(), "Enum Variants with Fields not supported"));
|
|
||||||
}
|
|
||||||
|
|
||||||
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(input : ItemEnum) -> Result<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
let ident = input.ident;
|
|
||||||
let generics = input.generics;
|
|
||||||
let where_clause = expand_where(&generics);
|
|
||||||
|
|
||||||
let attrs = parse_attributes(&input.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<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let ident = match &field.ident {
|
|
||||||
Some(ident) => ident,
|
|
||||||
None => return Err(Error::new(field.span(), "Fields without ident are not supported"))
|
|
||||||
};
|
|
||||||
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(stringify!(#ident).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()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_struct(input : ItemStruct) -> Result<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
let ident = input.ident;
|
|
||||||
let generics = input.generics;
|
|
||||||
let where_clause = expand_where(&generics);
|
|
||||||
|
|
||||||
let attrs = parse_attributes(&input.attrs)?;
|
|
||||||
let nullable = attrs.nullable;
|
|
||||||
let name = match attrs.rename {
|
|
||||||
Some(rename) => rename,
|
|
||||||
None => ident.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let fields : Vec<TokenStream2> = 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(), "Unnamed fields are not supported")),
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
use crate::util::CollectToResult;
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::quote;
|
|
||||||
use std::iter;
|
|
||||||
use syn::{
|
|
||||||
parse::{Parse, ParseStream, Result as SynResult},
|
|
||||||
punctuated::Punctuated,
|
|
||||||
token::Comma,
|
|
||||||
Error,
|
|
||||||
Generics,
|
|
||||||
Ident,
|
|
||||||
ItemStruct,
|
|
||||||
Path,
|
|
||||||
parenthesized,
|
|
||||||
parse_macro_input
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MimeList(Punctuated<Path, Comma>);
|
|
||||||
|
|
||||||
impl Parse for MimeList
|
|
||||||
{
|
|
||||||
fn parse(input: ParseStream) -> SynResult<Self>
|
|
||||||
{
|
|
||||||
let content;
|
|
||||||
let _paren = parenthesized!(content in input);
|
|
||||||
let list : Punctuated<Path, Comma> = Punctuated::parse_separated_nonempty(&content)?;
|
|
||||||
Ok(Self(list))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "openapi"))]
|
|
||||||
fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream2
|
|
||||||
{
|
|
||||||
quote!()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openapi")]
|
|
||||||
fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
impl #generics #krate::OpenapiType for #ident #generics
|
|
||||||
{
|
|
||||||
fn schema() -> #krate::OpenapiSchema
|
|
||||||
{
|
|
||||||
use #krate::{export::openapi::*, OpenapiSchema};
|
|
||||||
|
|
||||||
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
|
||||||
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
|
||||||
..Default::default()
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand(tokens : TokenStream) -> Result<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
let input = parse_macro_input::parse::<ItemStruct>(tokens)?;
|
|
||||||
let ident = input.ident;
|
|
||||||
let generics = input.generics;
|
|
||||||
|
|
||||||
let types = input.attrs.into_iter()
|
|
||||||
.filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()))
|
|
||||||
.flat_map(|attr|
|
|
||||||
syn::parse2::<MimeList>(attr.tokens)
|
|
||||||
.map(|list| Box::new(list.0.into_iter().map(|mime| Ok(mime))) as Box<dyn Iterator<Item = Result<Path, Error>>>)
|
|
||||||
.unwrap_or_else(|err| Box::new(iter::once(Err(err)))))
|
|
||||||
.collect_to_result()?;
|
|
||||||
|
|
||||||
let types = match types {
|
|
||||||
ref types if types.is_empty() => quote!(None),
|
|
||||||
types => quote!(Some(vec![#(#types),*]))
|
|
||||||
};
|
|
||||||
|
|
||||||
let impl_openapi_type = impl_openapi_type(&ident, &generics);
|
|
||||||
|
|
||||||
Ok(quote! {
|
|
||||||
impl #generics #krate::RequestBody for #ident #generics
|
|
||||||
where #ident #generics : #krate::FromBody
|
|
||||||
{
|
|
||||||
fn supported_types() -> Option<Vec<#krate::Mime>>
|
|
||||||
{
|
|
||||||
#types
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#impl_openapi_type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_request_body(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
expand(tokens)
|
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
|
||||||
.into()
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
use crate::{
|
|
||||||
method::Method,
|
|
||||||
util::CollectToResult
|
|
||||||
};
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
|
||||||
use quote::quote;
|
|
||||||
use syn::{
|
|
||||||
parse::{Parse, ParseStream},
|
|
||||||
punctuated::Punctuated,
|
|
||||||
token::Comma,
|
|
||||||
Error,
|
|
||||||
Ident,
|
|
||||||
ItemStruct,
|
|
||||||
parenthesized,
|
|
||||||
parse_macro_input
|
|
||||||
};
|
|
||||||
use std::{iter, str::FromStr};
|
|
||||||
|
|
||||||
struct MethodList(Punctuated<Ident, Comma>);
|
|
||||||
|
|
||||||
impl Parse for MethodList
|
|
||||||
{
|
|
||||||
fn parse(input: ParseStream) -> Result<Self, Error>
|
|
||||||
{
|
|
||||||
let content;
|
|
||||||
let _paren = parenthesized!(content in input);
|
|
||||||
let list : Punctuated<Ident, Comma> = Punctuated::parse_separated_nonempty(&content)?;
|
|
||||||
Ok(Self(list))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand(tokens : TokenStream) -> Result<TokenStream2, Error>
|
|
||||||
{
|
|
||||||
let krate = super::krate();
|
|
||||||
let input = parse_macro_input::parse::<ItemStruct>(tokens)?;
|
|
||||||
let ident = input.ident;
|
|
||||||
let name = ident.to_string();
|
|
||||||
|
|
||||||
let methods = input.attrs.into_iter().filter(|attr|
|
|
||||||
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("rest_resource".to_string()) // TODO wtf
|
|
||||||
).map(|attr| {
|
|
||||||
syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter())
|
|
||||||
}).flat_map(|list| match list {
|
|
||||||
Ok(iter) => Box::new(iter.map(|method| {
|
|
||||||
let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?;
|
|
||||||
let mod_ident = method.mod_ident(&name);
|
|
||||||
let ident = method.setup_ident(&name);
|
|
||||||
Ok(quote!(#mod_ident::#ident(&mut route);))
|
|
||||||
})) as Box<dyn Iterator<Item = Result<TokenStream2, Error>>>,
|
|
||||||
Err(err) => Box::new(iter::once(Err(err)))
|
|
||||||
}).collect_to_result()?;
|
|
||||||
|
|
||||||
Ok(quote! {
|
|
||||||
impl #krate::Resource for #ident
|
|
||||||
{
|
|
||||||
fn name() -> String
|
|
||||||
{
|
|
||||||
stringify!(#ident).to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
|
|
||||||
{
|
|
||||||
#(#methods)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_resource(tokens : TokenStream) -> TokenStream
|
|
||||||
{
|
|
||||||
expand(tokens)
|
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
|
||||||
.into()
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
use syn::Error;
|
|
||||||
|
|
||||||
pub trait CollectToResult
|
|
||||||
{
|
|
||||||
type Item;
|
|
||||||
|
|
||||||
fn collect_to_result(self) -> Result<Vec<Self::Item>, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Item, I> CollectToResult for I
|
|
||||||
where
|
|
||||||
I : Iterator<Item = Result<Item, Error>>
|
|
||||||
{
|
|
||||||
type Item = Item;
|
|
||||||
|
|
||||||
fn collect_to_result(self) -> Result<Vec<Item>, Error>
|
|
||||||
{
|
|
||||||
self.fold(<Result<Vec<Item>, Error>>::Ok(Vec::new()), |res, code| {
|
|
||||||
match (code, res) {
|
|
||||||
(Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) },
|
|
||||||
(Ok(_), Err(errors)) => Err(errors),
|
|
||||||
(Err(err), Ok(_)) => Err(err),
|
|
||||||
(Err(err), Err(mut errors)) => { errors.combine(err); Err(errors) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
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
|
|
@ -1,25 +1,27 @@
|
||||||
use crate::HeaderName;
|
use crate::{AuthError, Forbidden};
|
||||||
|
|
||||||
use cookie::CookieJar;
|
use cookie::CookieJar;
|
||||||
use futures::{future, future::Future};
|
use futures_util::{
|
||||||
|
future,
|
||||||
|
future::{FutureExt, TryFutureExt}
|
||||||
|
};
|
||||||
use gotham::{
|
use gotham::{
|
||||||
|
anyhow,
|
||||||
handler::HandlerFuture,
|
handler::HandlerFuture,
|
||||||
middleware::{Middleware, NewMiddleware},
|
hyper::header::{HeaderMap, HeaderName, AUTHORIZATION},
|
||||||
|
middleware::{cookie::CookieParser, Middleware, NewMiddleware},
|
||||||
state::{FromState, State}
|
state::{FromState, State}
|
||||||
};
|
};
|
||||||
use hyper::header::{AUTHORIZATION, HeaderMap};
|
use jsonwebtoken::{errors::ErrorKind, DecodingKey};
|
||||||
use jsonwebtoken::errors::ErrorKind;
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::{
|
use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin};
|
||||||
marker::PhantomData,
|
|
||||||
panic::RefUnwindSafe
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[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.
|
||||||
|
@ -34,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,
|
||||||
|
@ -48,10 +49,20 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for AuthStatus<T> where T: Copy + Send + 'static {}
|
||||||
|
|
||||||
|
impl<T: Send + 'static> AuthStatus<T> {
|
||||||
|
pub fn ok(self) -> Result<T, AuthError> {
|
||||||
|
match self {
|
||||||
|
Self::Authenticated(data) => Ok(data),
|
||||||
|
_ => Err(Forbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The source of the authentication token in the request.
|
/// The source of the authentication token in the request.
|
||||||
#[derive(Clone, 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.
|
||||||
|
@ -66,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::{export::State, AuthHandler};
|
# use gotham_restful::{AuthHandler, gotham::state::State};
|
||||||
#
|
#
|
||||||
const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc";
|
const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc";
|
||||||
|
|
||||||
|
@ -79,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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +129,7 @@ simply add it to your pipeline and request it inside your handler:
|
||||||
# use serde::{Deserialize, Serialize};
|
# use serde::{Deserialize, Serialize};
|
||||||
#
|
#
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
#[rest_resource(read_all)]
|
#[resource(read_all)]
|
||||||
struct AuthResource;
|
struct AuthResource;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
@ -133,7 +138,7 @@ struct AuthData {
|
||||||
exp: u64
|
exp: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rest_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()
|
||||||
}
|
}
|
||||||
|
@ -151,19 +156,19 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
pub struct AuthMiddleware<Data, Handler>
|
#[derive(Debug)]
|
||||||
{
|
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(),
|
||||||
|
@ -175,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(),
|
||||||
|
@ -191,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,
|
||||||
|
@ -204,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
|
||||||
|
@ -236,7 +236,7 @@ where
|
||||||
|
|
||||||
// get the secret from the handler, possibly decoding claims ourselves
|
// get the secret from the handler, possibly decoding claims ourselves
|
||||||
let secret = self.handler.jwt_secret(state, || {
|
let secret = self.handler.jwt_secret(state, || {
|
||||||
let b64 = token.split(".").nth(1)?;
|
let b64 = token.split('.').nth(1)?;
|
||||||
let raw = base64::decode_config(b64, base64::URL_SAFE_NO_PAD).ok()?;
|
let raw = base64::decode_config(b64, base64::URL_SAFE_NO_PAD).ok()?;
|
||||||
serde_json::from_slice(&raw).ok()?
|
serde_json::from_slice(&raw).ok()?
|
||||||
});
|
});
|
||||||
|
@ -248,7 +248,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
// validate the token
|
// validate the token
|
||||||
let data : Data = match jsonwebtoken::decode(&token, &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,
|
||||||
|
@ -257,18 +257,18 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
// we found a valid token
|
// we found a valid token
|
||||||
return AuthStatus::Authenticated(data);
|
AuthStatus::Authenticated(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) -> Box<HandlerFuture>
|
fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
|
||||||
where
|
where
|
||||||
Chain : FnOnce(State) -> 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());
|
||||||
|
@ -278,32 +278,31 @@ where
|
||||||
state.put(status);
|
state.put(status);
|
||||||
|
|
||||||
// call the rest of the chain
|
// call the rest of the chain
|
||||||
Box::new(chain(state).and_then(|(state, res)| future::ok((state, res))))
|
chain(state).and_then(|(state, res)| future::ok((state, res))).boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
||||||
|
@ -311,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(),
|
||||||
|
@ -334,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();
|
||||||
|
@ -357,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();
|
||||||
|
@ -378,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);
|
||||||
|
@ -398,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();
|
||||||
|
@ -414,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();
|
||||||
|
@ -430,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();
|
||||||
|
@ -446,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| {
|
||||||
|
@ -463,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| {
|
||||||
|
@ -478,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)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
300
src/cors.rs
Normal file
300
src/cors.rs
Normal file
|
@ -0,0 +1,300 @@
|
||||||
|
use gotham::{
|
||||||
|
handler::HandlerFuture,
|
||||||
|
helpers::http::response::create_empty_response,
|
||||||
|
hyper::{
|
||||||
|
header::{
|
||||||
|
HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE,
|
||||||
|
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY
|
||||||
|
},
|
||||||
|
Body, Method, Response, StatusCode
|
||||||
|
},
|
||||||
|
middleware::Middleware,
|
||||||
|
pipeline::chain::PipelineHandleChain,
|
||||||
|
router::{
|
||||||
|
builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher},
|
||||||
|
route::matcher::AccessControlRequestMethodMatcher
|
||||||
|
},
|
||||||
|
state::{FromState, State}
|
||||||
|
};
|
||||||
|
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
|
||||||
|
origin. This, when sent to the browser, will indicate whether or not the request's origin was
|
||||||
|
allowed to make the request.
|
||||||
|
*/
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Origin {
|
||||||
|
/// Do not send any `Access-Control-Allow-Origin` headers.
|
||||||
|
None,
|
||||||
|
/// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials.
|
||||||
|
Star,
|
||||||
|
/// Set the `Access-Control-Allow-Origin` header to a single origin.
|
||||||
|
Single(String),
|
||||||
|
/// Copy the `Origin` header into the `Access-Control-Allow-Origin` header.
|
||||||
|
Copy
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Origin {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Origin {
|
||||||
|
/// Get the header value for the `Access-Control-Allow-Origin` header.
|
||||||
|
fn header_value(&self, state: &State) -> Option<HeaderValue> {
|
||||||
|
match self {
|
||||||
|
Self::None => None,
|
||||||
|
Self::Star => Some("*".parse().unwrap()),
|
||||||
|
Self::Single(origin) => Some(origin.parse().unwrap()),
|
||||||
|
Self::Copy => {
|
||||||
|
let headers = HeaderMap::borrow_from(state);
|
||||||
|
headers.get(ORIGIN).map(Clone::clone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
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]:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||||
|
# use gotham_restful::{*, cors::Origin};
|
||||||
|
# #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_doctest_main))]
|
||||||
|
fn main() {
|
||||||
|
let cors = CorsConfig {
|
||||||
|
origin: Origin::Star,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build());
|
||||||
|
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||||
|
// your routing logic
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This easy approach allows you to have one global cors configuration. If you prefer to have separate
|
||||||
|
configurations for different scopes, you need to register the middleware inside your routing logic:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State};
|
||||||
|
# use gotham_restful::{*, cors::Origin};
|
||||||
|
let pipelines = new_pipeline_set();
|
||||||
|
|
||||||
|
// The first cors configuration
|
||||||
|
let cors_a = CorsConfig {
|
||||||
|
origin: Origin::Star,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (pipelines, chain_a) = pipelines.add(
|
||||||
|
new_pipeline().add(cors_a).build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// The second cors configuration
|
||||||
|
let cors_b = CorsConfig {
|
||||||
|
origin: Origin::Copy,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (pipelines, chain_b) = pipelines.add(
|
||||||
|
new_pipeline().add(cors_b).build()
|
||||||
|
);
|
||||||
|
|
||||||
|
let pipeline_set = finalize_pipeline_set(pipelines);
|
||||||
|
gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| {
|
||||||
|
// routing without any cors config
|
||||||
|
route.with_pipeline_chain((chain_a, ()), |route| {
|
||||||
|
// routing with cors config a
|
||||||
|
});
|
||||||
|
route.with_pipeline_chain((chain_b, ()), |route| {
|
||||||
|
// routing with cors config b
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
#[derive(Clone, Debug, Default, NewMiddleware, StateData)]
|
||||||
|
pub struct CorsConfig {
|
||||||
|
/// The allowed origins.
|
||||||
|
pub origin: Origin,
|
||||||
|
/// The allowed headers.
|
||||||
|
pub headers: Headers,
|
||||||
|
/// The amount of seconds that the preflight request can be cached.
|
||||||
|
pub max_age: u64,
|
||||||
|
/// Whether or not the request may be made with supplying credentials.
|
||||||
|
pub credentials: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Middleware for CorsConfig {
|
||||||
|
fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
|
||||||
|
where
|
||||||
|
Chain: FnOnce(State) -> Pin<Box<HandlerFuture>>
|
||||||
|
{
|
||||||
|
state.put(self);
|
||||||
|
chain(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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].
|
||||||
|
|
||||||
|
If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never
|
||||||
|
have to call this method. However, if you are writing your own handler method, you might want to
|
||||||
|
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
|
||||||
|
*/
|
||||||
|
pub fn handle_cors(state: &State, res: &mut Response<Body>) {
|
||||||
|
let config = CorsConfig::try_borrow_from(state);
|
||||||
|
if let Some(cfg) = config {
|
||||||
|
let headers = res.headers_mut();
|
||||||
|
|
||||||
|
// non-preflight requests require the Access-Control-Allow-Origin header
|
||||||
|
if let Some(header) = cfg.origin.header_value(state) {
|
||||||
|
headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the origin is copied over, we should tell the browser by specifying the Vary header
|
||||||
|
if cfg.origin.varies() {
|
||||||
|
let vary = headers.get(VARY).map(|vary| format!("{},origin", vary.to_str().unwrap()));
|
||||||
|
headers.insert(VARY, vary.as_deref().unwrap_or("origin").parse().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we allow credentials, tell the browser
|
||||||
|
if cfg.credentials {
|
||||||
|
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add CORS routing for your path. This is required for handling preflight requests.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use gotham::{hyper::{Body, Method, Response}, router::builder::*};
|
||||||
|
/// # use gotham_restful::*;
|
||||||
|
/// build_simple_router(|router| {
|
||||||
|
/// // The handler that needs preflight handling
|
||||||
|
/// router.post("/foo").to(|state| {
|
||||||
|
/// let mut res : Response<Body> = unimplemented!();
|
||||||
|
/// handle_cors(&state, &mut res);
|
||||||
|
/// (state, res)
|
||||||
|
/// });
|
||||||
|
/// // Add preflight handling
|
||||||
|
/// router.cors("/foo", Method::POST);
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub trait CorsRoute<C, P>
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
/// Handle a preflight request on `path` for `method`. To configure the behaviour, use
|
||||||
|
/// [CorsConfig].
|
||||||
|
fn cors(&mut self, path: &str, method: Method);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
||||||
|
let config = CorsConfig::try_borrow_from(&state);
|
||||||
|
|
||||||
|
// prepare the response
|
||||||
|
let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
|
||||||
|
let headers = res.headers_mut();
|
||||||
|
let mut vary: Vec<HeaderName> = Vec::new();
|
||||||
|
|
||||||
|
// copy the request method over to the response
|
||||||
|
let method = HeaderMap::borrow_from(&state)
|
||||||
|
.get(ACCESS_CONTROL_REQUEST_METHOD)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method);
|
||||||
|
vary.push(ACCESS_CONTROL_REQUEST_METHOD);
|
||||||
|
|
||||||
|
if let Some(cfg) = config {
|
||||||
|
// if we allow any headers, copy them over
|
||||||
|
if let Some(header) = cfg.headers.header_value(&state) {
|
||||||
|
headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, header);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the headers are copied over, we should tell the browser by specifying the Vary header
|
||||||
|
if cfg.headers.varies() {
|
||||||
|
vary.push(ACCESS_CONTROL_REQUEST_HEADERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the max age for the preflight cache
|
||||||
|
if let Some(age) = config.map(|cfg| cfg.max_age) {
|
||||||
|
headers.insert(ACCESS_CONTROL_MAX_AGE, age.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the browser knows that this request was based on the method
|
||||||
|
headers.insert(VARY, vary.join(",").parse().unwrap());
|
||||||
|
|
||||||
|
handle_cors(&state, &mut res);
|
||||||
|
(state, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D, C, P> CorsRoute<C, P> for D
|
||||||
|
where
|
||||||
|
D: DrawRoutes<C, P>,
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn cors(&mut self, path: &str, method: Method) {
|
||||||
|
let matcher = AccessControlRequestMethodMatcher::new(method);
|
||||||
|
self.options(path).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)
|
||||||
|
}
|
||||||
|
}
|
533
src/lib.rs
Normal file
533
src/lib.rs
Normal file
|
@ -0,0 +1,533 @@
|
||||||
|
#![warn(missing_debug_implementations, rust_2018_idioms)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
// can we have a lint for spaces in doc comments please?
|
||||||
|
#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))]
|
||||||
|
// intra-doc links only fully work when OpenAPI is enabled
|
||||||
|
#![cfg_attr(feature = "openapi", deny(broken_intra_doc_links))]
|
||||||
|
#![cfg_attr(not(feature = "openapi"), allow(broken_intra_doc_links))]
|
||||||
|
/*!
|
||||||
|
This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to
|
||||||
|
create resources with assigned endpoints that aim to be a more convenient way of creating handlers
|
||||||
|
for requests.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
- Automatically parse **JSON** request and produce response bodies
|
||||||
|
- Allow using **raw** request and response bodies
|
||||||
|
- Convenient **macros** to create responses that can be registered with gotham's router
|
||||||
|
- Auto-Generate an **OpenAPI** specification for your API
|
||||||
|
- Manage **CORS** headers so you don't have to
|
||||||
|
- Manage **Authentication** with JWT
|
||||||
|
- Integrate diesel connection pools for easy **database** integration
|
||||||
|
|
||||||
|
# Safety
|
||||||
|
|
||||||
|
This crate is just as safe as you'd expect from anything written in safe Rust - and
|
||||||
|
`#![forbid(unsafe_code)]` ensures that no unsafe was used.
|
||||||
|
|
||||||
|
# Endpoints
|
||||||
|
|
||||||
|
There are a set of pre-defined endpoints that should cover the majority of REST APIs. However,
|
||||||
|
it is also possible to define your own endpoints.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# use gotham::router::builder::*;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
/// Our RESTful resource.
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
/// The return type of the foo read endpoint.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
|
struct Foo {
|
||||||
|
id: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The foo read endpoint.
|
||||||
|
#[read]
|
||||||
|
fn read(id: u64) -> Success<Foo> {
|
||||||
|
Foo { id }.into()
|
||||||
|
}
|
||||||
|
# fn main() {
|
||||||
|
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||||
|
# route.resource::<FooResource>("foo");
|
||||||
|
# }));
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Endpoints
|
||||||
|
|
||||||
|
Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that
|
||||||
|
of the pre-defined endpoints, but you need to give it more context:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# #[macro_use] extern crate gotham_derive;
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# use gotham::router::builder::*;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
use gotham_restful::gotham::hyper::Method;
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(custom_endpoint)]
|
||||||
|
struct CustomResource;
|
||||||
|
|
||||||
|
/// This type is used to parse path parameters.
|
||||||
|
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
|
struct CustomPath {
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)]
|
||||||
|
fn custom_endpoint(path: CustomPath) -> Success<String> {
|
||||||
|
path.name.into()
|
||||||
|
}
|
||||||
|
# fn main() {
|
||||||
|
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||||
|
# route.resource::<CustomResource>("custom");
|
||||||
|
# }));
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
|
||||||
|
Some endpoints require arguments. Those should be
|
||||||
|
* **id** Should be a deserializable json-primitive like [`i64`] or [`String`].
|
||||||
|
* **body** Should be any deserializable object, or any type implementing [`RequestBody`].
|
||||||
|
* **query** Should be any deserializable object whose variables are json-primitives. It will
|
||||||
|
however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The
|
||||||
|
type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor).
|
||||||
|
|
||||||
|
Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async
|
||||||
|
handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds
|
||||||
|
improve.
|
||||||
|
|
||||||
|
# Uploads and Downloads
|
||||||
|
|
||||||
|
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,no_run
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# use gotham::router::builder::*;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
#[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]
|
||||||
|
fn create(body : RawImage) -> Raw<Vec<u8>> {
|
||||||
|
Raw::new(body.content, body.content_type)
|
||||||
|
}
|
||||||
|
# fn main() {
|
||||||
|
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||||
|
# route.resource::<ImageResource>("image");
|
||||||
|
# }));
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
# Custom HTTP Headers
|
||||||
|
|
||||||
|
You can read request headers from the state as you would in any other gotham handler, and specify
|
||||||
|
custom response headers using [Response::header].
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# use gotham::hyper::header::{ACCEPT, HeaderMap, VARY};
|
||||||
|
# use gotham::{router::builder::*, state::State};
|
||||||
|
# use gotham_restful::*;
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
async fn read_all(state: &mut State) -> NoContent {
|
||||||
|
let headers: &HeaderMap = state.borrow();
|
||||||
|
let accept = &headers[ACCEPT];
|
||||||
|
# drop(accept);
|
||||||
|
|
||||||
|
let mut res = NoContent::default();
|
||||||
|
res.header(VARY, "accept".parse().unwrap());
|
||||||
|
res
|
||||||
|
}
|
||||||
|
# fn main() {
|
||||||
|
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||||
|
# route.resource::<FooResource>("foo");
|
||||||
|
# }));
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
To make life easier for common use-cases, this create offers a few features that might be helpful
|
||||||
|
when you implement your web server. The complete feature list is
|
||||||
|
- [`auth`](#authentication-feature) Advanced JWT middleware
|
||||||
|
- `chrono` openapi support for chrono types
|
||||||
|
- `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
|
||||||
|
|
||||||
|
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 endpoint macros, it supports to lookup the required JWT secret
|
||||||
|
with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use.
|
||||||
|
None of this is currently supported by gotham's own JWT middleware.
|
||||||
|
|
||||||
|
A simple example that uses only a single secret looks like this:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# #[cfg(feature = "auth")]
|
||||||
|
# mod auth_feature_enabled {
|
||||||
|
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read)]
|
||||||
|
struct SecretResource;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
|
struct Secret {
|
||||||
|
id: u64,
|
||||||
|
intended_for: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct AuthData {
|
||||||
|
sub: String,
|
||||||
|
exp: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[read]
|
||||||
|
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, looks like this:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# #[cfg(feature = "cors")]
|
||||||
|
# mod cors_feature_enabled {
|
||||||
|
# use gotham::{hyper::header::*, router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||||
|
# use gotham_restful::{*, cors::*};
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
fn read_all() {
|
||||||
|
// your handler
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let cors = CorsConfig {
|
||||||
|
origin: Origin::Copy,
|
||||||
|
headers: Headers::List(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 looks like this:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# #[macro_use] extern crate diesel;
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# #[cfg(feature = "database")]
|
||||||
|
# mod database_feature_enabled {
|
||||||
|
# use diesel::{table, PgConnection, QueryResult, RunQueryDsl};
|
||||||
|
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
||||||
|
# use gotham_middleware_diesel::DieselMiddleware;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::{Deserialize, Serialize};
|
||||||
|
# use std::env;
|
||||||
|
# table! {
|
||||||
|
# foo (id) {
|
||||||
|
# id -> Int8,
|
||||||
|
# value -> Text,
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[derive(Queryable, Serialize)]
|
||||||
|
# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))]
|
||||||
|
struct Foo {
|
||||||
|
id: i64,
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
fn read_all(conn: &PgConnection) -> QueryResult<Vec<Foo>> {
|
||||||
|
foo::table.load(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repo = gotham_middleware_diesel::Repo<PgConnection>;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let repo = Repo::new(&env::var("DATABASE_URL").unwrap());
|
||||||
|
let diesel = DieselMiddleware::new(repo);
|
||||||
|
|
||||||
|
let (chain, pipelines) = single_pipeline(new_pipeline().add(diesel).build());
|
||||||
|
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
||||||
|
route.resource::<FooResource>("foo");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenAPI Feature
|
||||||
|
|
||||||
|
The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section
|
||||||
|
carefully both as a binary as well as a library author to avoid unwanted suprises.
|
||||||
|
|
||||||
|
In order to automatically create an openapi specification, gotham-restful needs knowledge over
|
||||||
|
all routes and the types returned. `serde` does a great job at serialization but doesn't give
|
||||||
|
enough type information, so all types used in the router need to implement
|
||||||
|
`OpenapiType`[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
|
||||||
|
|
||||||
|
This readme and the crate documentation contain some of example. In addition to that, there is
|
||||||
|
a collection of code in the [example] directory that might help you. Any help writing more
|
||||||
|
examples is highly appreciated.
|
||||||
|
|
||||||
|
|
||||||
|
[diesel]: https://diesel.rs/
|
||||||
|
[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example
|
||||||
|
[gotham]: https://gotham.rs/
|
||||||
|
[serde_json]: https://github.com/serde-rs/json#serde-json----
|
||||||
|
[`State`]: gotham::state::State
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[cfg(all(feature = "openapi", feature = "without-openapi"))]
|
||||||
|
compile_error!("The 'openapi' and 'without-openapi' features cannot be combined");
|
||||||
|
|
||||||
|
#[cfg(all(not(feature = "openapi"), not(feature = "without-openapi")))]
|
||||||
|
compile_error!("Either the 'openapi' or 'without-openapi' feature needs to be enabled");
|
||||||
|
|
||||||
|
// weird proc macro issue
|
||||||
|
extern crate self as gotham_restful;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful_derive;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde;
|
||||||
|
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use gotham;
|
||||||
|
#[doc(no_inline)]
|
||||||
|
pub use mime::Mime;
|
||||||
|
|
||||||
|
pub use gotham_restful_derive::*;
|
||||||
|
|
||||||
|
/// Not public API
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod private {
|
||||||
|
pub use crate::routing::PathExtractor as IdPlaceholder;
|
||||||
|
|
||||||
|
pub use futures_util::future::{BoxFuture, FutureExt};
|
||||||
|
|
||||||
|
pub use serde_json;
|
||||||
|
|
||||||
|
#[cfg(feature = "database")]
|
||||||
|
pub use gotham_middleware_diesel::Repo;
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use indexmap::IndexMap;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use openapi_type::{OpenapiSchema, OpenapiType};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use openapiv3 as openapi;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler};
|
||||||
|
|
||||||
|
#[cfg(feature = "cors")]
|
||||||
|
pub mod cors;
|
||||||
|
#[cfg(feature = "cors")]
|
||||||
|
pub use cors::{handle_cors, CorsConfig, CorsRoute};
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
mod openapi;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use openapi::{builder::OpenapiInfo, router::GetOpenapi};
|
||||||
|
|
||||||
|
mod endpoint;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use endpoint::EndpointWithSchema;
|
||||||
|
pub use endpoint::{Endpoint, NoopExtractor};
|
||||||
|
|
||||||
|
mod response;
|
||||||
|
pub use response::{
|
||||||
|
AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent,
|
||||||
|
Raw, Redirect, Response, Success
|
||||||
|
};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use response::{IntoResponseWithSchema, ResponseSchema};
|
||||||
|
|
||||||
|
mod routing;
|
||||||
|
pub use routing::{DrawResourceRoutes, DrawResources};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi};
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
|
/// This trait must be implemented for every resource. It allows you to register the different
|
||||||
|
/// endpoints that can be handled by this resource to be registered with the underlying router.
|
||||||
|
///
|
||||||
|
/// It is not recommended to implement this yourself, just use `#[derive(Resource)]`.
|
||||||
|
#[_private_openapi_trait(ResourceWithSchema)]
|
||||||
|
pub trait Resource {
|
||||||
|
/// Register all methods handled by this resource with the underlying router.
|
||||||
|
#[openapi_bound("D: crate::DrawResourceRoutesWithSchema")]
|
||||||
|
#[non_openapi_bound("D: crate::DrawResourceRoutes")]
|
||||||
|
fn setup<D>(route: D);
|
||||||
|
}
|
157
src/openapi/builder.rs
Normal file
157
src/openapi/builder.rs
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use openapi_type::OpenapiSchema;
|
||||||
|
use openapiv3::{
|
||||||
|
Components, OpenAPI, PathItem, ReferenceOr,
|
||||||
|
ReferenceOr::{Item, Reference},
|
||||||
|
Schema, Server
|
||||||
|
};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OpenapiInfo {
|
||||||
|
pub title: String,
|
||||||
|
pub version: String,
|
||||||
|
pub urls: Vec<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct OpenapiBuilder {
|
||||||
|
pub openapi: Arc<RwLock<OpenAPI>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenapiBuilder {
|
||||||
|
pub fn new(info: OpenapiInfo) -> Self {
|
||||||
|
Self {
|
||||||
|
openapi: Arc::new(RwLock::new(OpenAPI {
|
||||||
|
openapi: "3.0.2".to_string(),
|
||||||
|
info: openapiv3::Info {
|
||||||
|
title: info.title,
|
||||||
|
version: info.version,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
servers: info
|
||||||
|
.urls
|
||||||
|
.into_iter()
|
||||||
|
.map(|url| Server {
|
||||||
|
url,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
pub fn remove_path(&mut self, path: &str) -> PathItem {
|
||||||
|
let mut openapi = self.openapi.write().unwrap();
|
||||||
|
match openapi.paths.swap_remove(path) {
|
||||||
|
Some(Item(item)) => item,
|
||||||
|
_ => PathItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_path<Path: ToString>(&mut self, path: Path, item: PathItem) {
|
||||||
|
let mut openapi = self.openapi.write().unwrap();
|
||||||
|
openapi.paths.insert(path.to_string(), Item(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_schema_impl(&mut self, name: String, mut schema: OpenapiSchema) {
|
||||||
|
self.add_schema_dependencies(&mut schema.dependencies);
|
||||||
|
|
||||||
|
let mut openapi = self.openapi.write().unwrap();
|
||||||
|
match &mut openapi.components {
|
||||||
|
Some(comp) => {
|
||||||
|
comp.schemas.insert(name, Item(schema.into_schema()));
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let mut comp = Components::default();
|
||||||
|
comp.schemas.insert(name, Item(schema.into_schema()));
|
||||||
|
openapi.components = Some(comp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_schema_dependencies(&mut self, dependencies: &mut IndexMap<String, OpenapiSchema>) {
|
||||||
|
let keys: Vec<String> = dependencies.keys().map(|k| k.to_string()).collect();
|
||||||
|
for dep in keys {
|
||||||
|
let dep_schema = dependencies.swap_remove(&dep);
|
||||||
|
if let Some(dep_schema) = dep_schema {
|
||||||
|
self.add_schema_impl(dep, dep_schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_schema(&mut self, mut schema: OpenapiSchema) -> ReferenceOr<Schema> {
|
||||||
|
match schema.name.clone() {
|
||||||
|
Some(name) => {
|
||||||
|
let reference = Reference {
|
||||||
|
reference: format!("#/components/schemas/{}", name)
|
||||||
|
};
|
||||||
|
self.add_schema_impl(name, schema);
|
||||||
|
reference
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
self.add_schema_dependencies(&mut schema.dependencies);
|
||||||
|
Item(schema.into_schema())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Message {
|
||||||
|
msg: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(OpenapiType)]
|
||||||
|
struct Messages {
|
||||||
|
msgs: Vec<Message>
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info() -> OpenapiInfo {
|
||||||
|
OpenapiInfo {
|
||||||
|
title: "TEST CASE".to_owned(),
|
||||||
|
version: "1.2.3".to_owned(),
|
||||||
|
urls: vec!["http://localhost:1234".to_owned(), "https://example.org".to_owned()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openapi(builder: OpenapiBuilder) -> OpenAPI {
|
||||||
|
Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_builder() {
|
||||||
|
let info = info();
|
||||||
|
let builder = OpenapiBuilder::new(info.clone());
|
||||||
|
let openapi = openapi(builder);
|
||||||
|
|
||||||
|
assert_eq!(info.title, openapi.info.title);
|
||||||
|
assert_eq!(info.version, openapi.info.version);
|
||||||
|
assert_eq!(info.urls.len(), openapi.servers.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_schema() {
|
||||||
|
let mut builder = OpenapiBuilder::new(info());
|
||||||
|
builder.add_schema(<Option<Messages>>::schema());
|
||||||
|
let openapi = openapi(builder);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
261
src/openapi/handler.rs
Normal file
261
src/openapi/handler.rs
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
#![cfg_attr(not(feature = "auth"), allow(unused_imports))]
|
||||||
|
use super::SECURITY_NAME;
|
||||||
|
use futures_util::{future, future::FutureExt};
|
||||||
|
use gotham::{
|
||||||
|
anyhow,
|
||||||
|
handler::{Handler, HandlerFuture, NewHandler},
|
||||||
|
helpers::http::response::{create_empty_response, create_response},
|
||||||
|
hyper::{
|
||||||
|
header::{
|
||||||
|
HeaderMap, HeaderValue, CACHE_CONTROL, CONTENT_SECURITY_POLICY, ETAG, IF_NONE_MATCH, REFERRER_POLICY,
|
||||||
|
X_CONTENT_TYPE_OPTIONS
|
||||||
|
},
|
||||||
|
Body, Response, StatusCode, Uri
|
||||||
|
},
|
||||||
|
state::State
|
||||||
|
};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use mime::{APPLICATION_JSON, TEXT_HTML, TEXT_PLAIN};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{
|
||||||
|
pin::Pin,
|
||||||
|
sync::{Arc, RwLock}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
fn get_security(state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
|
||||||
|
use crate::AuthSource;
|
||||||
|
use gotham::state::FromState;
|
||||||
|
|
||||||
|
let source = match AuthSource::try_borrow_from(state) {
|
||||||
|
Some(source) => source,
|
||||||
|
None => return Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let security_scheme = match source {
|
||||||
|
AuthSource::Cookie(name) => SecurityScheme::APIKey {
|
||||||
|
location: APIKeyLocation::Cookie,
|
||||||
|
name: name.to_string()
|
||||||
|
},
|
||||||
|
AuthSource::Header(name) => SecurityScheme::APIKey {
|
||||||
|
location: APIKeyLocation::Header,
|
||||||
|
name: name.to_string()
|
||||||
|
},
|
||||||
|
AuthSource::AuthorizationHeader => SecurityScheme::HTTP {
|
||||||
|
scheme: "bearer".to_owned(),
|
||||||
|
bearer_format: Some("JWT".to_owned())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut security_schemes: IndexMap<String, ReferenceOr<SecurityScheme>> = Default::default();
|
||||||
|
security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme));
|
||||||
|
|
||||||
|
security_schemes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
fn get_security(_state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_openapi_response(state: &mut State, openapi: &Arc<RwLock<OpenAPI>>) -> Response<Body> {
|
||||||
|
let openapi = match openapi.read() {
|
||||||
|
Ok(openapi) => openapi,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
|
||||||
|
return create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut openapi = openapi.clone();
|
||||||
|
let security_schemes = get_security(state);
|
||||||
|
let mut components = openapi.components.unwrap_or_default();
|
||||||
|
components.security_schemes = security_schemes;
|
||||||
|
openapi.components = Some(components);
|
||||||
|
|
||||||
|
match serde_json::to_string(&openapi) {
|
||||||
|
Ok(body) => {
|
||||||
|
let 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()
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
{
|
||||||
|
let headers: &HeaderMap = state.borrow();
|
||||||
|
if headers
|
||||||
|
.get(IF_NONE_MATCH)
|
||||||
|
.map_or(false, |etag| etag.as_bytes() == SWAGGER_UI_HTML_ETAG.as_bytes())
|
||||||
|
{
|
||||||
|
let res = create_empty_response(&state, StatusCode::NOT_MODIFIED);
|
||||||
|
return future::ok((state, res)).boxed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = create_response(&state, StatusCode::OK, TEXT_HTML, SWAGGER_UI_HTML.as_bytes());
|
||||||
|
let headers = res.headers_mut();
|
||||||
|
headers.insert(CACHE_CONTROL, HeaderValue::from_static("public,max-age=2592000"));
|
||||||
|
headers.insert(CONTENT_SECURITY_POLICY, format!("default-src 'none'; script-src 'unsafe-inline' 'sha256-{}' 'strict-dynamic'; style-src 'unsafe-inline' https://cdnjs.cloudflare.com; connect-src 'self'; img-src data:;", SWAGGER_UI_SCRIPT_HASH.as_str()).parse().unwrap());
|
||||||
|
headers.insert(ETAG, SWAGGER_UI_HTML_ETAG.parse().unwrap());
|
||||||
|
headers.insert(REFERRER_POLICY, HeaderValue::from_static("strict-origin-when-cross-origin"));
|
||||||
|
headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
|
||||||
|
future::ok((state, res)).boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspired by https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html
|
||||||
|
const SWAGGER_UI_HTML: Lazy<&'static String> = Lazy::new(|| {
|
||||||
|
let template = indoc::indoc! {
|
||||||
|
r#"
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui.css" integrity="sha512-sphGjcjFvN5sAW6S28Ge+F9SCzRuc9IVkLinDHu7B1wOUHHFAY5sSQ2Axff+qs7/0GTm0Ifg4i0lQKgM8vdV2w==" crossorigin="anonymous"/>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script>{{script}}</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#
|
||||||
|
};
|
||||||
|
Box::leak(Box::new(template.replace("{{script}}", SWAGGER_UI_SCRIPT)))
|
||||||
|
});
|
||||||
|
static SWAGGER_UI_HTML_ETAG: Lazy<String> = Lazy::new(|| {
|
||||||
|
let mut hash = Sha256::new();
|
||||||
|
hash.update(SWAGGER_UI_HTML.as_bytes());
|
||||||
|
let hash = hash.finalize();
|
||||||
|
let hash = base64::encode(hash);
|
||||||
|
format!("\"{}\"", hash)
|
||||||
|
});
|
||||||
|
const SWAGGER_UI_SCRIPT: &str = r#"
|
||||||
|
let s0rdy = false;
|
||||||
|
let s1rdy = false;
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
const cb = function() {
|
||||||
|
if (!s0rdy || !s1rdy)
|
||||||
|
return;
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: window.location.origin + window.location.pathname + '?spec',
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: 'StandaloneLayout'
|
||||||
|
});
|
||||||
|
window.ui = ui;
|
||||||
|
};
|
||||||
|
|
||||||
|
const s0 = document.createElement('script');
|
||||||
|
s0.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-bundle.js');
|
||||||
|
s0.setAttribute('integrity', 'sha512-EfK//grBlevo9MrtEDyNvf4SkBA0avHZoVLEuSR2Yl6ymnjcIwClgZ7FXdr/42yGqnhEHxb+Sv/bJeUp26YPRw==');
|
||||||
|
s0.setAttribute('crossorigin', 'anonymous');
|
||||||
|
s0.onload = function() {
|
||||||
|
s0rdy = true;
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
document.head.appendChild(s0);
|
||||||
|
|
||||||
|
const s1 = document.createElement('script');
|
||||||
|
s1.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-standalone-preset.js');
|
||||||
|
s1.setAttribute('integrity', 'sha512-Hbx9NyAhG+P6YNBU9mp6hc6ntRjGYHqo/qae4OHhzPA69xbNmF8n2aJxpzUwdbXbYICO6eor4IhgSfiSQm9OYg==');
|
||||||
|
s1.setAttribute('crossorigin', 'anonymous');
|
||||||
|
s1.onload = function() {
|
||||||
|
s1rdy = true;
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
document.head.appendChild(s1);
|
||||||
|
};
|
||||||
|
"#;
|
||||||
|
static SWAGGER_UI_SCRIPT_HASH: Lazy<String> = Lazy::new(|| {
|
||||||
|
let mut hash = Sha256::new();
|
||||||
|
hash.update(SWAGGER_UI_SCRIPT);
|
||||||
|
let hash = hash.finalize();
|
||||||
|
base64::encode(hash)
|
||||||
|
});
|
6
src/openapi/mod.rs
Normal file
6
src/openapi/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const SECURITY_NAME: &str = "authToken";
|
||||||
|
|
||||||
|
pub mod builder;
|
||||||
|
pub mod handler;
|
||||||
|
pub mod operation;
|
||||||
|
pub mod router;
|
207
src/openapi/operation.rs
Normal file
207
src/openapi/operation.rs
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
use super::SECURITY_NAME;
|
||||||
|
use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use mime::Mime;
|
||||||
|
use openapi_type::OpenapiSchema;
|
||||||
|
use openapiv3::{
|
||||||
|
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item,
|
||||||
|
RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct OperationParams {
|
||||||
|
path_params: Option<OpenapiSchema>,
|
||||||
|
query_params: Option<OpenapiSchema>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationParams {
|
||||||
|
fn add_path_params(path_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||||
|
let path_params = match path_params {
|
||||||
|
Some(pp) => pp.schema,
|
||||||
|
None => return
|
||||||
|
};
|
||||||
|
let path_params = match path_params {
|
||||||
|
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||||
|
_ => panic!("Path Parameters needs to be a plain struct")
|
||||||
|
};
|
||||||
|
for (name, schema) in path_params.properties {
|
||||||
|
let required = path_params.required.contains(&name);
|
||||||
|
params.push(Item(Parameter::Path {
|
||||||
|
parameter_data: ParameterData {
|
||||||
|
name,
|
||||||
|
description: None,
|
||||||
|
required,
|
||||||
|
deprecated: None,
|
||||||
|
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||||
|
example: None,
|
||||||
|
examples: IndexMap::new()
|
||||||
|
},
|
||||||
|
style: Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_query_params(query_params: Option<OpenapiSchema>, params: &mut Vec<ReferenceOr<Parameter>>) {
|
||||||
|
let query_params = match query_params {
|
||||||
|
Some(qp) => qp.schema,
|
||||||
|
None => return
|
||||||
|
};
|
||||||
|
let query_params = match query_params {
|
||||||
|
SchemaKind::Type(Type::Object(ty)) => ty,
|
||||||
|
_ => panic!("Query Parameters needs to be a plain struct")
|
||||||
|
};
|
||||||
|
for (name, schema) in query_params.properties {
|
||||||
|
let required = query_params.required.contains(&name);
|
||||||
|
params.push(Item(Parameter::Query {
|
||||||
|
parameter_data: ParameterData {
|
||||||
|
name,
|
||||||
|
description: None,
|
||||||
|
required,
|
||||||
|
deprecated: None,
|
||||||
|
format: ParameterSchemaOrContent::Schema(schema.unbox()),
|
||||||
|
example: None,
|
||||||
|
examples: IndexMap::new()
|
||||||
|
},
|
||||||
|
allow_reserved: false,
|
||||||
|
style: Default::default(),
|
||||||
|
allow_empty_value: None
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_params(self) -> Vec<ReferenceOr<Parameter>> {
|
||||||
|
let mut params: Vec<ReferenceOr<Parameter>> = Vec::new();
|
||||||
|
Self::add_path_params(self.path_params, &mut params);
|
||||||
|
Self::add_query_params(self.query_params, &mut params);
|
||||||
|
params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OperationDescription {
|
||||||
|
operation_id: Option<String>,
|
||||||
|
default_status: gotham::hyper::StatusCode,
|
||||||
|
accepted_types: Option<Vec<Mime>>,
|
||||||
|
schema: ReferenceOr<Schema>,
|
||||||
|
params: OperationParams,
|
||||||
|
body_schema: Option<ReferenceOr<Schema>>,
|
||||||
|
supported_types: Option<Vec<Mime>>,
|
||||||
|
requires_auth: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperationDescription {
|
||||||
|
pub fn new<E: EndpointWithSchema>(schema: ReferenceOr<Schema>) -> Self {
|
||||||
|
Self {
|
||||||
|
operation_id: E::operation_id(),
|
||||||
|
default_status: E::Output::default_status(),
|
||||||
|
accepted_types: E::Output::accepted_types(),
|
||||||
|
schema,
|
||||||
|
params: Default::default(),
|
||||||
|
body_schema: None,
|
||||||
|
supported_types: None,
|
||||||
|
requires_auth: E::wants_auth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_path_params(&mut self, params: OpenapiSchema) {
|
||||||
|
self.params.path_params = Some(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_query_params(&mut self, params: OpenapiSchema) {
|
||||||
|
self.params.query_params = Some(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_body<Body: RequestBody>(&mut self, schema: ReferenceOr<Schema>) {
|
||||||
|
self.body_schema = Some(schema);
|
||||||
|
self.supported_types = Body::supported_types();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
content.insert(ty.to_string(), MediaType {
|
||||||
|
schema: Some(schema.clone()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_operation(self) -> Operation {
|
||||||
|
// 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) = (
|
||||||
|
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 mut responses: IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
|
||||||
|
responses.insert(
|
||||||
|
StatusCode::Code(default_status.as_u16()),
|
||||||
|
Item(Response {
|
||||||
|
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 {
|
||||||
|
description: None,
|
||||||
|
content: Self::schema_to_content(supported_types.or_all_types(), schema),
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut security = Vec::new();
|
||||||
|
if requires_auth {
|
||||||
|
let mut sec = IndexMap::new();
|
||||||
|
sec.insert(SECURITY_NAME.to_owned(), Vec::new());
|
||||||
|
security.push(sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation {
|
||||||
|
tags: Vec::new(),
|
||||||
|
operation_id,
|
||||||
|
parameters: params.into_params(),
|
||||||
|
request_body,
|
||||||
|
responses: Responses {
|
||||||
|
default: None,
|
||||||
|
responses
|
||||||
|
},
|
||||||
|
deprecated: false,
|
||||||
|
security,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::{NoContent, Raw, ResponseSchema};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_content_schema_to_content() {
|
||||||
|
let types = NoContent::accepted_types();
|
||||||
|
let schema = <NoContent as ResponseSchema>::schema();
|
||||||
|
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||||
|
assert!(content.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raw_schema_to_content() {
|
||||||
|
let types = Raw::<&str>::accepted_types();
|
||||||
|
let schema = <Raw<&str> as ResponseSchema>::schema();
|
||||||
|
let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema()));
|
||||||
|
assert_eq!(content.len(), 1);
|
||||||
|
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
|
||||||
|
assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#);
|
||||||
|
}
|
||||||
|
}
|
134
src/openapi/router.rs
Normal file
134
src/openapi/router.rs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
use super::{
|
||||||
|
builder::OpenapiBuilder,
|
||||||
|
handler::{OpenapiHandler, SwaggerUiHandler},
|
||||||
|
operation::OperationDescription
|
||||||
|
};
|
||||||
|
use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema};
|
||||||
|
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
use regex::{Captures, Regex};
|
||||||
|
use std::panic::RefUnwindSafe;
|
||||||
|
|
||||||
|
/// This trait adds the `get_openapi` and `swagger_ui` method to an OpenAPI-aware router.
|
||||||
|
pub trait GetOpenapi {
|
||||||
|
fn get_openapi(&mut self, path: &str);
|
||||||
|
fn swagger_ui(&mut self, path: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OpenapiRouter<'a, D> {
|
||||||
|
pub(crate) router: &'a mut D,
|
||||||
|
pub(crate) scope: Option<&'a str>,
|
||||||
|
pub(crate) openapi_builder: &'a mut OpenapiBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! implOpenapiRouter {
|
||||||
|
($implType:ident) => {
|
||||||
|
impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
pub fn scope<F>(&mut self, path: &str, callback: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
|
||||||
|
{
|
||||||
|
let mut openapi_builder = self.openapi_builder.clone();
|
||||||
|
let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/"));
|
||||||
|
self.router.scope(path, |router| {
|
||||||
|
let mut router = OpenapiRouter {
|
||||||
|
router,
|
||||||
|
scope: Some(new_scope.as_ref().map(String::as_ref).unwrap_or(path)),
|
||||||
|
openapi_builder: &mut openapi_builder
|
||||||
|
};
|
||||||
|
callback(&mut router);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn get_openapi(&mut self, path: &str) {
|
||||||
|
self.router
|
||||||
|
.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> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn resource<R: ResourceWithSchema>(&mut self, path: &str) {
|
||||||
|
R::setup((self, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn endpoint<E: EndpointWithSchema + 'static>(&mut self) {
|
||||||
|
let schema = (self.0).openapi_builder.add_schema(E::Output::schema());
|
||||||
|
let mut descr = OperationDescription::new::<E>(schema);
|
||||||
|
if E::has_placeholders() {
|
||||||
|
descr.set_path_params(E::Placeholders::schema());
|
||||||
|
}
|
||||||
|
if E::needs_params() {
|
||||||
|
descr.set_query_params(E::Params::schema());
|
||||||
|
}
|
||||||
|
if E::needs_body() {
|
||||||
|
let body_schema = (self.0).openapi_builder.add_schema(E::Body::schema());
|
||||||
|
descr.set_body::<E::Body>(body_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
match E::http_method() {
|
||||||
|
Method::GET => item.get = Some(op),
|
||||||
|
Method::PUT => item.put = Some(op),
|
||||||
|
Method::POST => item.post = Some(op),
|
||||||
|
Method::DELETE => item.delete = Some(op),
|
||||||
|
Method::OPTIONS => item.options = Some(op),
|
||||||
|
Method::HEAD => item.head = Some(op),
|
||||||
|
Method::PATCH => item.patch = Some(op),
|
||||||
|
Method::TRACE => item.trace = Some(op),
|
||||||
|
method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method)
|
||||||
|
};
|
||||||
|
(self.0).openapi_builder.add_path(path, item);
|
||||||
|
|
||||||
|
(&mut *(self.0).router, self.1).endpoint::<E>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
implOpenapiRouter!(RouterBuilder);
|
||||||
|
implOpenapiRouter!(ScopeBuilder);
|
122
src/response/auth_result.rs
Normal file
122
src/response/auth_result.rs
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
use gotham_restful_derive::ResourceError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
This is an error type that always yields a _403 Forbidden_ response. This type is best used in
|
||||||
|
combination with [AuthSuccess] or [AuthResult].
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone, Copy, ResourceError)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[status(FORBIDDEN)]
|
||||||
|
#[display("Forbidden")]
|
||||||
|
Forbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||||
|
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||||
|
response will be issued.
|
||||||
|
|
||||||
|
Use can look something like this (assuming the `auth` feature is enabled):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# #[cfg(feature = "auth")]
|
||||||
|
# mod auth_feature_enabled {
|
||||||
|
# use gotham::state::State;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::Deserialize;
|
||||||
|
#
|
||||||
|
# #[derive(Resource)]
|
||||||
|
# #[resource(read_all)]
|
||||||
|
# struct MyResource;
|
||||||
|
#
|
||||||
|
# #[derive(Clone, Deserialize)]
|
||||||
|
# struct MyAuthData { exp : u64 }
|
||||||
|
#
|
||||||
|
#[read_all]
|
||||||
|
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthSuccess<NoContent> {
|
||||||
|
let auth_data = match auth {
|
||||||
|
AuthStatus::Authenticated(data) => data,
|
||||||
|
_ => return Err(Forbidden)
|
||||||
|
};
|
||||||
|
// do something
|
||||||
|
Ok(NoContent::default())
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
pub type AuthSuccess<T> = Result<T, AuthError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
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].
|
||||||
|
*/
|
||||||
|
#[derive(Debug, ResourceError)]
|
||||||
|
pub enum AuthErrorOrOther<E> {
|
||||||
|
#[status(FORBIDDEN)]
|
||||||
|
#[display("Forbidden")]
|
||||||
|
Forbidden,
|
||||||
|
#[status(INTERNAL_SERVER_ERROR)]
|
||||||
|
#[display("{0}")]
|
||||||
|
Other(E)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<AuthError> for AuthErrorOrOther<E> {
|
||||||
|
fn from(err: AuthError) -> Self {
|
||||||
|
match err {
|
||||||
|
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>
|
||||||
|
where
|
||||||
|
// TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20
|
||||||
|
F: private::Sealed + Into<E>
|
||||||
|
{
|
||||||
|
fn from(err: F) -> Self {
|
||||||
|
Self::Other(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse)
|
||||||
|
that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_
|
||||||
|
response will be issued.
|
||||||
|
|
||||||
|
Use can look something like this (assuming the `auth` feature is enabled):
|
||||||
|
|
||||||
|
```
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# #[cfg(feature = "auth")]
|
||||||
|
# mod auth_feature_enabled {
|
||||||
|
# use gotham::state::State;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
# use serde::Deserialize;
|
||||||
|
# use std::io;
|
||||||
|
#
|
||||||
|
# #[derive(Resource)]
|
||||||
|
# #[resource(read_all)]
|
||||||
|
# struct MyResource;
|
||||||
|
#
|
||||||
|
# #[derive(Clone, Deserialize)]
|
||||||
|
# struct MyAuthData { exp : u64 }
|
||||||
|
#
|
||||||
|
#[read_all]
|
||||||
|
fn read_all(auth : AuthStatus<MyAuthData>) -> AuthResult<NoContent, io::Error> {
|
||||||
|
let auth_data = match auth {
|
||||||
|
AuthStatus::Authenticated(data) => data,
|
||||||
|
_ => Err(Forbidden)?
|
||||||
|
};
|
||||||
|
// do something
|
||||||
|
Ok(NoContent::default().into())
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
*/
|
||||||
|
pub type AuthResult<T, E> = Result<T, AuthErrorOrOther<E>>;
|
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("*"));
|
||||||
|
}
|
||||||
|
}
|
166
src/response/raw.rs
Normal file
166
src/response/raw.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
use super::{handle_error, IntoResponse, IntoResponseError};
|
||||||
|
use crate::{FromBody, RequestBody, ResourceType, Response};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::{IntoResponseWithSchema, ResponseSchema};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::{OpenapiSchema, OpenapiType};
|
||||||
|
|
||||||
|
use futures_core::future::Future;
|
||||||
|
use futures_util::{future, future::FutureExt};
|
||||||
|
use gotham::hyper::{
|
||||||
|
body::{Body, Bytes},
|
||||||
|
StatusCode
|
||||||
|
};
|
||||||
|
use mime::Mime;
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty};
|
||||||
|
use serde_json::error::Error as SerdeJsonError;
|
||||||
|
use std::{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
|
||||||
|
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
|
||||||
|
example that simply returns its body:
|
||||||
|
|
||||||
|
```rust,no_run
|
||||||
|
# #[macro_use] extern crate gotham_restful_derive;
|
||||||
|
# use gotham::router::builder::*;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(create)]
|
||||||
|
struct ImageResource;
|
||||||
|
|
||||||
|
#[create]
|
||||||
|
fn create(body : Raw<Vec<u8>>) -> Raw<Vec<u8>> {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
# fn main() {
|
||||||
|
# gotham::start("127.0.0.1:8080", build_simple_router(|route| {
|
||||||
|
# route.resource::<ImageResource>("img");
|
||||||
|
# }));
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Raw<T> {
|
||||||
|
pub raw: T,
|
||||||
|
pub mime: Mime
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Raw<T> {
|
||||||
|
pub fn new(raw: T, mime: Mime) -> Self {
|
||||||
|
Self { raw, mime }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> AsMut<U> for Raw<T>
|
||||||
|
where
|
||||||
|
T: AsMut<U>
|
||||||
|
{
|
||||||
|
fn as_mut(&mut self) -> &mut U {
|
||||||
|
self.raw.as_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, U> AsRef<U> for Raw<T>
|
||||||
|
where
|
||||||
|
T: AsRef<U>
|
||||||
|
{
|
||||||
|
fn as_ref(&self) -> &U {
|
||||||
|
self.raw.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Clone for Raw<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
raw: self.raw.clone(),
|
||||||
|
mime: self.mime.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: for<'a> From<&'a [u8]>> FromBody for Raw<T> {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_body(body: Bytes, mime: Mime) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self::new(body.as_ref().into(), mime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RequestBody for Raw<T> where Raw<T>: FromBody + ResourceType {}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
impl<T> OpenapiType for Raw<T> {
|
||||||
|
fn schema() -> OpenapiSchema {
|
||||||
|
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
|
||||||
|
format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary),
|
||||||
|
..Default::default()
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<Body>> IntoResponse for Raw<T>
|
||||||
|
where
|
||||||
|
Self: Send
|
||||||
|
{
|
||||||
|
type Err = SerdeJsonError; // just for easier handling of `Result<Raw<T>, E>`
|
||||||
|
|
||||||
|
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, SerdeJsonError>> + Send>> {
|
||||||
|
future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime))).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;
|
||||||
|
|
||||||
|
fn into_response(self) -> Pin<Box<dyn Future<Output = Result<Response, E::Err>> + Send>> {
|
||||||
|
match self {
|
||||||
|
Ok(raw) => raw.into_response(),
|
||||||
|
Err(e) => handle_error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
impl<T, E> ResponseSchema for Result<Raw<T>, E>
|
||||||
|
where
|
||||||
|
Raw<T>: IntoResponseWithSchema,
|
||||||
|
E: Display + IntoResponseError<Err = <Raw<T> as IntoResponse>::Err>
|
||||||
|
{
|
||||||
|
fn schema() -> OpenapiSchema {
|
||||||
|
<Raw<T> as ResponseSchema>::schema()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use futures_executor::block_on;
|
||||||
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn raw_response() {
|
||||||
|
let msg = "Test";
|
||||||
|
let raw = Raw::new(msg, TEXT_PLAIN);
|
||||||
|
let res = block_on(raw.into_response()).expect("didn't expect error response");
|
||||||
|
assert_eq!(res.status, StatusCode::OK);
|
||||||
|
assert_eq!(res.mime, Some(TEXT_PLAIN));
|
||||||
|
assert_eq!(res.full_body().unwrap(), msg.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
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))
|
||||||
|
}
|
||||||
|
}
|
256
src/routing.rs
Normal file
256
src/routing.rs
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use crate::openapi::{
|
||||||
|
builder::{OpenapiBuilder, OpenapiInfo},
|
||||||
|
router::OpenapiRouter
|
||||||
|
};
|
||||||
|
use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response};
|
||||||
|
#[cfg(feature = "cors")]
|
||||||
|
use gotham::router::route::matcher::AccessControlRequestMethodMatcher;
|
||||||
|
use gotham::{
|
||||||
|
handler::HandlerError,
|
||||||
|
helpers::http::response::{create_empty_response, create_response},
|
||||||
|
hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method, StatusCode},
|
||||||
|
pipeline::chain::PipelineHandleChain,
|
||||||
|
router::{
|
||||||
|
builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder},
|
||||||
|
non_match::RouteNonMatch,
|
||||||
|
route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher}
|
||||||
|
},
|
||||||
|
state::{FromState, State}
|
||||||
|
};
|
||||||
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
use std::{any::TypeId, panic::RefUnwindSafe};
|
||||||
|
|
||||||
|
/// Allow us to extract an id from a path.
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
|
pub struct PathExtractor<ID: RefUnwindSafe + Send + 'static> {
|
||||||
|
pub id: ID
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
|
||||||
|
/// router into one that will only allow RESTful resources, but record them and generate
|
||||||
|
/// an OpenAPI specification on request.
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub trait WithOpenapi<D> {
|
||||||
|
fn with_openapi<F>(&mut self, info: OpenapiInfo, block: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(OpenapiRouter<'_, D>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait adds the `resource` method to gotham's routing. It allows you to register
|
||||||
|
/// any RESTful [Resource] with a path.
|
||||||
|
#[_private_openapi_trait(DrawResourcesWithSchema)]
|
||||||
|
pub trait DrawResources {
|
||||||
|
#[openapi_bound("R: crate::ResourceWithSchema")]
|
||||||
|
#[non_openapi_bound("R: crate::Resource")]
|
||||||
|
fn resource<R>(&mut self, path: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait allows to draw routes within an resource. Use this only inside the
|
||||||
|
/// [Resource::setup] method.
|
||||||
|
#[_private_openapi_trait(DrawResourceRoutesWithSchema)]
|
||||||
|
pub trait DrawResourceRoutes {
|
||||||
|
#[openapi_bound("E: crate::EndpointWithSchema")]
|
||||||
|
#[non_openapi_bound("E: crate::Endpoint")]
|
||||||
|
fn endpoint<E: 'static>(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_from(res: Response, state: &State) -> gotham::hyper::Response<Body> {
|
||||||
|
let mut r = create_empty_response(state, res.status);
|
||||||
|
let headers = r.headers_mut();
|
||||||
|
if let Some(mime) = res.mime {
|
||||||
|
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);
|
||||||
|
if method != Method::HEAD {
|
||||||
|
*r.body_mut() = res.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cors")]
|
||||||
|
crate::cors::handle_cors(state, &mut r);
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn endpoint_handler<E: Endpoint>(state: &mut State) -> Result<gotham::hyper::Response<Body>, HandlerError>
|
||||||
|
where
|
||||||
|
E: Endpoint,
|
||||||
|
<E::Output as IntoResponse>::Err: Into<HandlerError>
|
||||||
|
{
|
||||||
|
trace!("entering endpoint_handler");
|
||||||
|
let placeholders = E::Placeholders::take_from(state);
|
||||||
|
// workaround for E::Placeholders and E::Param being the same type
|
||||||
|
// when fixed remove `Clone` requirement on endpoint
|
||||||
|
if TypeId::of::<E::Placeholders>() == TypeId::of::<E::Params>() {
|
||||||
|
state.put(placeholders.clone());
|
||||||
|
}
|
||||||
|
let params = E::Params::take_from(state);
|
||||||
|
|
||||||
|
let body = match E::needs_body() {
|
||||||
|
true => {
|
||||||
|
let body = to_bytes(Body::take_from(state)).await?;
|
||||||
|
|
||||||
|
let content_type: Mime = match HeaderMap::borrow_from(state).get(CONTENT_TYPE) {
|
||||||
|
Some(content_type) => content_type.to_str().unwrap().parse().unwrap(),
|
||||||
|
None => {
|
||||||
|
debug!("Missing Content-Type: Returning 415 Response");
|
||||||
|
let res = create_empty_response(state, StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match E::Body::from_body(body, content_type) {
|
||||||
|
Ok(body) => Some(body),
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Invalid Body: Returning 400 Response");
|
||||||
|
let error: ResourceError = e.into();
|
||||||
|
let json = serde_json::to_string(&error)?;
|
||||||
|
let res = create_response(state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json);
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false => None
|
||||||
|
};
|
||||||
|
|
||||||
|
let out = E::handle(state, placeholders, params, body).await;
|
||||||
|
let res = out.into_response().await.map_err(Into::into)?;
|
||||||
|
debug!("Returning response {:?}", res);
|
||||||
|
Ok(response_from(res, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MaybeMatchAcceptHeader {
|
||||||
|
matcher: Option<AcceptHeaderRouteMatcher>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouteMatcher for MaybeMatchAcceptHeader {
|
||||||
|
fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
|
||||||
|
match &self.matcher {
|
||||||
|
Some(matcher) => matcher.is_match(state),
|
||||||
|
None => Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaybeMatchAcceptHeader {
|
||||||
|
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||||
|
let types = match types {
|
||||||
|
Some(types) if types.is_empty() => None,
|
||||||
|
types => types
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
matcher: types.map(AcceptHeaderRouteMatcher::new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<Vec<Mime>>> for MaybeMatchAcceptHeader {
|
||||||
|
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||||
|
Self::new(types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MaybeMatchContentTypeHeader {
|
||||||
|
matcher: Option<ContentTypeHeaderRouteMatcher>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouteMatcher for MaybeMatchContentTypeHeader {
|
||||||
|
fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
|
||||||
|
match &self.matcher {
|
||||||
|
Some(matcher) => matcher.is_match(state),
|
||||||
|
None => Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaybeMatchContentTypeHeader {
|
||||||
|
fn new(types: Option<Vec<Mime>>) -> Self {
|
||||||
|
Self {
|
||||||
|
matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<Vec<Mime>>> for MaybeMatchContentTypeHeader {
|
||||||
|
fn from(types: Option<Vec<Mime>>) -> Self {
|
||||||
|
Self::new(types)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! implDrawResourceRoutes {
|
||||||
|
($implType:ident) => {
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
impl<'a, C, P> WithOpenapi<Self> for $implType<'a, C, P>
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn with_openapi<F>(&mut self, info: OpenapiInfo, block: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>)
|
||||||
|
{
|
||||||
|
let router = OpenapiRouter {
|
||||||
|
router: self,
|
||||||
|
scope: None,
|
||||||
|
openapi_builder: &mut OpenapiBuilder::new(info)
|
||||||
|
};
|
||||||
|
block(router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, C, P> DrawResources for $implType<'a, C, P>
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn resource<R: Resource>(&mut self, path: &str) {
|
||||||
|
R::setup((self, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str)
|
||||||
|
where
|
||||||
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
||||||
|
P: RefUnwindSafe + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
fn endpoint<E: Endpoint + 'static>(&mut self) {
|
||||||
|
let uri = format!("{}/{}", self.1, E::uri());
|
||||||
|
debug!("Registering endpoint for {}", uri);
|
||||||
|
self.0.associate(&uri, |assoc| {
|
||||||
|
assoc
|
||||||
|
.request(vec![E::http_method()])
|
||||||
|
.add_route_matcher(MaybeMatchAcceptHeader::new(E::Output::accepted_types()))
|
||||||
|
.with_path_extractor::<E::Placeholders>()
|
||||||
|
.with_query_string_extractor::<E::Params>()
|
||||||
|
.to_async_borrowing(endpoint_handler::<E>);
|
||||||
|
|
||||||
|
#[cfg(feature = "cors")]
|
||||||
|
if E::http_method() != Method::GET {
|
||||||
|
assoc
|
||||||
|
.options()
|
||||||
|
.add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method()))
|
||||||
|
.to(crate::cors::cors_preflight_handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
implDrawResourceRoutes!(RouterBuilder);
|
||||||
|
implDrawResourceRoutes!(ScopeBuilder);
|
99
src/types.rs
Normal file
99
src/types.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use gotham::hyper::body::Bytes;
|
||||||
|
use mime::{Mime, APPLICATION_JSON};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "openapi"))]
|
||||||
|
pub trait ResourceType {}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "openapi"))]
|
||||||
|
impl<T> ResourceType for T {}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
pub trait ResourceType: OpenapiType {}
|
||||||
|
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
impl<T: OpenapiType> ResourceType for T {}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// [OpenapiType].
|
||||||
|
///
|
||||||
|
/// [OpenapiType]: trait.OpenapiType.html
|
||||||
|
pub trait ResponseBody: ResourceType + Serialize {}
|
||||||
|
|
||||||
|
impl<T: ResourceType + Serialize> ResponseBody for T {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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 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]. If you have any additional variables, they
|
||||||
|
need to be [Default]. This is an example of such a struct:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate gotham_restful;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
#[derive(FromBody, RequestBody)]
|
||||||
|
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
||||||
|
struct RawImage {
|
||||||
|
content: Vec<u8>,
|
||||||
|
content_type: Mime
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
pub trait FromBody: Sized {
|
||||||
|
/// 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 [std::convert::Infallible] is used here.
|
||||||
|
/// However, this might change in the future.
|
||||||
|
type Err: Error;
|
||||||
|
|
||||||
|
/// Perform the conversion.
|
||||||
|
fn from_body(body: Bytes, content_type: Mime) -> Result<Self, Self::Err>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DeserializeOwned> FromBody for T {
|
||||||
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
|
fn from_body(body: Bytes, _content_type: Mime) -> Result<Self, Self::Err> {
|
||||||
|
serde_json::from_slice(&body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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].
|
||||||
|
|
||||||
|
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
|
||||||
|
[FromBody] and optionally a list of supported media types:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate gotham_restful;
|
||||||
|
# use gotham_restful::*;
|
||||||
|
#[derive(FromBody, RequestBody)]
|
||||||
|
#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)]
|
||||||
|
struct RawImage {
|
||||||
|
content: Vec<u8>,
|
||||||
|
content_type: Mime
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[OpenapiType]: trait.OpenapiType.html
|
||||||
|
*/
|
||||||
|
pub trait RequestBody: ResourceType + FromBody {
|
||||||
|
/// Return all types that are supported as content types. Use `None` if all types are supported.
|
||||||
|
fn supported_types() -> Option<Vec<Mime>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ResourceType + DeserializeOwned> RequestBody for T {
|
||||||
|
fn supported_types() -> Option<Vec<Mime>> {
|
||||||
|
Some(vec![APPLICATION_JSON])
|
||||||
|
}
|
||||||
|
}
|
133
tests/async_methods.rs
Normal file
133
tests/async_methods.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
|
use gotham::{
|
||||||
|
hyper::{HeaderMap, Method},
|
||||||
|
router::builder::*,
|
||||||
|
state::State,
|
||||||
|
test::TestServer
|
||||||
|
};
|
||||||
|
use gotham_restful::*;
|
||||||
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
mod util {
|
||||||
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
|
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all, read, search, create, change_all, change, remove_all, remove, state_test)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct FooBody {
|
||||||
|
data: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct FooSearch {
|
||||||
|
query: String
|
||||||
|
}
|
||||||
|
|
||||||
|
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||||
|
#[read_all]
|
||||||
|
async fn read_all() -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||||
|
#[read]
|
||||||
|
async fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||||
|
#[search]
|
||||||
|
async fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||||
|
#[create]
|
||||||
|
async fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||||
|
#[change_all]
|
||||||
|
async fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||||
|
#[change]
|
||||||
|
async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||||
|
#[remove_all]
|
||||||
|
async fn remove_all() -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||||
|
#[remove]
|
||||||
|
async fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM";
|
||||||
|
#[endpoint(method = "Method::GET", uri = "state_test")]
|
||||||
|
async fn state_test(state: &mut State) -> Raw<&'static [u8]> {
|
||||||
|
sleep(Duration::from_nanos(1)).await;
|
||||||
|
state.borrow::<HeaderMap>();
|
||||||
|
sleep(Duration::from_nanos(1)).await;
|
||||||
|
Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn async_methods() {
|
||||||
|
let _ = pretty_env_logger::try_init_timed();
|
||||||
|
|
||||||
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
|
router.resource::<FooResource>("foo");
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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/search?query=hello+world", SEARCH_RESPONSE);
|
||||||
|
test_post_response(
|
||||||
|
&server,
|
||||||
|
"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/1", REMOVE_RESPONSE);
|
||||||
|
test_get_response(&server, "http://localhost/foo/state_test", STATE_TEST_RESPONSE);
|
||||||
|
}
|
323
tests/cors_handling.rs
Normal file
323
tests/cors_handling.rs
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
#![cfg(feature = "cors")]
|
||||||
|
use gotham::{
|
||||||
|
hyper::{body::Body, client::connect::Connect, header::*, StatusCode},
|
||||||
|
pipeline::{new_pipeline, single::single_pipeline},
|
||||||
|
router::builder::*,
|
||||||
|
test::{Server, TestRequest, TestServer}
|
||||||
|
};
|
||||||
|
use gotham_restful::{
|
||||||
|
change_all,
|
||||||
|
cors::{Headers, Origin},
|
||||||
|
read_all, CorsConfig, DrawResources, Raw, Resource
|
||||||
|
};
|
||||||
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all, change_all)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
fn read_all() {}
|
||||||
|
|
||||||
|
#[change_all]
|
||||||
|
fn change_all(_body: Raw<Vec<u8>>) {}
|
||||||
|
|
||||||
|
fn test_server(cfg: CorsConfig) -> TestServer {
|
||||||
|
let (chain, pipeline) = single_pipeline(new_pipeline().add(cfg).build());
|
||||||
|
TestServer::new(build_router(chain, pipeline, |router| router.resource::<FooResource>("/foo"))).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_response<TS, C>(req: TestRequest<TS, C>, origin: Option<&str>, vary: Option<&str>, credentials: bool)
|
||||||
|
where
|
||||||
|
TS: Server + 'static,
|
||||||
|
C: Connect + Clone + Send + Sync + 'static
|
||||||
|
{
|
||||||
|
let res = req
|
||||||
|
.with_header(ORIGIN, "http://example.org".parse().unwrap())
|
||||||
|
.perform()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||||
|
let headers = res.headers();
|
||||||
|
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(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!(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) {
|
||||||
|
let res = server
|
||||||
|
.client()
|
||||||
|
.options("http://example.org/foo")
|
||||||
|
.with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap())
|
||||||
|
.with_header(ORIGIN, "http://example.org".parse().unwrap())
|
||||||
|
.perform()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res.status(), StatusCode::NO_CONTENT);
|
||||||
|
let headers = res.headers();
|
||||||
|
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!(
|
||||||
|
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(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]
|
||||||
|
fn cors_origin_none() {
|
||||||
|
let cfg = Default::default();
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
test_preflight(&server, "PUT", None, "access-control-request-method", false, 0);
|
||||||
|
|
||||||
|
test_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]
|
||||||
|
fn cors_origin_star() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
origin: Origin::Star,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
Some("*"),
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_origin_single() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
origin: Origin::Single("https://foo.com".to_owned()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
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(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
Some("https://foo.com"),
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_origin_copy() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
origin: Origin::Copy,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
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(
|
||||||
|
server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
Some("http://example.org"),
|
||||||
|
Some("origin"),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_headers_none() {
|
||||||
|
let cfg = Default::default();
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
test_preflight_headers(&server, "PUT", None, None, "access-control-request-method");
|
||||||
|
test_preflight_headers(&server, "PUT", Some("Content-Type"), None, "access-control-request-method");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_headers_list() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
headers: Headers::List(vec![CONTENT_TYPE]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
test_preflight_headers(&server, "PUT", None, Some("content-type"), "access-control-request-method");
|
||||||
|
test_preflight_headers(
|
||||||
|
&server,
|
||||||
|
"PUT",
|
||||||
|
Some("content-type"),
|
||||||
|
Some("content-type"),
|
||||||
|
"access-control-request-method"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_headers_copy() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
headers: Headers::Copy,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
test_preflight_headers(
|
||||||
|
&server,
|
||||||
|
"PUT",
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"access-control-request-method,access-control-request-headers"
|
||||||
|
);
|
||||||
|
test_preflight_headers(
|
||||||
|
&server,
|
||||||
|
"PUT",
|
||||||
|
Some("content-type"),
|
||||||
|
Some("content-type"),
|
||||||
|
"access-control-request-method,access-control-request-headers"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_credentials() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
origin: Origin::None,
|
||||||
|
credentials: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_max_age() {
|
||||||
|
let cfg = CorsConfig {
|
||||||
|
origin: Origin::None,
|
||||||
|
max_age: 31536000,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server = test_server(cfg);
|
||||||
|
|
||||||
|
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().put("http://example.org/foo", Body::empty(), TEXT_PLAIN),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
39
tests/custom_request_body.rs
Normal file
39
tests/custom_request_body.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use gotham::{hyper::header::CONTENT_TYPE, router::builder::*, test::TestServer};
|
||||||
|
use gotham_restful::*;
|
||||||
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
|
const RESPONSE: &[u8] = b"This is the only valid response.";
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(create)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[derive(FromBody, RequestBody)]
|
||||||
|
#[supported_types(TEXT_PLAIN)]
|
||||||
|
struct Foo {
|
||||||
|
content: Vec<u8>,
|
||||||
|
content_type: Mime
|
||||||
|
}
|
||||||
|
|
||||||
|
#[create]
|
||||||
|
fn create(body: Foo) -> Raw<Vec<u8>> {
|
||||||
|
Raw::new(body.content, body.content_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_request_body() {
|
||||||
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
|
router.resource::<FooResource>("foo");
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let res = server
|
||||||
|
.client()
|
||||||
|
.post("http://localhost/foo", RESPONSE, TEXT_PLAIN)
|
||||||
|
.perform()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain");
|
||||||
|
let res = res.read_body().unwrap();
|
||||||
|
let body: &[u8] = res.as_ref();
|
||||||
|
assert_eq!(body, RESPONSE);
|
||||||
|
}
|
252
tests/openapi_specification.json
Normal file
252
tests/openapi_specification.json
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
{
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"Secret": {
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"format": "float",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"code"
|
||||||
|
],
|
||||||
|
"title": "Secret",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"Secrets": {
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"secrets"
|
||||||
|
],
|
||||||
|
"title": "Secrets",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {
|
||||||
|
"authToken": {
|
||||||
|
"bearerFormat": "JWT",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"type": "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"title": "This is just a test",
|
||||||
|
"version": "1.2.3"
|
||||||
|
},
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"paths": {
|
||||||
|
"/custom": {
|
||||||
|
"patch": {
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/custom/read/{from}/with/{id}": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "from",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/img/{id}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getImage",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "setImage",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"image/png": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/secret/search": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "date",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "date",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "hour",
|
||||||
|
"schema": {
|
||||||
|
"format": "int16",
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"style": "form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "minute",
|
||||||
|
"schema": {
|
||||||
|
"format": "int16",
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"style": "form"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Secrets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"authToken": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/secret/{id}": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"style": "simple"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"authToken": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://localhost:12345/api/v1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
125
tests/openapi_specification.rs
Normal file
125
tests/openapi_specification.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
|
use gotham::{
|
||||||
|
hyper::Method,
|
||||||
|
pipeline::{new_pipeline, single::single_pipeline},
|
||||||
|
router::builder::*,
|
||||||
|
test::TestServer
|
||||||
|
};
|
||||||
|
use gotham_restful::*;
|
||||||
|
use mime::IMAGE_PNG;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod util {
|
||||||
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
|
use util::{test_get_response, test_openapi_response};
|
||||||
|
|
||||||
|
const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=";
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(get_image, set_image)]
|
||||||
|
struct ImageResource;
|
||||||
|
|
||||||
|
#[derive(FromBody, RequestBody)]
|
||||||
|
#[supported_types(IMAGE_PNG)]
|
||||||
|
struct Image(Vec<u8>);
|
||||||
|
|
||||||
|
#[read(operation_id = "getImage")]
|
||||||
|
fn get_image(_id: u64) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[change(operation_id = "setImage")]
|
||||||
|
fn set_image(_id: u64, _image: Image) {}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_secret, search_secret)]
|
||||||
|
struct SecretResource;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
struct AuthData {
|
||||||
|
sub: String,
|
||||||
|
iat: u64,
|
||||||
|
exp: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthStatus = gotham_restful::AuthStatus<AuthData>;
|
||||||
|
|
||||||
|
#[derive(OpenapiType, Serialize)]
|
||||||
|
struct Secret {
|
||||||
|
code: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(OpenapiType, Serialize)]
|
||||||
|
struct Secrets {
|
||||||
|
secrets: Vec<Secret>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)]
|
||||||
|
struct SecretQuery {
|
||||||
|
date: NaiveDate,
|
||||||
|
hour: Option<u16>,
|
||||||
|
minute: Option<u16>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[read]
|
||||||
|
fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess<Secret> {
|
||||||
|
auth.ok()?;
|
||||||
|
Ok(Secret { code: 4.2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[search]
|
||||||
|
fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess<Secrets> {
|
||||||
|
auth.ok()?;
|
||||||
|
Ok(Secrets {
|
||||||
|
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]
|
||||||
|
fn openapi_specification() {
|
||||||
|
let info = OpenapiInfo {
|
||||||
|
title: "This is just a test".to_owned(),
|
||||||
|
version: "1.2.3".to_owned(),
|
||||||
|
urls: vec!["http://localhost:12345/api/v1".to_owned()]
|
||||||
|
};
|
||||||
|
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());
|
||||||
|
let server = TestServer::new(build_router(chain, pipelines, |router| {
|
||||||
|
router.with_openapi(info, |mut router| {
|
||||||
|
router.resource::<ImageResource>("img");
|
||||||
|
router.resource::<SecretResource>("secret");
|
||||||
|
router.resource::<CustomResource>("custom");
|
||||||
|
router.get_openapi("openapi");
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json");
|
||||||
|
}
|
78
tests/openapi_supports_scope.json
Normal file
78
tests/openapi_supports_scope.json
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"components": {},
|
||||||
|
"info": {
|
||||||
|
"title": "Test",
|
||||||
|
"version": "1.2.3"
|
||||||
|
},
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"paths": {
|
||||||
|
"/bar/baz/foo3": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/bar/foo2": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/foo1": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/foo4": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
tests/openapi_supports_scope.rs
Normal file
50
tests/openapi_supports_scope.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#![cfg(feature = "openapi")]
|
||||||
|
use gotham::{router::builder::*, test::TestServer};
|
||||||
|
use gotham_restful::*;
|
||||||
|
use mime::TEXT_PLAIN;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod util {
|
||||||
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
|
use util::{test_get_response, test_openapi_response};
|
||||||
|
|
||||||
|
const RESPONSE: &[u8] = b"This is the only valid response.";
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
fn read_all() -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn openapi_supports_scope() {
|
||||||
|
let info = OpenapiInfo {
|
||||||
|
title: "Test".to_owned(),
|
||||||
|
version: "1.2.3".to_owned(),
|
||||||
|
urls: Vec::new()
|
||||||
|
};
|
||||||
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
|
router.with_openapi(info, |mut router| {
|
||||||
|
router.get_openapi("openapi");
|
||||||
|
router.resource::<FooResource>("foo1");
|
||||||
|
router.scope("/bar", |router| {
|
||||||
|
router.resource::<FooResource>("foo2");
|
||||||
|
router.scope("/baz", |router| {
|
||||||
|
router.resource::<FooResource>("foo3");
|
||||||
|
})
|
||||||
|
});
|
||||||
|
router.resource::<FooResource>("foo4");
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
test_get_response(&server, "http://localhost/foo1", RESPONSE);
|
||||||
|
test_get_response(&server, "http://localhost/bar/foo2", RESPONSE);
|
||||||
|
test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE);
|
||||||
|
test_get_response(&server, "http://localhost/foo4", RESPONSE);
|
||||||
|
test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_supports_scope.json");
|
||||||
|
}
|
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?
|
||||||
|
}
|
||||||
|
}
|
117
tests/sync_methods.rs
Normal file
117
tests/sync_methods.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_derive;
|
||||||
|
|
||||||
|
use gotham::{router::builder::*, test::TestServer};
|
||||||
|
use gotham_restful::*;
|
||||||
|
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||||
|
#[cfg(feature = "openapi")]
|
||||||
|
use openapi_type::OpenapiType;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
mod util {
|
||||||
|
include!("util/mod.rs");
|
||||||
|
}
|
||||||
|
use util::{test_delete_response, test_get_response, test_post_response, test_put_response};
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all, read, search, create, change_all, change, remove_all, remove)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct FooBody {
|
||||||
|
data: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, StateData, StaticResponseExtender)]
|
||||||
|
#[cfg_attr(feature = "openapi", derive(OpenapiType))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct FooSearch {
|
||||||
|
query: String
|
||||||
|
}
|
||||||
|
|
||||||
|
const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e";
|
||||||
|
#[read_all]
|
||||||
|
fn read_all() -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9";
|
||||||
|
#[read]
|
||||||
|
fn read(_id: u64) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(READ_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E";
|
||||||
|
#[search]
|
||||||
|
fn search(_body: FooSearch) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(SEARCH_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83";
|
||||||
|
#[create]
|
||||||
|
fn create(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(CREATE_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv";
|
||||||
|
#[change_all]
|
||||||
|
fn change_all(_body: FooBody) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu";
|
||||||
|
#[change]
|
||||||
|
fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(CHANGE_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5";
|
||||||
|
#[remove_all]
|
||||||
|
fn remove_all() -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq";
|
||||||
|
#[remove]
|
||||||
|
fn remove(_id: u64) -> Raw<&'static [u8]> {
|
||||||
|
Raw::new(REMOVE_RESPONSE, TEXT_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_methods() {
|
||||||
|
let _ = pretty_env_logger::try_init_timed();
|
||||||
|
|
||||||
|
let server = TestServer::new(build_simple_router(|router| {
|
||||||
|
router.resource::<FooResource>("foo");
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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/search?query=hello+world", SEARCH_RESPONSE);
|
||||||
|
test_post_response(
|
||||||
|
&server,
|
||||||
|
"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/1", REMOVE_RESPONSE);
|
||||||
|
}
|
10
tests/trybuild_ui.rs
Normal file
10
tests/trybuild_ui.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use trybuild::TestCases;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn trybuild_ui() {
|
||||||
|
let t = TestCases::new();
|
||||||
|
t.compile_fail("tests/ui/endpoint/*.rs");
|
||||||
|
t.compile_fail("tests/ui/from_body/*.rs");
|
||||||
|
t.compile_fail("tests/ui/resource/*.rs");
|
||||||
|
}
|
12
tests/ui/endpoint/async_state.rs
Normal file
12
tests/ui/endpoint/async_state.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate gotham_restful;
|
||||||
|
use gotham::state::State;
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
#[resource(read_all)]
|
||||||
|
struct FooResource;
|
||||||
|
|
||||||
|
#[read_all]
|
||||||
|
async fn read_all(state: &State) {}
|
||||||
|
|
||||||
|
fn main() {}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue