1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-04-19 22:44:38 +00:00

Compare commits

...

203 commits

Author SHA1 Message Date
7bac379e05
update readme: moved to github [skip ci] 2021-03-10 19:03:25 +01:00
2dd3f3e21a
fix broken doc link 2021-03-09 22:35:22 +01:00
e206ab10eb
update trybuild tests 2021-03-09 21:23:44 +01:00
a5257608e3
update readme 2021-03-09 21:00:35 +01:00
9c7f681e3d
remove outdated tests 2021-03-09 20:51:44 +01:00
63567f5480
ci: fix typo 2021-03-09 20:08:12 +01:00
3a3f743369
ci: include openapi_type crate 2021-03-09 19:55:04 +01:00
ebea39fe0d
use openapi_type::OpenapiType for gotham_restful 2021-03-09 19:46:11 +01:00
eecd192458
redo and test openapi type implementations 2021-03-09 17:07:41 +01:00
2a35e044db
more error messages
[skip ci]
2021-03-09 16:17:11 +01:00
a57f1c097d
enum representations
[skip ci]
2021-03-09 00:17:13 +01:00
5f60599c41
more enum stuff
[skip ci]
2021-03-08 17:33:49 +01:00
43d3a1cd89
start implementing enums 2021-03-08 17:20:41 +01:00
667009bd22
copy OpenapiType implementations and fix codegen reference 2021-03-08 16:33:38 +01:00
d9c7f4135f
start openapi-type codegen 2021-03-07 23:09:50 +01:00
90870e3b6a
basic structure for openapi_type crate 2021-03-07 19:05:25 +01:00
2251c29d7b
Merge commit '09dee5a673ac29ec38ab6f1442f403b20a776959' (release 0.2.1)
[skip ci]
2021-03-04 00:10:07 +01:00
09dee5a673
update changelog 2021-03-03 23:46:13 +01:00
fabcbc4e78
pin version of openapiv3
they introduced breaking changes in patch release 0.3.3
2021-03-03 23:44:53 +01:00
960ba0e8bc
track gotham master 2021-02-28 01:59:59 +01:00
bb6f5b0fdd
release 0.2.0 2021-02-27 17:16:04 +01:00
msrd0
31f92c07cd Custom HTTP Headers 2021-02-27 15:40:34 +00:00
28ae4dfdee
add swagger_ui to the router 2021-02-25 00:37:55 +01:00
msrd0
7ed98c82e8 OpenAPI: Key Schema for HashMap's 2021-02-24 18:53:44 +00:00
e7ef6bdf5a
cargo is dumb 2021-02-22 10:01:43 +01:00
666514c8e2
fix the example 2021-02-22 09:58:01 +01:00
msrd0
7de11cdae1 split the ResourceResult trait 2021-02-21 18:21:09 +00:00
c640efcb88
fix ui when openapi is enabled 2021-02-21 18:31:44 +01:00
30edd349ed
improve ui on invalid types for endpoints 2021-02-21 18:06:50 +01:00
90fc17e57d
ci: update cargo-readme 2021-02-17 04:02:54 +01:00
3b95b9f495
update trybuild rust version and rustfmt 2021-02-17 03:39:49 +01:00
1bd398c7ee
bump MSRV to 1.49+
not sure why but all older rust versions fail generic type resolution
2021-02-04 00:12:17 +01:00
8b73701405
replace some std::error::Error bounds with Into<HandlerError> 2021-02-03 22:58:08 +01:00
9e65540cd8
ci: make the rustfmt.sh script ash-compatible 2021-02-03 22:37:34 +01:00
44e2f0317c
ci: use less broken docker image for rustfmt 2021-02-03 22:24:05 +01:00
msrd0
af28e0d916 Reexports 2021-02-03 21:22:46 +00:00
msrd0
441a42c75e Add a Redirect type that can be returned by endpoints 2021-01-26 17:49:11 +00:00
cf0223473f
allow more types to appear in AuthErrorOrOther (related to #20) 2021-01-23 16:11:33 +01:00
f2bcc8438f
fix implicit &'static mut State error 2021-01-18 19:04:06 +01:00
681ef5d894
add debug option to endpoint macro 2021-01-18 18:50:26 +01:00
70914d107b
fix missing FromState import 2021-01-18 18:38:12 +01:00
msrd0
5261aa9931 Custom Endpoints 2021-01-18 16:56:16 +00:00
002cfb1b4d
fix some lints 2021-01-18 01:07:41 +01:00
msrd0
b807ae2796 Replace methods with more flexible endpoints 2021-01-18 00:05:30 +00:00
0ac0f0f504
fix features for doc 2021-01-15 17:33:40 +01:00
0251c03ceb
ci: fix tarpaulin features 2021-01-15 15:16:09 +01:00
75cd7e2c96
ci: the same applies to tarpaulin 2021-01-15 03:55:11 +01:00
edd8bb618d
ci: --workspace applies features from the example which breaks stuff 2021-01-15 03:49:35 +01:00
daea3ba9ec
introduce without-openapi feature to improve #4 2021-01-14 18:55:44 +01:00
b7a1193333
make all fields of response private, we're breaking change anyways
Closes #34
Related to #27
2021-01-14 18:45:32 +01:00
44f3c9fe84
add headers to the response (#27) 2021-01-14 18:37:51 +01:00
3600a115d0
use rust 1.49 for trybuild ui testing 2021-01-01 19:31:09 +01:00
388bf8b49c
apply some clippy suggestions 2021-01-01 18:03:31 +01:00
6ee382242b
fix example 2021-01-01 17:43:43 +01:00
813c12614f
update readme 2021-01-01 16:49:35 +01:00
766bc9d17d
support copying headers in cors preflight requests 2021-01-01 16:44:55 +01:00
b005346e54
fix the readme template 2021-01-01 16:18:50 +01:00
141e5ac2d7
bump version to 0.2.0-dev and change license to Apache-2.0 only 2021-01-01 15:56:47 +01:00
2b8796b9c9
update readme 2021-01-01 15:43:40 +01:00
71961268c4
add include directive to Cargo.toml to reduce crate size 2020-12-28 18:57:47 +01:00
9efc1cb192
bump version to 0.1.1 and add changelog 2020-12-28 18:35:48 +01:00
1ac323accc
example: replace log4rs with pretty_env_logger 2020-12-28 18:18:10 +01:00
7851b50bcd
update itertools to 0.10 and mark it as optional 2020-12-28 18:11:29 +01:00
8a8e01e757
add openapi support for NonZeroU types 2020-11-25 03:11:30 +01:00
fee9dc89b0
support cookie auth without cookie jar in state 2020-11-23 23:17:28 +01:00
4fd5464e44
get rid of mod's in error messages 2020-11-23 01:22:03 +01:00
f9c2009023
ci: check rustfmt 2020-11-23 00:02:16 +01:00
f1e1c5e521
update readme 2020-11-22 23:56:25 +01:00
4ae860dd32
docs: use [Type] syntax from rust 1.48 2020-11-22 23:55:52 +01:00
ed1bbbd1fb
add resource error integration test 2020-11-22 23:18:28 +01:00
bb945e2cc6
ci: fix tarpaulin report and include cobertura.xml 2020-11-22 22:31:51 +01:00
2cb326a4c3
ci: inspect the present files in publish stage 2020-11-22 22:09:38 +01:00
00ffe95354
fix broken doc links 2020-11-21 16:53:18 +01:00
msrd0
37aa497e59 Update CI to use the rust Docker image 2020-11-21 14:47:29 +00:00
e6721275ae
ci: remove 'cargo sweep' 2020-11-21 02:07:26 +01:00
24893223b2
remove unused code and fix lint warning 2020-11-20 01:30:12 +01:00
ce570c4787
allow &mut State in async fn (needs test) 2020-11-20 01:12:20 +01:00
6ded517824
update futures to 0.3.7 (https://rustsec.org/advisories/RUSTSEC-2020-0059.html) 2020-11-04 19:34:25 +01:00
d3da7d0182
remove publish from ci 2020-10-04 18:25:02 +02:00
1369d25c8a
bump version to 0.1.0 2020-10-02 14:21:56 +02:00
a0059fd7b9
some dependency updates 2020-10-02 13:37:33 +02:00
d9c22579b0
only log errors on 500 responses (fixes #30) 2020-09-19 19:40:39 +02:00
eeea62859f
update log4rs to 0.13.0 2020-09-19 19:31:43 +02:00
0729d5b8ed
use anyhow 2020-09-17 12:25:58 +02:00
6470bd59ef
fix weird rust issue where PartialEq is not being detected since 1.44.0
I did not manage to get a repro of this since all old versions of rust
fail to compile, eventhough the CI clearly confirms that it used to
compile with 1.43.0
2020-09-17 12:21:09 +02:00
31835fe57f
use upstream Access-Control-Request-Method route matcher 2020-09-15 15:20:04 +02:00
38feb5b781
oops 2020-09-15 15:12:11 +02:00
d55b0897e9
update to gotham 0.5 and start using rustfmt 2020-09-15 15:10:41 +02:00
5317e50961
improve feature documentation 2020-05-27 10:22:13 +02:00
0541ee2e0b
fix derive's Cargo.toml 2020-05-21 01:44:18 +02:00
f72b9ac797
bump version to 0.1.0-rc0 2020-05-20 23:55:19 +02:00
912f030bfd
improve the From impl for AuthErrorOrOther #20 2020-05-20 19:51:54 +02:00
0b06528742
fix #19 remove ugly regex 2020-05-20 19:36:07 +02:00
c1cb0e692a
gotham finally has a release candidate 2020-05-20 09:33:12 +02:00
8321b63982
tests for the access control request method matcher 2020-05-20 09:01:11 +02:00
e5e9cd5d3c
openapi spec tests 2020-05-19 21:07:29 +02:00
81803fd54a
add HashMap as OpenapiType 2020-05-19 19:23:29 +02:00
7268cc0567
cors in the example 2020-05-19 19:09:23 +02:00
b39b30694e
update readme 2020-05-17 01:41:58 +02:00
955715eea6
I don't know how I ended up with spaces 2020-05-16 14:27:13 +02:00
dc26e9a02e
improve cors doc 2020-05-16 14:22:23 +02:00
94abc75268
works on my machineTM 2020-05-16 13:59:47 +02:00
20818b0f95
enable doc test for default features 2020-05-16 01:06:07 +02:00
4ff5a8d7e4
doctest fix #26 2020-05-16 01:06:07 +02:00
msrd0
604494651d Merge branch 'cors' into 'master'
Allow configuring CORS

Closes #22

See merge request msrd0/gotham-restful!14
2020-05-15 19:36:30 +00:00
74ef0af512
cors tests 2020-05-15 21:19:26 +02:00
f20c768d02
cors preflight 2020-05-14 23:30:59 +02:00
748bf65d3e
cors for non-preflight requests 2020-05-13 19:11:22 +02:00
40c90e6b4a
no need to use stringify! in a proc macro 2020-05-09 18:10:50 +02:00
b9002bd70d
remove Resource::name() method and update resource documentation 2020-05-09 18:01:47 +02:00
6680887b84
add trybuild tests for OpenapiType and Resource derive macros 2020-05-09 15:29:29 +02:00
9ed24c9bcb
ci: use cargo sweep instead of cargo clean 2020-05-09 03:27:58 +02:00
e2eb9b0fcc
fix 2020-05-08 22:54:54 +02:00
b1b9858da4
ci: run cargo clean before caching
the shared runners ofter run out of memory when creating the cache, but
are definitely far too slow to be able to work without caching, so try
to minimize the amout of storage required.
2020-05-08 21:52:57 +02:00
e05f9bb963
a whole bunch of tests for the method macros 2020-05-08 18:39:23 +02:00
4bf0bd7b09
add design goals to readme 2020-05-08 15:10:37 +02:00
ea80689db2
update badges 2020-05-06 17:28:36 +02:00
f8181bcb7e
cargo test is stupid 2020-05-06 17:19:17 +02:00
e470f060e3
deploy documentation as gitlab pages 2020-05-06 15:58:11 +02:00
b1801f2486
update log4rs 2020-05-06 13:46:22 +02:00
5587ded60d
merge workspace and main crate 2020-05-05 23:18:05 +02:00
52679ad29d
remove FromBodyNoError and replace with std::convert::Infallible 2020-05-05 23:08:57 +02:00
4cd2474d90
add documentation and some traits for Raw<T> 2020-05-05 23:05:17 +02:00
bccefa8248
make tarpaulin not timeout 2020-05-05 19:57:13 +02:00
e5f13792c6
doc & test for RequestBody 2020-05-05 19:50:23 +02:00
aa9fa0f457
doc & ui-test for FromBody 2020-05-05 19:31:02 +02:00
e7e55514a2
support scopes inside openapi router (implements #5) 2020-05-05 00:34:19 +02:00
022edede62
the next release will be 0.1.0, not 0.0.5 2020-05-04 23:52:09 +02:00
msrd0
d8c4215cc2 Merge branch 'rename-update-and-delete' into 'master'
Rename update and delete methods

Closes #17

See merge request msrd0/gotham-restful!13
2020-05-04 21:48:45 +00:00
a1acc06f6d
update doc 2020-05-04 23:38:39 +02:00
cc86d3396c
rename update to change, delete to remove, and remove rest_ prefix from macros 2020-05-04 20:45:46 +02:00
3de130e104
emit proper error message for async fn read_all(state: &State) 2020-05-04 20:30:15 +02:00
110ef2be7a
simplify derive/macro code 2020-05-04 19:08:22 +02:00
7ef964b0a0
fix 2020-05-04 00:27:14 +02:00
328ebf821e
some minor improvements 2020-05-03 23:46:46 +02:00
992d9be195
use less non-public syn api 2020-05-03 23:43:42 +02:00
5e5e3aaf9d
don't use syn::token module 2020-05-03 23:25:48 +02:00
da30f34d97
use DeriveInput for input into derive macros from syn 2020-05-03 23:10:19 +02:00
f7157dcf62
add some tests for OpenapiBuilder 2020-05-03 19:17:55 +02:00
101e94b900
improve test coverage for the result types 2020-05-03 18:49:23 +02:00
msrd0
0d95ca4abb Merge branch 'error-derive' into 'master'
Allow custom error types through a macro and allow them to be used with Result

Closes #13 and #9

See merge request msrd0/gotham-restful!12
2020-05-01 14:48:11 +00:00
msrd0
d754d6044d Allow custom error types through a macro and allow them to be used with Result 2020-05-01 14:48:11 +00:00
8593e133b7
also detect _state as state argument 2020-04-30 16:49:40 +02:00
a36993f615
there is no need to force people to take &State arg
this highly improves async compatibility
2020-04-30 00:37:24 +02:00
cd7cf07318
rust can't think for itself 2020-04-29 21:00:06 +02:00
e013af8e18
remove some of the &mut &mut types (#6) 2020-04-29 19:22:32 +02:00
45eac21726
Proper OpenAPI type for path parameters (Fixes #18) 2020-04-29 19:10:11 +02:00
9fd0bceaf4
move openapi operation extraction code into its own mod 2020-04-27 02:12:51 +02:00
01f818e268
split the openapi code into several files 2020-04-26 22:34:22 +02:00
b4eaeca01c
don't require the get_openapi call be the last one to contain the full spec 2020-04-26 22:20:07 +02:00
96317cdfb7
fix documentation for accept header matcher 2020-04-25 20:47:13 +02:00
msrd0
805df80971 Merge branch 'path-matchers' into 'master'
Add path matchers that are more capable than gotham's stock ones

See merge request msrd0/gotham-restful!11
2020-04-25 18:31:57 +00:00
msrd0
4ce53bc361 Add path matchers that are more capable than gotham's stock ones 2020-04-25 18:31:57 +00:00
d08d9bea8c
fix some clippy warnings 2020-04-25 17:01:16 +02:00
147ea980bf
move to Rust 1.42 features 2020-04-25 16:47:33 +02:00
ad6e3dd00d
make sure that 204-responses can be accepted 2020-04-22 11:46:15 +02:00
876f44ceff
advertise the stable branch 2020-04-22 11:31:45 +02:00
f70865d246
fix no schema having content 2020-04-22 11:29:23 +02:00
b6006797f4
update readme 2020-04-20 22:34:39 +02:00
8834f3f64b
fix import error 2020-04-19 22:27:34 +02:00
1e607bbcc9
more generous FromBody implementation 2020-04-19 22:26:29 +02:00
fdc34fc296
remove weird useless constraint 2020-04-19 20:49:47 +02:00
45cad64923
add ResourceResult impl for Result<AuthResult<T>, E> 2020-04-18 16:18:02 +02:00
523d01d443
async fn and conn are not compatible atm since diesel is completely sync 2020-04-18 15:48:00 +02:00
310d7f79d5
fix state ownership issue when using the database feature 2020-04-16 23:48:54 +02:00
63e6eb9b32
re-export gotham 2020-04-15 23:20:41 +02:00
a493071ff8
dependency management 2020-04-15 23:16:03 +02:00
40e6d1bc03
make clear that this tracks gotham master 2020-04-15 23:01:21 +02:00
694b45ea60
Merge branch 'master' into gotham-master
Conflicts:
	example/Cargo.toml
	gotham_restful/Cargo.toml
2020-04-15 22:55:26 +02:00
659fd2f7e2
bump version to 0.0.4 / derive 0.0.3 2020-04-15 21:50:38 +02:00
c3e2185396
remove some more expect/panic stuff 2020-04-15 21:41:24 +02:00
89f6494b51
asyncify method proc macro 2020-04-15 21:15:40 +02:00
427c836f52
expose async to handlers 2020-04-15 20:55:25 +02:00
d7282786b1
require all resource results to be sendable 2020-04-14 22:45:06 +02:00
06e6c93a46
fix openapi routing errors 2020-04-14 22:41:20 +02:00
a8ae939019
clean up async shit 2020-04-14 22:40:27 +02:00
ede0d75161
start moving shit over to async 2020-04-14 21:17:12 +02:00
f425f21ff3
update jsonwebtoken, futures, and hyper and co 2020-04-14 17:44:07 +02:00
fbcc626478
write tests for the openapi types 2020-04-13 02:46:01 +02:00
93cbc36046
dump codecov.io and redo badges 2020-04-12 22:31:53 +02:00
095686f390 Merge branch 'idiomatify-error-handling-code' into 'master'
Idiomatify error handling code in gotham_derive

See merge request msrd0/gotham-restful!10
2020-04-12 19:58:05 +00:00
d610103750 Idiomatify error handling code in gotham_derive 2020-04-12 19:58:05 +00:00
08a8f3557b
fix no accepted types result in no openapi content being generated
fixes #15
2020-04-11 20:34:51 +02:00
8d85893ca4 Merge branch 'auth-detection' into 'master'
auth is now per-method and not per-return-type

Closes #14

See merge request msrd0/gotham-restful!9
2020-04-11 18:13:36 +00:00
d10895076e auth is now per-method and not per-return-type 2020-04-11 18:13:36 +00:00
f6f16949a1 Merge branch 'improve-error-messages' into 'master'
Improve error messages

See merge request msrd0/gotham-restful!8
2020-04-11 17:36:24 +00:00
1d4d75c84a
proper error message for too many / too few parameters 2020-04-11 19:20:30 +02:00
6748130ff5
easier debugging 2020-04-08 22:18:06 +02:00
381a230b81
remove panics from expand_method 2020-04-08 22:07:33 +02:00
f677789747
remove panics from FromBody 2020-04-08 21:53:57 +02:00
2b8ad48504
remove panics from RequestBody 2020-04-07 23:01:26 +02:00
5954be324a
remove panics from derive(OpenapiType) 2020-04-07 22:54:23 +02:00
d8c2ffaa9d Merge branch 'allow-openapi-customizations' into 'master'
Allow OpenAPI customizations

See merge request msrd0/gotham-restful!7
2020-04-07 20:44:02 +00:00
810680d9b1 Allow OpenAPI customizations 2020-04-07 20:44:02 +00:00
75d2a8f557 Merge branch 'less-generic-type-arguments' into 'master'
Less generic type arguments

See merge request msrd0/gotham-restful!6
2020-04-06 16:20:08 +00:00
744f56acf9 Less generic type arguments 2020-04-06 16:20:08 +00:00
e0a1505d13
update readme 2020-04-05 23:17:53 +02:00
212fca738b
fix doc 2020-04-05 22:18:31 +02:00
1d1682a03d
remove the annoying ToString type when what we really need is a &str 2020-03-30 22:42:22 +02:00
c508ac878d
clean up some stuff ;; use Default::default() more often 2020-03-30 22:40:08 +02:00
4c50ea0959
releasing is hard, try 0.0.3 2020-02-24 20:05:35 +01:00
a57176529c
fix 2020-02-24 20:03:09 +01:00
5d730df90d
properly enable/disable errorlog 2020-02-24 19:19:21 +01:00
145 changed files with 8673 additions and 3874 deletions

View file

@ -1,12 +0,0 @@
codecov:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "0...100"
status:
project: no
patch: no
changes: no

View file

@ -1,58 +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:
- apk add --no-cache bash curl
- 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 'example/*' --ignore-panics --ignore-tests --out Xml -v - cargo test
- wget -qO- https://codecov.io/bash | bash -s -- -y .codecov.yml -X gcov
cache: 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:
paths:
- tarpaulin-report.html
reports:
cobertura: cobertura.xml
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
View 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

View file

@ -1,12 +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" }
openapi_type = { path = "./openapi_type" }
openapi_type_derive = { path = "./openapi_type_derive" }

View file

@ -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.

View file

@ -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)

View file

@ -1,96 +1,4 @@
# gotham_restful # Moved to GitHub
[![Build Status](https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg)](https://gitlab.com/msrd0/gotham-restful/commits/master) This project has moved to GitHub: https://github.com/msrd0/gotham_restful
[![Coverage Status](https://codecov.io/gl/msrd0/gotham-restful/branch/master/graph/badge.svg)](https://codecov.io/gl/msrd0/gotham-restful)
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
This crate targets stable rust, currently requiring rustc 1.40+. To use this crate, add the
following to your `Cargo.toml`:
```toml
[dependencies]
gotham_restful = "0.0.1"
```
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----

View file

@ -1,5 +1,61 @@
# {{crate}} <br/>
<div>
<a href="https://gitlab.com/msrd0/gotham-restful/pipelines">
<img alt="pipeline status" src="https://gitlab.com/msrd0/gotham-restful/badges/master/pipeline.svg"/>
</a>
<a href="https://msrd0.gitlab.io/gotham-restful/coverage.html">
<img alt="coverage report" src="https://gitlab.com/msrd0/gotham-restful/badges/master/coverage.svg"/>
</a>
<a href="https://msrd0.gitlab.io/gotham-restful/gotham_restful/index.html">
<img alt="rustdoc" src="https://img.shields.io/badge/docs-master-blue.svg"/>
</a>
<a href="https://blog.rust-lang.org/2020/12/31/Rust-1.49.0.html">
<img alt="Minimum Rust Version" src="https://img.shields.io/badge/rustc-1.49+-orange.svg"/>
</a>
<a href="https://deps.rs/repo/gitlab/msrd0/gotham-restful">
<img alt="dependencies" src="https://deps.rs/repo/gitlab/msrd0/gotham-restful/status.svg"/>
</a>
</div>
<br/>
{{badges}} This repository contains the following crates:
- **gotham_restful**
[![gotham_restful on crates.io](https://img.shields.io/crates/v/gotham_restful.svg)](https://crates.io/crates/gotham_restful)
[![gotham_restful on docs.rs](https://docs.rs/gotham_restful/badge.svg)](https://docs.rs/gotham_restful)
- **gotham_restful_derive**
[![gotham_restful_derive on crates.io](https://img.shields.io/crates/v/gotham_restful_derive.svg)](https://crates.io/crates/gotham_restful_derive)
[![gotham_restful_derive on docs.rs](https://docs.rs/gotham_restful_derive/badge.svg)](https://docs.rs/gotham_restful_derive)
- **openapi_type**
[![openapi_type on crates.io](https://img.shields.io/crates/v/openapi_type.svg)](https://crates.io/crates/openapi_type)
[![openapi_type on docs.rs](https://docs.rs/openapi_type/badge.svg)](https://docs.rs/crate/openapi_type)
- **openapi_type_derive**
[![openapi_type_derive on crates.io](https://img.shields.io/crates/v/openapi_type_derive.svg)](https://crates.io/crates/openapi_type_derive)
[![openapi_type_derive on docs.rs](https://docs.rs/openapi_type_derive/badge.svg)](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.
```

View file

@ -2,26 +2,28 @@
[package] [package]
name = "gotham_restful_derive" name = "gotham_restful_derive"
version = "0.0.2" 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
[badges] [badges]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" } gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
codecov = { repository = "msrd0/gotham-restful", branch = "master", service = "gitlab" }
[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
View file

@ -0,0 +1 @@
../LICENSE

590
derive/src/endpoint.rs Normal file
View 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
View 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
View 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)
}

View 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
})
}

View 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
View 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
})
}

View 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
View 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()
}

View file

@ -2,28 +2,23 @@
[package] [package]
name = "example" name = "example"
version = "0.0.2" 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.2", 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 }

View file

@ -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 { username: format!("{}{}", username, id) }.into() User {
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", "0.0.1", format!("http://{}", ADDR), |mut route| { ADDR,
route.resource::<Users, _>("users"); build_router(chain, pipelines, |route| {
route.resource::<Auth, _>("auth"); let info = OpenapiInfo {
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.get_openapi("openapi");
route.swagger_ui("");
}); });
})); })
);
println!("Gotham started on {} for testing", ADDR); println!("Gotham started on {} for testing", ADDR);
} }

View file

@ -1,48 +0,0 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "gotham_restful"
version = "0.0.2"
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" }
codecov = { repository = "msrd0/gotham-restful", branch = "master", service = "gitlab" }
[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.2" }
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]
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

View file

@ -1 +0,0 @@
../LICENSE-Apache

View file

@ -1 +0,0 @@
../LICENSE-EPL

View file

@ -1 +0,0 @@
../LICENSE.md

View file

@ -1 +0,0 @@
../README.md

View file

@ -1,188 +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
This crate targets stable rust, currently requiring rustc 1.40+. To use this crate, add the
following to your `Cargo.toml`:
```toml
[dependencies]
gotham_restful = "0.0.1"
```
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,
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::*;

View file

@ -1,3 +0,0 @@
pub mod router;
pub mod types;

View file

@ -1,605 +0,0 @@
use crate::{
resource::*,
result::*,
routing::*,
OpenapiSchema,
OpenapiType,
RequestBody,
ResourceType
};
use futures::future::ok;
use gotham::{
extractor::QueryStringExtractor,
handler::{Handler, HandlerFuture, NewHandler},
helpers::http::response::create_response,
pipeline::chain::PipelineHandleChain,
router::builder::*,
state::State
};
use hyper::Body;
use indexmap::IndexMap;
use log::error;
use mime::{Mime, APPLICATION_JSON, TEXT_PLAIN};
use openapiv3::{
APIKeyLocation, Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema,
SchemaKind, SecurityScheme, Server, StatusCode, Type
};
use serde::de::DeserializeOwned;
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 : ToString, Version : ToString, Url : ToString>(title : Title, version : Version, server_url : Url) -> Self
{
Self(OpenAPI {
openapi: "3.0.2".to_string(),
info: openapiv3::Info {
title: title.to_string(),
description: None,
terms_of_service: None,
contact: None,
license: None,
version: version.to_string()
},
servers: vec![Server {
url: server_url.to_string(),
description: None,
variables: None
}],
paths: Paths::new(),
components: None,
security: Vec::new(),
tags: Vec::new(),
external_docs: None
})
}
/// 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()),
example: None,
examples: IndexMap::new(),
encoding: IndexMap::new()
});
}
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(
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_default(), 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_default(), 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(),
summary: None,
description: None,
external_documentation: None,
operation_id: None, // TODO
parameters: params.into_params(),
request_body,
responses: Responses {
default: None,
responses
},
deprecated: false,
security,
servers: Vec::new()
}
}
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, T : ToString>(&mut self, path : T)
{
R::setup((self, path.to_string()));
}
}
impl<'a, C, P> DrawResourceRoutes for (&mut (&mut $implType<'a, C, P>, &mut OpenapiRouter), String)
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceReadAll<Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>()
}
fn read<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceRead<ID, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>()
}
fn search<Handler, Query, Res>(&mut self)
where
Query : ResourceType + DeserializeOwned + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/search", &self.1);
let mut item = (self.0).1.remove_path(&self.1);
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_query_params(Query::schema()), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).search::<Handler, Query, Res>()
}
fn create<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.post = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types(), Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>()
}
fn update_all<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types(), Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>()
}
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Body::supported_types(), Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>()
}
fn delete_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>()
}
fn delete<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>()
}
}
}
}
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(&params).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(&params).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(&params).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"}}"#);
}
}

View file

@ -1,312 +0,0 @@
#[cfg(feature = "chrono")]
use chrono::{
Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc
};
use indexmap::IndexMap;
use openapiv3::{
ArrayType, IntegerType, 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::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),*) => {$(
impl OpenapiType for $num_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType::default())))
}
}
)*}
}
num_types!(f32, f64);
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);
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()
}
}
#[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 OpenapiType for serde_json::Value
{
fn schema() -> OpenapiSchema
{
OpenapiSchema {
nullable: true,
name: None,
schema: SchemaKind::Any(Default::default()),
dependencies: Default::default()
}
}
}

View file

@ -1,75 +0,0 @@
use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType};
use gotham::{
router::response::extender::StaticResponseExtender,
state::{State, StateData}
};
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);
}
/// Handle a GET request on the Resource root.
pub trait ResourceReadAll<R : ResourceResult>
{
fn read_all(state : &mut State) -> R;
}
/// Handle a GET request on the Resource with an id.
pub trait ResourceRead<ID, R : ResourceResult>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static
{
fn read(state : &mut State, id : ID) -> R;
}
/// Handle a GET request on the Resource with additional search parameters.
pub trait ResourceSearch<Query : ResourceType, R : ResourceResult>
where
Query : ResourceType + DeserializeOwned + StateData + StaticResponseExtender
{
fn search(state : &mut State, query : Query) -> R;
}
/// Handle a POST request on the Resource root.
pub trait ResourceCreate<Body : RequestBody, R : ResourceResult>
{
fn create(state : &mut State, body : Body) -> R;
}
/// Handle a PUT request on the Resource root.
pub trait ResourceUpdateAll<Body : RequestBody, R : ResourceResult>
{
fn update_all(state : &mut State, body : Body) -> R;
}
/// Handle a PUT request on the Resource with an id.
pub trait ResourceUpdate<ID, Body : RequestBody, R : ResourceResult>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static
{
fn update(state : &mut State, id : ID, body : Body) -> R;
}
/// Handle a DELETE request on the Resource root.
pub trait ResourceDeleteAll<R : ResourceResult>
{
fn delete_all(state : &mut State) -> R;
}
/// Handle a DELETE request on the Resource with an id.
pub trait ResourceDelete<ID, R : ResourceResult>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static
{
fn delete(state : &mut State, id : ID) -> R;
}

View file

@ -1,587 +0,0 @@
use crate::{ResponseBody, StatusCode};
#[cfg(feature = "openapi")]
use crate::{OpenapiSchema, OpenapiType};
use hyper::Body;
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")]
fn requires_auth() -> bool
{
false
}
}
#[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()
}
}
}
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) => {
if cfg!(feature = "errorlog")
{
error!("The handler encountered an error: {}", 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;
# 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;
# 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()
}
#[cfg(feature = "openapi")]
fn requires_auth() -> bool
{
true
}
}
/**
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;
# 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());
}
}

View file

@ -1,486 +0,0 @@
use crate::{
resource::*,
result::{ResourceError, ResourceResult, Response},
RequestBody,
ResourceType,
StatusCode
};
#[cfg(feature = "openapi")]
use crate::OpenapiRouter;
use futures::{
future::{Future, err, ok},
stream::Stream
};
use gotham::{
extractor::QueryStringExtractor,
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 serde::de::DeserializeOwned;
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, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F)
where
F : FnOnce((&mut D, &mut OpenapiRouter)),
Title : ToString,
Version : ToString,
Url : ToString;
}
/// 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, T : ToString>(&mut self, path : T);
}
/// This trait allows to draw routes within an resource. Use this only inside the
/// `Resource::setup` method.
pub trait DrawResourceRoutes
{
fn read_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceReadAll<Res>;
fn read<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceRead<ID, Res>;
fn search<Handler, Query, Res>(&mut self)
where
Query : ResourceType + DeserializeOwned + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>;
fn create<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>;
fn update_all<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>;
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>;
fn delete_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>;
fn delete<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>;
}
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, Res>(state : State) -> Box<HandlerFuture>
where
Res : ResourceResult,
Handler : ResourceReadAll<Res>
{
to_handler_future(state, |state| Handler::read_all(state))
}
fn read_handler<Handler, ID, Res>(state : State) -> Box<HandlerFuture>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceRead<ID, Res>
{
let id = {
let path : &PathExtractor<ID> = PathExtractor::borrow_from(&state);
path.id.clone()
};
to_handler_future(state, |state| Handler::read(state, id))
}
fn search_handler<Handler, Query, Res>(mut state : State) -> Box<HandlerFuture>
where
Query : ResourceType + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>
{
let query = Query::take_from(&mut state);
to_handler_future(state, |state| Handler::search(state, query))
}
fn create_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture>
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
handle_with_body::<Body, _, _>(state, |state, body| Handler::create(state, body))
}
fn update_all_handler<Handler, Body, Res>(state : State) -> Box<HandlerFuture>
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
handle_with_body::<Body, _, _>(state, |state, body| Handler::update_all(state, body))
}
fn update_handler<Handler, ID, Body, Res>(state : State) -> Box<HandlerFuture>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
let id = {
let path : &PathExtractor<ID> = PathExtractor::borrow_from(&state);
path.id.clone()
};
handle_with_body::<Body, _, _>(state, |state, body| Handler::update(state, id, body))
}
fn delete_all_handler<Handler, Res>(state : State) -> Box<HandlerFuture>
where
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>
{
to_handler_future(state, |state| Handler::delete_all(state))
}
fn delete_handler<Handler, ID, Res>(state : State) -> Box<HandlerFuture>
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>
{
let id = {
let path : &PathExtractor<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, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F)
where
F : FnOnce((&mut Self, &mut OpenapiRouter)),
Title : ToString,
Version : ToString,
Url : ToString
{
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, T : ToString>(&mut self, path : T)
{
R::setup((self, path.to_string()));
}
}
#[allow(clippy::redundant_closure)] // doesn't work because of type parameters
impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, String)
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceReadAll<Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.get(&self.1)
.extend_route_matcher(matcher)
.to(|state| read_all_handler::<Handler, Res>(state));
}
fn read<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceRead<ID, Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.get(&format!("{}/:id", self.1))
.extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<ID>>()
.to(|state| read_handler::<Handler, ID, Res>(state));
}
fn search<Handler, Query, Res>(&mut self)
where
Query : ResourceType + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.get(&format!("{}/search", self.1))
.extend_route_matcher(matcher)
.with_query_string_extractor::<Query>()
.to(|state| search_handler::<Handler, Query, Res>(state));
}
fn create<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = Body::supported_types().into();
self.0.post(&self.1)
.extend_route_matcher(accept_matcher)
.extend_route_matcher(content_matcher)
.to(|state| create_handler::<Handler, Body, Res>(state));
}
fn update_all<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = 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, Body, Res>(state));
}
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
let accept_matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
let content_matcher : MaybeMatchContentTypeHeader = 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<ID>>()
.to(|state| update_handler::<Handler, ID, Body, Res>(state));
}
fn delete_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.delete(&self.1)
.extend_route_matcher(matcher)
.to(|state| delete_all_handler::<Handler, Res>(state));
}
fn delete<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>
{
let matcher : MaybeMatchAcceptHeader = Res::accepted_types().into();
self.0.delete(&format!("{}/:id", self.1))
.extend_route_matcher(matcher)
.with_path_extractor::<PathExtractor<ID>>()
.to(|state| delete_handler::<Handler, ID, Res>(state));
}
}
}
}
implDrawResourceRoutes!(RouterBuilder);
implDrawResourceRoutes!(ScopeBuilder);

View file

@ -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])
}
}

View file

@ -1 +0,0 @@
../LICENSE-Apache

View file

@ -1 +0,0 @@
../LICENSE-EPL

View file

@ -1 +0,0 @@
../LICENSE.md

View file

@ -1,59 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Fields,
ItemStruct,
parse_macro_input
};
pub fn expand_from_body(tokens : TokenStream) -> TokenStream
{
let krate = super::krate();
let input = parse_macro_input!(tokens as ItemStruct);
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() }))
},
_ => panic!("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())))
},
_ => panic!("FromBody can only be derived for structs with at most one field")
}
},
Fields::Unit => (quote!(), quote!(Self{}))
};
let output = 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)
}
}
};
output.into()
}

View file

@ -1,94 +0,0 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
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;
fn krate() -> TokenStream2
{
quote!(::gotham_restful)
}
#[proc_macro_derive(FromBody)]
pub fn derive_from_body(tokens : TokenStream) -> TokenStream
{
expand_from_body(tokens)
}
#[cfg(feature = "openapi")]
#[proc_macro_derive(OpenapiType)]
pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream
{
openapi_type::expand(tokens)
}
#[proc_macro_derive(RequestBody, attributes(supported_types))]
pub fn derive_request_body(tokens : TokenStream) -> TokenStream
{
expand_request_body(tokens)
}
#[proc_macro_derive(Resource, attributes(rest_resource))]
pub fn derive_resource(tokens : TokenStream) -> TokenStream
{
expand_resource(tokens)
}
#[proc_macro_attribute]
pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::ReadAll, attr, item)
}
#[proc_macro_attribute]
pub fn rest_read(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::Read, attr, item)
}
#[proc_macro_attribute]
pub fn rest_search(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::Search, attr, item)
}
#[proc_macro_attribute]
pub fn rest_create(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::Create, attr, item)
}
#[proc_macro_attribute]
pub fn rest_update_all(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::UpdateAll, attr, item)
}
#[proc_macro_attribute]
pub fn rest_update(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::Update, attr, item)
}
#[proc_macro_attribute]
pub fn rest_delete_all(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::DeleteAll, attr, item)
}
#[proc_macro_attribute]
pub fn rest_delete(attr : TokenStream, item : TokenStream) -> TokenStream
{
expand_method(Method::Delete, attr, item)
}

View file

@ -1,299 +0,0 @@
use heck::SnakeCase;
use proc_macro::TokenStream;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::{
Attribute,
FnArg,
ItemFn,
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("unknown method".to_string())
}
}
}
impl Method
{
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 setup_ident(&self, resource : String) -> 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,
ty : MethodArgumentType
}
fn interpret_arg_ty(index : usize, attrs : &[Attribute], name : &str, ty : Type) -> MethodArgumentType
{
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 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 MethodArgumentType::DatabaseConnection(match ty {
Type::Reference(ty) => *ty.elem,
ty => ty
});
}
if index == 0
{
return match ty {
Type::Reference(ty) => if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef },
_ => panic!("The first argument, unless some feature is used, has to be a (mutable) reference to gotham::state::State")
};
}
MethodArgumentType::MethodArg(ty)
}
fn interpret_arg(index : usize, arg : &PatType) -> MethodArgument
{
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());
MethodArgument { ident, ty }
}
pub fn expand_method(method : Method, attrs : TokenStream, item : TokenStream) -> TokenStream
{
let krate = super::krate();
let resource_ident = parse_macro_input!(attrs as Ident);
let fun = parse_macro_input!(item as ItemFn);
let fun_ident = &fun.sig.ident;
let fun_vis = &fun.vis;
let trait_ident = method.trait_ident();
let method_ident = method.fn_ident();
let setup_ident = method.setup_ident(resource_ident.to_string());
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 : Vec<MethodArgument> = fun.sig.inputs.iter().enumerate().map(|(i, arg)| match arg {
FnArg::Typed(arg) => interpret_arg(i, arg),
FnArg::Receiver(_) => panic!("didn't expect self parameter")
}).collect();
// extract the generic parameters to use
let mut generics : Vec<TokenStream2> = args.iter()
.filter(|arg| (*arg).ty.is_method_arg())
.map(|arg| arg.ty.quote_ty().unwrap())
.collect();
generics.push(quote!(#ret));
// 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_ident : #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,);
}
// put everything together
let output = quote! {
#fun
impl #krate::#trait_ident<#(#generics),*> for #resource_ident
where #where_clause
{
fn #method_ident(#(#args_def),*) -> #ret
{
#[allow(unused_imports)]
use #krate::export::{Future, FromState};
#block
}
}
#[deny(dead_code)]
#fun_vis fn #setup_ident<D : #krate::DrawResourceRoutes>(route : &mut D)
{
route.#method_ident::<#resource_ident, #(#generics),*>();
}
};
output.into()
}

View file

@ -1,196 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
Field,
Fields,
Generics,
GenericParam,
Item,
ItemEnum,
ItemStruct,
Variant,
parse_macro_input
};
pub fn expand(tokens : TokenStream) -> TokenStream
{
let input = parse_macro_input!(tokens as Item);
match input {
Item::Enum(item) => expand_enum(item),
Item::Struct(item) => expand_struct(item),
_ => panic!("derive(OpenapiType) not supported for this context")
}.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),*
}
}
}
fn expand_variant(variant : &Variant) -> TokenStream2
{
if variant.fields != Fields::Unit
{
panic!("Enum Variants with Fields not supported");
}
let ident = &variant.ident;
quote! {
enumeration.push(stringify!(#ident).to_string());
}
}
fn expand_enum(input : ItemEnum) -> TokenStream2
{
let krate = super::krate();
let ident = input.ident;
let generics = input.generics;
let where_clause = expand_where(&generics);
let variants : Vec<TokenStream2> = input.variants.iter().map(expand_variant).collect();
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(stringify!(#ident).to_string()),
nullable: false,
schema,
dependencies: Default::default()
}
}
}
}
}
fn expand_field(field : &Field) -> TokenStream2
{
let ident = match &field.ident {
Some(ident) => ident,
None => panic!("Fields without ident are not supported")
};
let ty = &field.ty;
quote! {{
let mut schema = <#ty>::schema();
if schema.nullable
{
schema.nullable = false;
}
else
{
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(name) => {
properties.insert(
stringify!(#ident).to_string(),
ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name) }
);
dependencies.insert(name, schema);
},
None => {
properties.insert(
stringify!(#ident).to_string(),
ReferenceOr::Item(Box::new(schema.into_schema()))
);
}
}
}}
}
pub fn expand_struct(input : ItemStruct) -> TokenStream2
{
let krate = super::krate();
let ident = input.ident;
let generics = input.generics;
let where_clause = expand_where(&generics);
let fields : Vec<TokenStream2> = match input.fields {
Fields::Named(fields) => {
fields.named.iter().map(|field| expand_field(field)).collect()
},
Fields::Unnamed(_) => panic!("Unnamed fields are not supported"),
Fields::Unit => Vec::new()
};
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(stringify!(#ident).to_string()),
nullable: false,
schema,
dependencies
}
}
}
}
}

View file

@ -1,90 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse::{Parse, ParseStream, Result as SynResult},
punctuated::Punctuated,
token::Comma,
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()
})))
}
}
}
}
pub fn expand_request_body(tokens : TokenStream) -> TokenStream
{
let krate = super::krate();
let input = parse_macro_input!(tokens as ItemStruct);
let ident = input.ident;
let generics = input.generics;
let types : Vec<Path> = input.attrs.into_iter().filter(|attr|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()) // TODO wtf
).flat_map(|attr| {
let m : MimeList = syn::parse2(attr.tokens).expect("unable to parse attributes");
m.0.into_iter()
}).collect();
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);
let output = quote! {
impl #generics #krate::RequestBody for #ident #generics
where #ident #generics : #krate::FromBody
{
fn supported_types() -> Option<Vec<#krate::Mime>>
{
#types
}
}
#impl_openapi_type
};
output.into()
}

View file

@ -1,61 +0,0 @@
use crate::method::Method;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
parse::{Parse, ParseStream, Result as SynResult},
punctuated::Punctuated,
token::Comma,
Ident,
ItemStruct,
parenthesized,
parse_macro_input
};
use std::str::FromStr;
struct MethodList(Punctuated<Ident, Comma>);
impl Parse for MethodList
{
fn parse(input: ParseStream) -> SynResult<Self>
{
let content;
let _paren = parenthesized!(content in input);
let list : Punctuated<Ident, Comma> = Punctuated::parse_separated_nonempty(&content)?;
Ok(Self(list))
}
}
pub fn expand_resource(tokens : TokenStream) -> TokenStream
{
let krate = super::krate();
let input = parse_macro_input!(tokens as ItemStruct);
let ident = input.ident;
let methods : Vec<TokenStream2> = input.attrs.into_iter().filter(|attr|
attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("rest_resource".to_string()) // TODO wtf
).flat_map(|attr| {
let m : MethodList = syn::parse2(attr.tokens).expect("unable to parse attributes");
m.0.into_iter()
}).map(|method| {
let method = Method::from_str(&method.to_string()).expect("unknown method");
let ident = method.setup_ident(ident.to_string());
quote!(#ident(&mut route);)
}).collect();
let output = quote! {
impl #krate::Resource for #ident
{
fn name() -> String
{
stringify!(#ident).to_string()
}
fn setup<D : #krate::DrawResourceRoutes>(mut route : D)
{
#(#methods)*
}
}
};
output.into()
}

27
openapi_type/Cargo.toml Normal file
View 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
View 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
View 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()
}
}

View 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);
}
}
}

View 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
}]
});

View file

@ -0,0 +1,6 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
enum Foo {}
fn main() {}

View 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 {}
| ^^

View file

@ -0,0 +1,12 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo {
bar: Bar
}
struct Bar;
fn main() {
Foo::schema();
}

View 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)

View file

@ -0,0 +1,12 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo<T> {
bar: T
}
struct Bar;
fn main() {
<Foo<Bar>>::schema();
}

View 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`

View 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

View file

@ -0,0 +1,6 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
struct Foo(i64, i64);
fn main() {}

View file

@ -0,0 +1,5 @@
error: #[derive(OpenapiType)] does not support tuple structs
--> $DIR/tuple_struct.rs:4:11
|
4 | struct Foo(i64, i64);
| ^^^^^^^^^^

View file

@ -0,0 +1,8 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
enum Foo {
Pair(i64, i64)
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: #[derive(OpenapiType)] does not support tuple variants
--> $DIR/tuple_variant.rs:5:6
|
5 | Pair(i64, i64)
| ^^^^^^^^^^

View file

@ -0,0 +1,9 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
union Foo {
signed: i64,
unsigned: u64
}
fn main() {}

View file

@ -0,0 +1,5 @@
error: #[derive(OpenapiType)] cannot be used on unions
--> $DIR/union.rs:4:1
|
4 | union Foo {
| ^^^^^

View file

@ -0,0 +1,7 @@
use openapi_type::OpenapiType;
#[derive(OpenapiType)]
#[openapi(pizza)]
struct Foo;
fn main() {}

View file

@ -0,0 +1,5 @@
error: Unexpected token
--> $DIR/unknown_attribute.rs:4:11
|
4 | #[openapi(pizza)]
| ^^^^^

View 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"
}
});

View file

@ -0,0 +1,7 @@
use trybuild::TestCases;
#[test]
fn trybuild() {
let t = TestCases::new();
t.compile_fail("tests/fail/*.rs");
}

View 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"

View 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()
}
)
)
}
}

View 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
}
}
}
})
}

View 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(())
}

View 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
View 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

View file

@ -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.
@ -36,8 +38,7 @@ 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()
} }
@ -146,13 +151,13 @@ fn main() {
); );
let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build());
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
route.resource::<AuthResource, _>("auth"); route.resource::<AuthResource>("auth");
})); }));
} }
``` ```
*/ */
pub struct AuthMiddleware<Data, Handler> #[derive(Debug)]
{ pub struct AuthMiddleware<Data, Handler> {
source: AuthSource, source: AuthSource,
validation: AuthValidation, validation: AuthValidation,
handler: Handler, handler: Handler,
@ -160,10 +165,10 @@ pub struct AuthMiddleware<Data, Handler>
} }
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(),
@ -178,8 +183,7 @@ 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(),
@ -194,8 +198,7 @@ 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(|| {
CookieParser::from_state(&state)
.get(&name)
.map(|cookie| cookie.value().to_owned()) .map(|cookie| cookie.value().to_owned())
}, }),
AuthSource::Header(name) => { AuthSource::Header(name) => HeaderMap::try_borrow_from(&state)
HeaderMap::try_borrow_from(&state)
.and_then(|map| map.get(name)) .and_then(|map| map.get(name))
.and_then(|header| header.to_str().ok()) .and_then(|header| header.to_str().ok())
.map(|value| value.to_owned()) .map(|value| value.to_owned()),
}, AuthSource::AuthorizationHeader => HeaderMap::try_borrow_from(&state)
AuthSource::AuthorizationHeader => {
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,7 +257,7 @@ where
}; };
// we found a valid token // we found a valid token
return AuthStatus::Authenticated(data); AuthStatus::Authenticated(data)
} }
} }
@ -266,9 +266,9 @@ 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,7 +278,7 @@ 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()
} }
} }
@ -288,18 +288,17 @@ where
{ {
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
@ -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();
@ -379,14 +371,14 @@ 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
);
}

View 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);
}

View 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"
}
]
}

View 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");
}

View 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"
}
}
}
}
}
}

View 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
View 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
View 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
View 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");
}

View 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