diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3db33c0..fb7c703 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,128 +6,75 @@ stages: variables: CARGO_HOME: $CI_PROJECT_DIR/cargo - RUST_LOG: info,gotham=debug,gotham_restful=trace - -check-example: - stage: test - image: rust:slim - before_script: - - cargo -V - script: - - cargo check --manifest-path example/Cargo.toml - cache: - key: cargo-stable-example - paths: - - cargo/ - - target/ test-default: stage: test - image: rust:1.49-slim + image: msrd0/rust:alpine-sweep before_script: - cargo -V + - cargo sweep -s script: - - cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild - - cargo test + - cargo test --workspace --doc + - cargo test --workspace --tests + - cargo test --workspace --tests -- --ignored + after_script: + - cargo sweep -f cache: - key: cargo-1-49-default + key: cargo-default paths: - cargo/ - target/ -test-full: +test-all: stage: test - image: rust:1.49-slim + image: msrd0/rust:alpine-tarpaulin-sweep before_script: - - apt update -y - - apt install -y --no-install-recommends libpq-dev + - apk add --no-cache postgresql-dev - cargo -V + - cargo sweep -s 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 + - cargo test --workspace --all-features --doc + - cargo test --workspace --tests -- --ignored + - cargo tarpaulin --target-dir target/tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v + after_script: + - cargo sweep -f 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 + key: cargo-all paths: - cargo/ - target/ readme: stage: test - image: ghcr.io/msrd0/cargo-readme - before_script: - - cargo readme -V + image: msrd0/cargo-readme script: - - cargo readme -t README.tpl -o README.md.new + - cargo readme -t README.tpl >README.md.new - diff README.md README.md.new -rustfmt: - stage: test - image: - name: alpine:3.13 - before_script: - - apk add rustup - - rustup-init -qy --default-host x86_64-unknown-linux-musl --default-toolchain none The documentation is located here' >target/doc/index.html + after_script: + - cargo sweep -f artifacts: paths: - target/doc/ cache: - key: cargo-stable-doc + key: cargo-doc paths: - cargo/ - target/ + only: + - master pages: stage: publish @@ -135,9 +82,24 @@ pages: script: - mv target/doc public - mv tarpaulin-report.html public/coverage.html - - echo 'The documentation is located here' >public/index.html artifacts: paths: - public only: - master + +publish: + stage: publish + image: msrd0/rust:alpine + before_script: + - cargo -V + - cargo login $CRATES_IO_TOKEN + script: + - cd gotham_restful_derive + - cargo publish + - sleep 1m + - cd ../gotham_restful + - cargo publish + - cd .. + only: + - tags diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index dc51188..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,48 +0,0 @@ -# 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`) 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 diff --git a/Cargo.toml b/Cargo.toml index 787b6da..1a28e4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,77 +1,60 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"] +members = ["derive", "example"] [package] name = "gotham_restful" -version = "0.3.0-dev" +version = "0.1.0-rc0" authors = ["Dominic Meiser "] 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" +license = "EPL-2.0 OR 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" +base64 = { version = "0.12.1", optional = true } +chrono = { version = "0.4.11", features = ["serde"], optional = true } +cookie = { version = "0.13.3", optional = true } +futures-core = "0.3.5" +futures-util = "0.3.5" +gotham = { version = "0.5.0-rc.1", default-features = false } +gotham_derive = "0.5.0-rc.1" +gotham_middleware_diesel = { version = "0.1.2", optional = true } +gotham_restful_derive = { version = "0.1.0-rc0" } +indexmap = { version = "1.3.2", optional = true } +itertools = "0.9.0" +jsonwebtoken = { version = "7.1.0", optional = true } log = "0.4.8" mime = "0.3.16" +openapiv3 = { version = "0.3.2", optional = true } 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 } +serde_json = "1.0.53" +uuid = { version = "0.8.1", 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 } +paste = "0.1.12" thiserror = "1.0.18" trybuild = "1.0.27" [features] -default = ["cors", "errorlog", "without-openapi"] -full = ["auth", "cors", "database", "errorlog", "openapi"] - +default = ["cors", "errorlog"] 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"] +database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] +openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] [package.metadata.docs.rs] -no-default-features = true -features = ["full"] +all-features = true [patch.crates-io] gotham_restful = { path = "." } gotham_restful_derive = { path = "./derive" } -openapi_type = { path = "./openapi_type" } -openapi_type_derive = { path = "./openapi_type_derive" } diff --git a/LICENSE b/LICENSE-Apache similarity index 100% rename from LICENSE rename to LICENSE-Apache diff --git a/LICENSE-EPL b/LICENSE-EPL new file mode 100644 index 0000000..d3087e4 --- /dev/null +++ b/LICENSE-EPL @@ -0,0 +1,277 @@ +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. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..50a1376 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,6 @@ +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) + \ No newline at end of file diff --git a/README.md b/README.md index 9c1e534..4de2465 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,263 @@ -# Moved to GitHub +
+

gotham-restful

+
+
+ + pipeline status + + + coverage report + + + crates.io + + + docs.rs + + + rustdoc + + + Minimum Rust Version + + + dependencies + +
+
-This project has moved to GitHub: https://github.com/msrd0/gotham_restful +**Note:** The `stable` branch contains some bugfixes against the last release. The `master` +branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be +compatible with the new future / async stuff. +This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to +create resources with assigned methods that aim to be a more convenient way of creating handlers +for requests. + +## Design Goals + +This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as +request/response bodies and does that in a RESTful way, this framework is probably a bad fit for +your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful +web server in Rust as convenient as possible with the least amount of boilerplate neccessary. + +## Methods + +Assuming you assign `/foobar` to your resource, you can implement the following methods: + +| Method Name | Required Arguments | HTTP Verb | HTTP Path | +| ----------- | ------------------ | --------- | ----------- | +| read_all | | GET | /foobar | +| read | id | GET | /foobar/:id | +| search | query | GET | /foobar/search | +| create | body | POST | /foobar | +| change_all | body | PUT | /foobar | +| change | id, body | PUT | /foobar/:id | +| remove_all | | DELETE | /foobar | +| remove | id | DELETE | /foobar/:id | + +Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A +simple example could look like this: + +```rust +/// Our RESTful resource. +#[derive(Resource)] +#[resource(read)] +struct FooResource; + +/// The return type of the foo read method. +#[derive(Serialize)] +struct Foo { + id: u64 +} + +/// The foo read method handler. +#[read(FooResource)] +fn read(id: u64) -> Success { + Foo { id }.into() +} +``` + +## Arguments + +Some methods require arguments. Those should be + * **id** Should be a deserializable json-primitive like `i64` or `String`. + * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. + * **query** Should be any deserializable object whose variables are json-primitives. It will + however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The + type needs to implement [`QueryStringExtractor`]. + +Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to +have an async handler (that is, the function that the method macro is invoked on is declared +as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement +`Sync` there is unfortunately no more convenient way. + +## Uploads and Downloads + +By default, every request body is parsed from json, and every respone is converted to json using +[serde_json]. However, you may also use raw bodies. This is an example where the request body +is simply returned as the response again, no json parsing involved: + +```rust +#[derive(Resource)] +#[resource(create)] +struct ImageResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage { + content: Vec, + content_type: Mime +} + +#[create(ImageResource)] +fn create(body : RawImage) -> Raw> { + Raw::new(body.content, body.content_type) +} +``` + +## Features + +To make life easier for common use-cases, this create offers a few features that might be helpful +when you implement your web server. + +### Authentication Feature + +In order to enable authentication support, enable the `auth` feature gate. This allows you to +register a middleware that can automatically check for the existence of an JWT authentication +token. Besides being supported by the method macros, it supports to lookup the required JWT secret +with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. +None of this is currently supported by gotham's own JWT middleware. + +A simple example that uses only a single secret could look like this: + +```rust +#[derive(Resource)] +#[resource(read)] +struct SecretResource; + +#[derive(Serialize)] +struct Secret { + id: u64, + intended_for: String +} + +#[derive(Deserialize, Clone)] +struct AuthData { + sub: String, + exp: u64 +} + +#[read(SecretResource)] +fn read(auth: AuthStatus, id: u64) -> AuthSuccess { + let intended_for = auth.ok()?.sub; + Ok(Secret { id, intended_for }) +} + +fn main() { + let auth: AuthMiddleware = 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::("secret"); + })); +} +``` + +### CORS Feature + +The cors feature allows an easy usage of this web server from other origins. By default, only +the `Access-Control-Allow-Methods` header is touched. To change the behaviour, add your desired +configuration as a middleware. + +A simple example that allows authentication from every origin (note that `*` always disallows +authentication), and every content type, could look like this: + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() { + // your handler +} + +fn main() { + let cors = CorsConfig { + origin: Origin::Copy, + headers: vec![CONTENT_TYPE], + max_age: 0, + credentials: true + }; + let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); + gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { + route.resource::("foo"); + })); +} +``` + +The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`] +for an example. + +### Database Feature + +The database feature allows an easy integration of [diesel] into your handler functions. Please +note however that due to the way gotham's diesel middleware implementation, it is not possible +to run async code while holding a database connection. If you need to combine async and database, +you'll need to borrow the connection from the [`State`] yourself and return a boxed future. + +A simple non-async example could look like this: + +```rust +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[derive(Queryable, Serialize)] +struct Foo { + id: i64, + value: String +} + +#[read_all(FooResource)] +fn read_all(conn: &PgConnection) -> QueryResult> { + foo::table.load(conn) +} + +type Repo = gotham_middleware_diesel::Repo; + +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::("foo"); + })); +} +``` + +## Examples + +There is a lack of good examples, but there is currently a collection of code in the [example] +directory, that might help you. Any help writing more examples is highly appreciated. + +## License + +Licensed under your option of: + - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) + - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) + + + [diesel]: https://diesel.rs/ + [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example + [gotham]: https://gotham.rs/ + [serde_json]: https://github.com/serde-rs/json#serde-json---- + [`CorsRoute`]: trait.CorsRoute.html + [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html + [`RequestBody`]: trait.RequestBody.html + [`State`]: ../gotham/state/struct.State.html diff --git a/README.tpl b/README.tpl index 1769315..c87a454 100644 --- a/README.tpl +++ b/README.tpl @@ -1,16 +1,24 @@ -
-
+
+

gotham-restful

+
+
-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 +**Note:** The `stable` branch contains some bugfixes against the last release. The `master` +branch currently tracks gotham's master branch and the next release will use gotham 0.5.0 and be +compatible with the new future / async stuff. {{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. -``` diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 58c877e..3511f73 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,14 +2,13 @@ [package] name = "gotham_restful_derive" -version = "0.3.0-dev" +version = "0.1.0-rc0" authors = ["Dominic Meiser "] edition = "2018" -description = "Derive macros for gotham_restful" -keywords = ["gotham", "rest", "restful", "web", "http"] -license = "Apache-2.0" +description = "RESTful additions for the gotham web framework - Derive" +keywords = ["gotham", "rest", "restful", "web", "http", "derive"] +license = "EPL-2.0 OR Apache-2.0" repository = "https://gitlab.com/msrd0/gotham-restful" -workspace = ".." [lib] proc-macro = true @@ -18,12 +17,10 @@ proc-macro = true gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] -once_cell = "1.5" -paste = "1.0" +heck = "0.3.1" proc-macro2 = "1.0.13" quote = "1.0.6" -regex = "1.4" -syn = { version = "1.0.22", features = ["full"] } +syn = "1.0.22" [features] default = [] diff --git a/derive/LICENSE b/derive/LICENSE deleted file mode 120000 index ea5b606..0000000 --- a/derive/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/derive/LICENSE-Apache b/derive/LICENSE-Apache new file mode 120000 index 0000000..0cd69a3 --- /dev/null +++ b/derive/LICENSE-Apache @@ -0,0 +1 @@ +../LICENSE-Apache \ No newline at end of file diff --git a/derive/LICENSE-EPL b/derive/LICENSE-EPL new file mode 120000 index 0000000..2004d06 --- /dev/null +++ b/derive/LICENSE-EPL @@ -0,0 +1 @@ +../LICENSE-EPL \ No newline at end of file diff --git a/derive/LICENSE.md b/derive/LICENSE.md new file mode 120000 index 0000000..7eabdb1 --- /dev/null +++ b/derive/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/derive/src/endpoint.rs b/derive/src/endpoint.rs deleted file mode 100644 index 457f8ee..0000000 --- a/derive/src/endpoint.rs +++ /dev/null @@ -1,590 +0,0 @@ -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, - uri: Option, - params: Option, - body: Option - } -} - -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 [](&mut self, span: Span, []: $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([]); - 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 { - 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 = Lazy::new(|| Regex::new(r#"(^|/):(?P[^/]+)(/|$)"#).unwrap()); - -impl EndpointType { - fn http_method(&self) -> Option { - 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 { - 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 { - 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 { - 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 { - 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) -> Option { - match operation_id { - Some(operation_id) => Some(quote! { - fn operation_id() -> Option { - Some(#operation_id.to_string()) - } - }), - None => None - } -} - -#[cfg(not(feature = "openapi"))] -fn expand_operation_id(_: Option) -> Option { - None -} - -fn expand_wants_auth(wants_auth: Option, 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 { - // 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 = None; - let mut wants_auth: Option = 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::>(); - 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 = 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 - ) -> ::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 { - let endpoint_type = match expand_endpoint_type(ty, attrs, &fun) { - Ok(code) => code, - Err(err) => err.to_compile_error() - }; - Ok(quote! { - #fun - #endpoint_type - }) -} diff --git a/derive/src/from_body.rs b/derive/src/from_body.rs index a4ec7b2..f7c04ad 100644 --- a/derive/src/from_body.rs +++ b/derive/src/from_body.rs @@ -1,65 +1,73 @@ 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}; +use syn::{ + spanned::Spanned, + Data, + DeriveInput, + Error, + Field, + Fields, + Ident, + Result, + Type +}; -struct ParsedFields { - fields: Vec<(Ident, Type)>, - named: bool +struct ParsedFields +{ + fields : Vec<(Ident, Type)>, + named : bool } -impl ParsedFields { - fn from_named(fields: I) -> Self +impl ParsedFields +{ + fn from_named(fields : I) -> Self where - I: Iterator + I : Iterator { let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect(); Self { fields, named: true } } - - fn from_unnamed(fields: I) -> Self + + fn from_unnamed(fields : I) -> Self where - I: Iterator + I : Iterator { - let fields = fields - .enumerate() - .map(|(i, field)| (format_ident!("arg{}", i), field.ty)) - .collect(); + let fields = fields.enumerate().map(|(i, field)| (format_ident!("arg{}", i), field.ty)).collect(); Self { fields, named: false } } - - fn from_unit() -> Self { - Self { - fields: Vec::new(), - named: false - } + + fn from_unit() -> Self + { + Self { fields: Vec::new(), named: false } } } -pub fn expand_from_body(input: DeriveInput) -> Result { +pub fn expand_from_body(input : DeriveInput) -> Result +{ 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"))?; - + }.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) { + + if let Some(body_field) = fields.fields.get(0) + { body_ident = body_field.0.clone(); let body_ty = &body_field.1; where_clause = quote! { @@ -72,8 +80,9 @@ pub fn expand_from_body(input: DeriveInput) -> Result { let #body_ident : #body_ty = #body_ident.into(); }; } - - if let Some(type_field) = fields.fields.get(1) { + + if let Some(type_field) = fields.fields.get(1) + { type_ident = type_field.0.clone(); let type_ty = &type_field.1; where_clause = quote! { @@ -85,8 +94,9 @@ pub fn expand_from_body(input: DeriveInput) -> Result { let #type_ident : #type_ty = #type_ident.into(); }; } - - for field in &fields.fields[min(2, fields.fields.len())..] { + + for field in &fields.fields[min(2, fields.fields.len())..] + { let field_ident = &field.0; let field_ty = &field.1; where_clause = quote! { @@ -98,20 +108,20 @@ pub fn expand_from_body(input: DeriveInput) -> Result { let #field_ident : #field_ty = Default::default(); }; } - - let field_names: Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect(); + + let field_names : Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect(); let ctor = if fields.named { 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 { #block diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 59ee8b6..7e0dc00 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,7 +1,3 @@ -#![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; @@ -9,121 +5,130 @@ use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Re mod util; -mod endpoint; -use endpoint::{expand_endpoint, EndpointType}; - mod from_body; use from_body::expand_from_body; - +mod method; +use method::{expand_method, Method}; mod request_body; use request_body::expand_request_body; - mod resource; use resource::expand_resource; - mod resource_error; use resource_error::expand_resource_error; - -mod private_openapi_trait; -use private_openapi_trait::expand_private_openapi_trait; +#[cfg(feature = "openapi")] +mod openapi_type; +#[cfg(feature = "openapi")] +use openapi_type::expand_openapi_type; #[inline] -fn print_tokens(tokens: TokenStream2) -> TokenStream { - // eprintln!("{}", tokens); +fn print_tokens(tokens : TokenStream2) -> TokenStream +{ + //eprintln!("{}", tokens); tokens.into() } #[inline] -fn expand_derive(input: TokenStream, expand: F) -> TokenStream +fn expand_derive(input : TokenStream, expand : F) -> TokenStream where - F: FnOnce(DeriveInput) -> Result + F : FnOnce(DeriveInput) -> Result { - print_tokens(expand(parse_macro_input!(input)).unwrap_or_else(|err| err.to_compile_error())) + print_tokens(expand(parse_macro_input!(input)) + .unwrap_or_else(|err| err.to_compile_error())) } #[inline] -fn expand_macro(attrs: TokenStream, item: TokenStream, expand: F) -> TokenStream +fn expand_macro(attrs : TokenStream, item : TokenStream, expand : F) -> TokenStream where - F: FnOnce(A, I) -> Result, - A: ParseMacroInput, - I: ParseMacroInput + F : FnOnce(A, I) -> Result, + A : ParseMacroInput, + I : ParseMacroInput { - print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)).unwrap_or_else(|err| err.to_compile_error())) + print_tokens(expand(parse_macro_input!(attrs), parse_macro_input!(item)) + .unwrap_or_else(|err| err.to_compile_error())) } #[inline] -fn krate() -> TokenStream2 { +fn krate() -> TokenStream2 +{ quote!(::gotham_restful) } #[proc_macro_derive(FromBody)] -pub fn derive_from_body(input: TokenStream) -> TokenStream { +pub fn derive_from_body(input : TokenStream) -> TokenStream +{ expand_derive(input, expand_from_body) } +#[cfg(feature = "openapi")] +#[proc_macro_derive(OpenapiType, attributes(openapi))] +pub fn derive_openapi_type(input : TokenStream) -> TokenStream +{ + expand_derive(input, expand_openapi_type) +} + #[proc_macro_derive(RequestBody, attributes(supported_types))] -pub fn derive_request_body(input: TokenStream) -> TokenStream { +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 { +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 { +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)) +pub fn read_all(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::ReadAll, 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)) +pub fn read(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::Read, 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)) +pub fn search(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::Search, 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)) +pub fn create(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::Create, 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)) +pub fn change_all(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::ChangeAll, 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)) +pub fn change(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::Change, 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)) +pub fn remove_all(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::RemoveAll, 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) +pub fn remove(attr : TokenStream, item : TokenStream) -> TokenStream +{ + expand_macro(attr, item, |attr, item| expand_method(Method::Remove, attr, item)) } diff --git a/derive/src/method.rs b/derive/src/method.rs new file mode 100644 index 0000000..be19e0b --- /dev/null +++ b/derive/src/method.rs @@ -0,0 +1,480 @@ +use crate::util::CollectToResult; +use heck::{CamelCase, SnakeCase}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{ + spanned::Spanned, + Attribute, + AttributeArgs, + Error, + FnArg, + ItemFn, + Lit, + LitBool, + Meta, + NestedMeta, + PatType, + Result, + ReturnType, + Type +}; +use std::str::FromStr; + +pub enum Method +{ + ReadAll, + Read, + Search, + Create, + ChangeAll, + Change, + RemoveAll, + Remove +} + +impl FromStr for Method +{ + type Err = Error; + + fn from_str(str : &str) -> Result + { + match str { + "ReadAll" | "read_all" => Ok(Self::ReadAll), + "Read" | "read" => Ok(Self::Read), + "Search" | "search" => Ok(Self::Search), + "Create" | "create" => Ok(Self::Create), + "ChangeAll" | "change_all" => Ok(Self::ChangeAll), + "Change" | "change" => Ok(Self::Change), + "RemoveAll" | "remove_all" => Ok(Self::RemoveAll), + "Remove" | "remove" => Ok(Self::Remove), + _ => Err(Error::new(Span::call_site(), format!("Unknown method: `{}'", str))) + } + } +} + +impl Method +{ + pub fn type_names(&self) -> Vec<&'static str> + { + use Method::*; + + match self { + ReadAll | RemoveAll => vec![], + Read | Remove => vec!["ID"], + Search => vec!["Query"], + Create | ChangeAll => vec!["Body"], + Change => vec!["ID", "Body"] + } + } + + pub fn trait_ident(&self) -> Ident + { + use Method::*; + + let name = match self { + ReadAll => "ReadAll", + Read => "Read", + Search => "Search", + Create => "Create", + ChangeAll => "ChangeAll", + Change => "Change", + RemoveAll => "RemoveAll", + Remove => "Remove" + }; + format_ident!("Resource{}", name) + } + + pub fn fn_ident(&self) -> Ident + { + use Method::*; + + let name = match self { + ReadAll => "read_all", + Read => "read", + Search => "search", + Create => "create", + ChangeAll => "change_all", + Change => "change", + RemoveAll => "remove_all", + Remove => "remove" + }; + format_ident!("{}", name) + } + + pub fn mod_ident(&self, resource : &str) -> Ident + { + format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident()) + } + + pub fn handler_struct_ident(&self, resource : &str) -> Ident + { + format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) + } + + pub fn setup_ident(&self, resource : &str) -> Ident + { + format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) + } +} + +#[allow(clippy::large_enum_variant)] +enum MethodArgumentType +{ + StateRef, + StateMutRef, + MethodArg(Type), + DatabaseConnection(Type), + AuthStatus(Type), + AuthStatusRef(Type) +} + +impl MethodArgumentType +{ + fn is_state_ref(&self) -> bool + { + matches!(self, Self::StateRef | Self::StateMutRef) + } + + fn is_method_arg(&self) -> bool + { + matches!(self, Self::MethodArg(_)) + } + + fn is_database_conn(&self) -> bool + { + matches!(self, Self::DatabaseConnection(_)) + } + + fn is_auth_status(&self) -> bool + { + matches!(self, Self::AuthStatus(_) | Self::AuthStatusRef(_)) + } + + fn ty(&self) -> Option<&Type> + { + match self { + Self::MethodArg(ty) | Self::DatabaseConnection(ty) | Self::AuthStatus(ty) | Self::AuthStatusRef(ty) => Some(ty), + _ => None + } + } + + fn quote_ty(&self) -> Option + { + self.ty().map(|ty| quote!(#ty)) + } +} + +struct MethodArgument +{ + ident : Ident, + ident_span : Span, + ty : MethodArgumentType +} + +impl Spanned for MethodArgument +{ + fn span(&self) -> Span + { + self.ident_span + } +} + +fn interpret_arg_ty(attrs : &[Attribute], name : &str, ty : Type) -> Result +{ + let attr = attrs.iter() + .find(|arg| arg.path.segments.iter().any(|path| &path.ident.to_string() == "rest_arg")) + .map(|arg| arg.tokens.to_string()); + + // TODO issue a warning for _state usage once diagnostics become stable + if attr.as_deref() == Some("state") || (attr.is_none() && (name == "state" || name == "_state")) + { + return match ty { + Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), + _ => Err(Error::new(ty.span(), "The state parameter has to be a (mutable) reference to gotham_restful::State")) + }; + } + + if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) + { + return Ok(match ty { + Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem), + ty => MethodArgumentType::AuthStatus(ty) + }); + } + + if cfg!(feature = "database") && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) + { + return Ok(MethodArgumentType::DatabaseConnection(match ty { + Type::Reference(ty) => *ty.elem, + ty => ty + })); + } + + Ok(MethodArgumentType::MethodArg(ty)) +} + +fn interpret_arg(index : usize, arg : &PatType) -> Result +{ + let pat = &arg.pat; + let ident = format_ident!("arg{}", index); + let orig_name = quote!(#pat); + let ty = interpret_arg_ty(&arg.attrs, &orig_name.to_string(), *arg.ty.clone())?; + + Ok(MethodArgument { ident, ident_span: arg.pat.span(), ty }) +} + +#[cfg(feature = "openapi")] +fn expand_operation_id(attrs : &[NestedMeta]) -> TokenStream +{ + let mut operation_id : Option<&Lit> = None; + for meta in attrs + { + if let NestedMeta::Meta(Meta::NameValue(kv)) = meta + { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) + { + operation_id = Some(&kv.lit) + } + } + } + + match operation_id { + Some(operation_id) => quote! { + fn operation_id() -> Option + { + Some(#operation_id.to_string()) + } + }, + None => quote!() + } +} + +#[cfg(not(feature = "openapi"))] +fn expand_operation_id(_ : &[NestedMeta]) -> TokenStream +{ + quote!() +} + +fn expand_wants_auth(attrs : &[NestedMeta], default : bool) -> TokenStream +{ + let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() }); + let mut wants_auth = &default_lit; + for meta in attrs + { + if let NestedMeta::Meta(Meta::NameValue(kv)) = meta + { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) + { + wants_auth = &kv.lit + } + } + } + + quote! { + fn wants_auth() -> bool + { + #wants_auth + } + } +} + +#[allow(clippy::comparison_chain)] +pub fn expand_method(method : Method, mut attrs : AttributeArgs, fun : ItemFn) -> Result +{ + let krate = super::krate(); + + // parse attributes + if attrs.len() < 1 + { + return Err(Error::new(Span::call_site(), "Missing Resource struct. Example: #[read_all(MyResource)]")); + } + let resource_path = match attrs.remove(0) { + NestedMeta::Meta(Meta::Path(path)) => path, + p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) + }; + let resource_name = resource_path.segments.last().map(|s| s.ident.to_string()) + .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; + + let fun_ident = &fun.sig.ident; + let fun_vis = &fun.vis; + let fun_is_async = fun.sig.asyncness.is_some(); + + if let Some(unsafety) = fun.sig.unsafety + { + return Err(Error::new(unsafety.span(), "Resource methods must not be unsafe")); + } + + let trait_ident = method.trait_ident(); + let method_ident = method.fn_ident(); + let mod_ident = method.mod_ident(&resource_name); + let handler_ident = method.handler_struct_ident(&resource_name); + let setup_ident = method.setup_ident(&resource_name); + + let (ret, is_no_content) = match &fun.sig.output { + ReturnType::Default => (quote!(#krate::NoContent), true), + ReturnType::Type(_, ty) => (quote!(#ty), false) + }; + + // some default idents we'll need + let state_ident = format_ident!("state"); + let repo_ident = format_ident!("repo"); + let conn_ident = format_ident!("conn"); + let auth_ident = format_ident!("auth"); + let res_ident = format_ident!("res"); + + // extract arguments into pattern, ident and type + let args = fun.sig.inputs.iter() + .enumerate() + .map(|(i, arg)| match arg { + FnArg::Typed(arg) => interpret_arg(i, arg), + FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter")) + }) + .collect_to_result()?; + + // extract the generic parameters to use + let ty_names = method.type_names(); + let ty_len = ty_names.len(); + let generics_args : Vec<&MethodArgument> = args.iter() + .filter(|arg| (*arg).ty.is_method_arg()) + .collect(); + if generics_args.len() > ty_len + { + return Err(Error::new(generics_args[ty_len].span(), "Too many arguments")); + } + else if generics_args.len() < ty_len + { + return Err(Error::new(fun_ident.span(), "Too few arguments")); + } + let generics : Vec = generics_args.iter() + .map(|arg| arg.ty.quote_ty().unwrap()) + .zip(ty_names) + .map(|(arg, name)| { + let ident = format_ident!("{}", name); + quote!(type #ident = #arg;) + }) + .collect(); + + // extract the definition of our method + let mut args_def : Vec = args.iter() + .filter(|arg| (*arg).ty.is_method_arg()) + .map(|arg| { + let ident = &arg.ident; + let ty = arg.ty.quote_ty(); + quote!(#ident : #ty) + }).collect(); + args_def.insert(0, quote!(mut #state_ident : #krate::State)); + + // extract the arguments to pass over to the supplied method + let args_pass : Vec = args.iter().map(|arg| match (&arg.ty, &arg.ident) { + (MethodArgumentType::StateRef, _) => quote!(&#state_ident), + (MethodArgumentType::StateMutRef, _) => quote!(&mut #state_ident), + (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), + (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), + (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), + (MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident) + }).collect(); + + // prepare the method block + let mut block = quote!(#fun_ident(#(#args_pass),*)); + let mut state_block = quote!(); + if fun_is_async + { + if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_state_ref()) + { + return Err(Error::new(arg.span(), "async fn must not take &State as an argument as State is not Sync, consider boxing")); + } + block = quote!(#block.await); + } + if is_no_content + { + block = quote!(#block; Default::default()) + } + if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_database_conn()) + { + if fun_is_async + { + return Err(Error::new(arg.span(), "async fn is not supported when database support is required, consider boxing")); + } + let conn_ty = arg.ty.quote_ty(); + state_block = quote! { + #state_block + let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone(); + }; + block = quote! { + { + let #res_ident = #repo_ident.run::<_, (#krate::State, #ret), ()>(move |#conn_ident| { + let #res_ident = { #block }; + Ok((#state_ident, #res_ident)) + }).await.unwrap(); + #state_ident = #res_ident.0; + #res_ident.1 + } + }; + } + if let Some(arg) = args.iter().find(|arg| (*arg).ty.is_auth_status()) + { + let auth_ty = arg.ty.quote_ty(); + state_block = quote! { + #state_block + let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(&#state_ident).clone(); + }; + } + + // prepare the where clause + let mut where_clause = quote!(#resource_path : #krate::Resource,); + for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) + { + let auth_ty = arg.ty.quote_ty(); + where_clause = quote!(#where_clause #auth_ty : Clone,); + } + + // attribute generated code + let operation_id = expand_operation_id(&attrs); + let wants_auth = expand_wants_auth(&attrs, args.iter().any(|arg| (*arg).ty.is_auth_status())); + + // put everything together + Ok(quote! { + #fun + + #fun_vis mod #mod_ident + { + use super::*; + + struct #handler_ident; + + impl #krate::ResourceMethod for #handler_ident + { + type Res = #ret; + + #operation_id + #wants_auth + } + + impl #krate::#trait_ident for #handler_ident + where #where_clause + { + #(#generics)* + + fn #method_ident(#(#args_def),*) -> std::pin::Pin + Send>> + { + #[allow(unused_imports)] + use #krate::{export::FutureExt, FromState}; + + #state_block + + async move { + let #res_ident = { #block }; + (#state_ident, #res_ident) + }.boxed() + } + } + + #[deny(dead_code)] + pub fn #setup_ident(route : &mut D) + { + route.#method_ident::<#handler_ident>(); + } + + } + }) +} diff --git a/derive/src/openapi_type.rs b/derive/src/openapi_type.rs new file mode 100644 index 0000000..57a7ceb --- /dev/null +++ b/derive/src/openapi_type.rs @@ -0,0 +1,282 @@ +use crate::util::{CollectToResult, remove_parens}; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{ + parse_macro_input, + spanned::Spanned, + Attribute, + AttributeArgs, + Data, + DataEnum, + DataStruct, + DeriveInput, + Error, + Field, + Fields, + Generics, + GenericParam, + Lit, + LitStr, + Meta, + NestedMeta, + Result, + Variant +}; + +pub fn expand_openapi_type(input : DeriveInput) -> Result +{ + match (input.ident, input.generics, input.attrs, input.data) { + (ident, generics, attrs, Data::Enum(inum)) => expand_enum(ident, generics, attrs, inum), + (ident, generics, attrs, Data::Struct(strukt)) => expand_struct(ident, generics, attrs, strukt), + (_, _, _, Data::Union(uni)) => Err(Error::new(uni.union_token.span(), "#[derive(OpenapiType)] only works for structs and enums")) + } +} + +fn expand_where(generics : &Generics) -> TokenStream +{ + if generics.params.is_empty() + { + return quote!(); + } + + let krate = super::krate(); + let idents = generics.params.iter() + .map(|param| match param { + GenericParam::Type(ty) => Some(ty.ident.clone()), + _ => None + }) + .filter(|param| param.is_some()) + .map(|param| param.unwrap()); + + quote! { + where #(#idents : #krate::OpenapiType),* + } +} + +#[derive(Debug, Default)] +struct Attrs +{ + nullable : bool, + rename : Option +} + +fn to_string(lit : &Lit) -> Result +{ + match lit { + Lit::Str(str) => Ok(str.value()), + _ => Err(Error::new(lit.span(), "Expected string literal")) + } +} + +fn to_bool(lit : &Lit) -> Result +{ + match lit { + Lit::Bool(bool) => Ok(bool.value), + _ => Err(Error::new(lit.span(), "Expected bool")) + } +} + +fn parse_attributes(input : &[Attribute]) -> Result +{ + let mut parsed = Attrs::default(); + for attr in input + { + if attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("openapi".to_owned()) + { + let tokens = remove_parens(attr.tokens.clone()); + // TODO this is not public api but syn currently doesn't offer another convenient way to parse AttributeArgs + let nested = parse_macro_input::parse::(tokens.into())?; + for meta in nested + { + match &meta { + NestedMeta::Meta(Meta::NameValue(kv)) => match kv.path.segments.last().map(|s| s.ident.to_string()) { + Some(key) => match key.as_ref() { + "nullable" => parsed.nullable = to_bool(&kv.lit)?, + "rename" => parsed.rename = Some(to_string(&kv.lit)?), + _ => return Err(Error::new(kv.path.span(), "Unknown key")), + }, + _ => return Err(Error::new(meta.span(), "Unexpected token")) + }, + _ => return Err(Error::new(meta.span(), "Unexpected token")) + } + } + } + } + Ok(parsed) +} + +fn expand_variant(variant : &Variant) -> Result +{ + if variant.fields != Fields::Unit + { + return Err(Error::new(variant.span(), "#[derive(OpenapiType)] does not support enum variants with fields")); + } + + let ident = &variant.ident; + + let attrs = parse_attributes(&variant.attrs)?; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + Ok(quote! { + enumeration.push(#name.to_string()); + }) +} + +fn expand_enum(ident : Ident, generics : Generics, attrs : Vec, input : DataEnum) -> Result +{ + let krate = super::krate(); + let where_clause = expand_where(&generics); + + let attrs = parse_attributes(&attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + let variants = input.variants.iter() + .map(expand_variant) + .collect_to_result()?; + + Ok(quote! { + impl #generics #krate::OpenapiType for #ident #generics + #where_clause + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{export::openapi::*, OpenapiSchema}; + + let mut enumeration : Vec = Vec::new(); + + #(#variants)* + + let schema = SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Empty, + enumeration, + ..Default::default() + })); + + OpenapiSchema { + name: Some(#name.to_string()), + nullable: #nullable, + schema, + dependencies: Default::default() + } + } + } + }) +} + +fn expand_field(field : &Field) -> Result +{ + let ident = match &field.ident { + Some(ident) => ident, + None => return Err(Error::new(field.span(), "#[derive(OpenapiType)] does not support fields without an ident")) + }; + let ident_str = LitStr::new(&ident.to_string(), ident.span()); + let ty = &field.ty; + + let attrs = parse_attributes(&field.attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + Ok(quote! {{ + let mut schema = <#ty>::schema(); + + if schema.nullable + { + schema.nullable = false; + } + else if !#nullable + { + required.push(#ident_str.to_string()); + } + + let keys : Vec = schema.dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = schema.dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + dependencies.insert(dep, dep_schema); + } + } + + match schema.name.clone() { + Some(schema_name) => { + properties.insert( + #name.to_string(), + ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) } + ); + dependencies.insert(schema_name, schema); + }, + None => { + properties.insert( + #name.to_string(), + ReferenceOr::Item(Box::new(schema.into_schema())) + ); + } + } + }}) +} + +fn expand_struct(ident : Ident, generics : Generics, attrs : Vec, input : DataStruct) -> Result +{ + let krate = super::krate(); + let where_clause = expand_where(&generics); + + let attrs = parse_attributes(&attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + let fields : Vec = match input.fields { + Fields::Named(named_fields) => { + named_fields.named.iter() + .map(expand_field) + .collect_to_result()? + }, + Fields::Unnamed(fields) => return Err(Error::new(fields.span(), "#[derive(OpenapiType)] does not support unnamed fields")), + Fields::Unit => Vec::new() + }; + + Ok(quote!{ + impl #generics #krate::OpenapiType for #ident #generics + #where_clause + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{export::{openapi::*, IndexMap}, OpenapiSchema}; + + let mut properties : IndexMap>> = IndexMap::new(); + let mut required : Vec = Vec::new(); + let mut dependencies : IndexMap = IndexMap::new(); + + #(#fields)* + + let schema = SchemaKind::Type(Type::Object(ObjectType { + properties, + required, + additional_properties: None, + min_properties: None, + max_properties: None + })); + + OpenapiSchema { + name: Some(#name.to_string()), + nullable: #nullable, + schema, + dependencies + } + } + } + }) +} diff --git a/derive/src/private_openapi_trait.rs b/derive/src/private_openapi_trait.rs deleted file mode 100644 index d590c7e..0000000 --- a/derive/src/private_openapi_trait.rs +++ /dev/null @@ -1,171 +0,0 @@ -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, - non_openapi_bound: Vec, - other_attrs: Vec -} - -impl TraitItemAttrs { - fn parse(attrs: Vec) -> Result { - 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 { - 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 - }) -} diff --git a/derive/src/request_body.rs b/derive/src/request_body.rs index c543dfa..76c80aa 100644 --- a/derive/src/request_body.rs +++ b/derive/src/request_body.rs @@ -6,82 +6,78 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, - DeriveInput, Error, Generics, Path, Result, Token + DeriveInput, + Error, + Generics, + Path, + Result, + Token }; struct MimeList(Punctuated); -impl Parse for MimeList { - fn parse(input: ParseStream<'_>) -> Result { +impl Parse for MimeList +{ + fn parse(input: ParseStream) -> Result + { let list = Punctuated::parse_separated_nonempty(&input)?; Ok(Self(list)) } } #[cfg(not(feature = "openapi"))] -fn impl_openapi_type(_ident: &Ident, _generics: &Generics) -> TokenStream { +fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream +{ quote!() } #[cfg(feature = "openapi")] -fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream { +fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream +{ let krate = super::krate(); - let openapi = quote!(#krate::private::openapi); - + quote! { - impl #generics #krate::private::OpenapiType for #ident #generics + impl #generics #krate::OpenapiType for #ident #generics { - fn schema() -> #krate::private::OpenapiSchema + fn schema() -> #krate::OpenapiSchema { - #krate::private::OpenapiSchema::new( - #openapi::SchemaKind::Type( - #openapi::Type::String( - #openapi::StringType { - format: #openapi::VariantOrUnknownOrEmpty::Item( - #openapi::StringFormat::Binary - ), - .. ::std::default::Default::default() - } - ) - ) - ) + use #krate::{export::openapi::*, OpenapiSchema}; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) } } } } -pub fn expand_request_body(input: DeriveInput) -> Result { +pub fn expand_request_body(input : DeriveInput) -> Result +{ 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()) - }) + + 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::() .map(|list| Box::new(list.0.into_iter().map(Ok)) as Box>>) .unwrap_or_else(|mut err| { - err.combine(Error::new( - span, - "Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]" - )); + err.combine(Error::new(span, "Hint: Types list should look like #[supported_types(TEXT_PLAIN, APPLICATION_JSON)]")); Box::new(iter::once(Err(err))) }) }) .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 @@ -91,7 +87,7 @@ pub fn expand_request_body(input: DeriveInput) -> Result { #types } } - + #impl_openapi_type }) } diff --git a/derive/src/resource.rs b/derive/src/resource.rs index 8e30275..79482e6 100644 --- a/derive/src/resource.rs +++ b/derive/src/resource.rs @@ -1,21 +1,23 @@ -use crate::{ - endpoint::endpoint_ident, - util::{CollectToResult, PathEndsWith} -}; +use crate::{method::Method, util::CollectToResult}; use proc_macro2::{Ident, TokenStream}; use quote::quote; -use std::iter; use syn::{ parenthesized, parse::{Parse, ParseStream}, punctuated::Punctuated, - DeriveInput, Result, Token + DeriveInput, + Error, + Result, + Token }; +use std::{iter, str::FromStr}; struct MethodList(Punctuated); -impl Parse for MethodList { - fn parse(input: ParseStream<'_>) -> Result { +impl Parse for MethodList +{ + fn parse(input: ParseStream) -> Result + { let content; let _paren = parenthesized!(content in input); let list = Punctuated::parse_separated_nonempty(&content)?; @@ -23,25 +25,27 @@ impl Parse for MethodList { } } -pub fn expand_resource(input: DeriveInput) -> Result { +pub fn expand_resource(input : DeriveInput) -> Result +{ 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>>, - Err(err) => Box::new(iter::once(Err(err))) - }) - .collect_to_result()?; - - let non_openapi_impl = quote! { + let name = ident.to_string(); + + let methods = input.attrs.into_iter().filter(|attr| + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("resource".to_string()) // TODO wtf + ).map(|attr| { + syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter()) + }).flat_map(|list| match list { + Ok(iter) => Box::new(iter.map(|method| { + let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; + let mod_ident = method.mod_ident(&name); + let ident = method.setup_ident(&name); + Ok(quote!(#mod_ident::#ident(&mut route);)) + })) as Box>>, + Err(err) => Box::new(iter::once(Err(err))) + }).collect_to_result()?; + + Ok(quote! { impl #krate::Resource for #ident { fn setup(mut route : D) @@ -49,22 +53,5 @@ pub fn expand_resource(input: DeriveInput) -> Result { #(#methods)* } } - }; - let openapi_impl = if !cfg!(feature = "openapi") { - None - } else { - Some(quote! { - impl #krate::ResourceWithSchema for #ident - { - fn setup(mut route : D) - { - #(#methods)* - } - } - }) - }; - Ok(quote! { - #non_openapi_impl - #openapi_impl }) } diff --git a/derive/src/resource_error.rs b/derive/src/resource_error.rs index 9239c80..032151b 100644 --- a/derive/src/resource_error.rs +++ b/derive/src/resource_error.rs @@ -1,54 +1,68 @@ -use crate::util::{remove_parens, CollectToResult}; +use crate::util::{CollectToResult, remove_parens}; 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, + spanned::Spanned, + Attribute, + Data, + DeriveInput, + Error, + Fields, + GenericParam, + LitStr, + Path, + PathSegment, + Result, + Type, Variant }; -struct ErrorVariantField { - attrs: Vec, - ident: Ident, - ty: Type + +struct ErrorVariantField +{ + attrs : Vec, + ident : Ident, + ty : Type } -struct ErrorVariant { - ident: Ident, - status: Option, - is_named: bool, - fields: Vec, - from_ty: Option<(usize, Type)>, - display: Option +struct ErrorVariant +{ + ident : Ident, + status : Option, + is_named : bool, + fields : Vec, + from_ty : Option<(usize, Type)>, + display : Option } -fn process_variant(variant: Variant) -> Result { - 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 - }; - +fn process_variant(variant : Variant) -> Result +{ + 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 { + 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"))?, + 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() { + for (i, field) in unnamed.unnamed.into_iter().enumerate() + { fields.push(ErrorVariantField { attrs: field.attrs, ident: format_ident!("arg{}", i), @@ -58,25 +72,19 @@ fn process_variant(variant: Variant) -> Result { }, Fields::Unit => {} } - - let from_ty = fields - .iter() + + 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())) - }) + .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()) - }) { + + 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, @@ -87,15 +95,18 @@ fn process_variant(variant: Variant) -> Result { }) } -fn path_segment(name: &str) -> PathSegment { +fn path_segment(name : &str) -> PathSegment +{ PathSegment { ident: format_ident!("{}", name), arguments: Default::default() } } -impl ErrorVariant { - fn fields_pat(&self) -> TokenStream { +impl ErrorVariant +{ + fn fields_pat(&self) -> TokenStream + { let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); if fields.peek().is_none() { quote!() @@ -105,90 +116,74 @@ impl ErrorVariant { quote!( ( #( #fields ),* ) ) } } - - fn to_display_match_arm(&self, formatter_ident: &Ident, enum_ident: &Ident) -> Result { + + fn to_display_match_arm(&self, formatter_ident : &Ident, enum_ident : &Ident) -> Result + { let ident = &self.ident; - let display = self - .display - .as_ref() - .ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; - + let display = self.display.as_ref().ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; + // lets find all required format parameters let display_str = display.value(); - let mut params: Vec<&str> = Vec::new(); + let mut params : Vec<&str> = Vec::new(); let len = display_str.len(); let mut start = len; let mut iter = display_str.chars().enumerate().peekable(); - while let Some((i, c)) = iter.next() { + while let Some((i, c)) = iter.next() + { // we found a new opening brace - if start == len && c == '{' { + if start == len && c == '{' + { start = i + 1; } // we found a duplicate opening brace - else if start == i && c == '{' { + else if start == i && c == '{' + { start = len; } // we found a closing brace - else if start < i && c == '}' { + 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" - )) - }, + 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" - )); + 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" - )); + 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 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 { + + fn into_match_arm(self, krate : &TokenStream, enum_ident : &Ident) -> Result + { 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 { + 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() + segments: vec![path_segment("gotham_restful"), path_segment("gotham"), path_segment("hyper"), path_segment("StatusCode"), status_ident].into_iter().collect() } - } else { - status } + else { status } }); - + // the response will come directly from the from_ty if present let res = match (self.from_ty, status) { (Some((from_index, _)), None) => { @@ -196,20 +191,21 @@ impl ErrorVariant { quote!(#from_field.into_response_error()) }, (Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")), - (None, Some(status)) => quote!(Ok(#krate::Response::new( - { #status }.into(), - #krate::gotham::hyper::Body::empty(), - None - ))), + (None, Some(status)) => quote!(Ok(#krate::Response { + status: { #status }.into(), + body: #krate::gotham::hyper::Body::empty(), + mime: 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 { + + fn were(&self) -> Option + { match self.from_ty.as_ref() { Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), None => None @@ -217,22 +213,22 @@ impl ErrorVariant { } } -pub fn expand_resource_error(input: DeriveInput) -> Result { +pub fn expand_resource_error(input : DeriveInput) -> Result +{ 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 { + }.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 } else { let were = generics.params.iter().filter_map(|param| match param { GenericParam::Type(ty) => { let ident = &ty.ident; @@ -241,8 +237,7 @@ pub fn expand_resource_error(input: DeriveInput) -> Result { _ => None }); let formatter_ident = format_ident!("resource_error_display_formatter"); - let match_arms = variants - .iter() + let match_arms = variants.iter() .map(|v| v.to_display_match_arm(&formatter_ident, &ident)) .collect_to_result()?; Some(quote! { @@ -258,39 +253,34 @@ pub fn expand_resource_error(input: DeriveInput) -> Result { } }) }; - - let mut from_impls: Vec = Vec::new(); - - for var in &variants { + + let mut from_impls : Vec = 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() + 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() + 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 ),* @@ -303,21 +293,20 @@ pub fn expand_resource_error(input: DeriveInput) -> Result { } }); } - + let were = variants.iter().filter_map(|variant| variant.were()).collect::>(); - let variants = variants - .into_iter() + let variants = variants.into_iter() .map(|variant| variant.into_match_arm(&krate, &ident)) - .collect_to_result()?; - + .collect_to_result()?; + Ok(quote! { #display_impl - + impl #generics #krate::IntoResponseError for #ident #generics where #( #were ),* { - type Err = #krate::private::serde_json::Error; - + type Err = #krate::export::serde_json::Error; + fn into_response_error(self) -> Result<#krate::Response, Self::Err> { match self { @@ -325,7 +314,7 @@ pub fn expand_resource_error(input: DeriveInput) -> Result { } } } - + #( #from_impls )* }) } diff --git a/derive/src/util.rs b/derive/src/util.rs index ef55659..d82dc31 100644 --- a/derive/src/util.rs +++ b/derive/src/util.rs @@ -1,70 +1,41 @@ use proc_macro2::{Delimiter, TokenStream, TokenTree}; use std::iter; -use syn::{Error, Lit, LitBool, LitStr, Path, Result}; +use syn::Error; -pub(crate) trait CollectToResult { +pub trait CollectToResult +{ type Item; - - fn collect_to_result(self) -> Result>; + + fn collect_to_result(self) -> Result, Error>; } impl CollectToResult for I where - I: Iterator> + I : Iterator> { type Item = Item; - - fn collect_to_result(self) -> Result> { - 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) - } + + fn collect_to_result(self) -> Result, Error> + { + self.fold(, Error>>::Ok(Vec::new()), |res, code| { + match (code, res) { + (Ok(code), Ok(mut codes)) => { codes.push(code); Ok(codes) }, + (Ok(_), Err(errors)) => Err(errors), + (Err(err), Ok(_)) => Err(err), + (Err(err), Err(mut errors)) => { errors.combine(err); Err(errors) } + } }) } } -pub(crate) trait ExpectLit { - fn expect_bool(self) -> Result; - fn expect_str(self) -> Result; -} -impl ExpectLit for Lit { - fn expect_bool(self) -> Result { - match self { - Self::Bool(bool) => Ok(bool), - _ => Err(Error::new(self.span(), "Expected boolean literal")) - } - } - - fn expect_str(self) -> Result { - 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 { +pub 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 { + if let TokenTree::Group(group) = &tt + { + if group.delimiter() == Delimiter::Parenthesis + { return Box::new(group.stream().into_iter()) as Box>; } } diff --git a/example/Cargo.toml b/example/Cargo.toml index 398a5ef..de21cba 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -2,23 +2,22 @@ [package] name = "example" -version = "0.0.0" +version = "0.0.1" authors = ["Dominic Meiser "] edition = "2018" license = "Unlicense" readme = "README.md" +include = ["src/**/*", "Cargo.toml", "LICENSE"] repository = "https://gitlab.com/msrd0/gotham-restful" -publish = false -workspace = ".." [badges] gitlab = { repository = "msrd0/gotham-restful", branch = "master" } [dependencies] fake = "2.2.2" -gotham = { version = "0.5.0", default-features = false } -gotham_derive = "0.5.0" -gotham_restful = { version = "0.2.0", features = ["auth", "cors", "openapi"], default-features = false } +gotham = { version = "0.5.0-rc.1", default-features = false } +gotham_derive = "0.5.0-rc.1" +gotham_restful = { version = "0.1.0-rc0", features = ["auth", "openapi"] } log = "0.4.8" -pretty_env_logger = "0.4" +log4rs = { version = "0.12.0", features = ["console_appender"], default-features = false } serde = "1.0.110" diff --git a/example/src/main.rs b/example/src/main.rs index e85f911..f953e45 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,7 +1,5 @@ -#[macro_use] -extern crate gotham_derive; -#[macro_use] -extern crate log; +#[macro_use] extern crate gotham_derive; +#[macro_use] extern crate log; use fake::{faker::internet::en::Username, Fake}; use gotham::{ @@ -11,24 +9,36 @@ use gotham::{ router::builder::*, state::State }; -use gotham_restful::{cors::*, *}; +use gotham_restful::*; +use log::LevelFilter; +use log4rs::{ + append::console::ConsoleAppender, + config::{Appender, Config, Root}, + encode::pattern::PatternEncoder +}; use serde::{Deserialize, Serialize}; #[derive(Resource)] -#[resource(read_all, read, search, create, update_all, update, remove, remove_all)] -struct Users {} - -#[derive(Resource)] -#[resource(auth_read_all)] -struct Auth {} - -#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)] -struct User { - username: String +#[resource(read_all, read, search, create, change_all, change, remove, remove_all)] +struct Users +{ } -#[read_all] -fn read_all() -> Success>> { +#[derive(Resource)] +#[resource(ReadAll)] +struct Auth +{ +} + +#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)] +struct User +{ + username : String +} + +#[read_all(Users)] +fn read_all() -> Success>> +{ vec![Username().fake(), Username().fake()] .into_iter() .map(|username| Some(User { username })) @@ -36,95 +46,114 @@ fn read_all() -> Success>> { .into() } -#[read] -fn read(id: u64) -> Success { - let username: String = Username().fake(); - User { - username: format!("{}{}", username, id) - } - .into() +#[read(Users)] +fn read(id : u64) -> Success +{ + let username : String = Username().fake(); + User { username: format!("{}{}", username, id) }.into() } -#[search] -fn search(query: User) -> Success { +#[search(Users)] +fn search(query : User) -> Success +{ query.into() } -#[create] -fn create(body: User) { +#[create(Users)] +fn create(body : User) +{ info!("Created User: {}", body.username); } -#[change_all] -fn update_all(body: Vec) { - info!( - "Changing all Users to {:?}", - body.into_iter().map(|u| u.username).collect::>() - ); +#[change_all(Users)] +fn update_all(body : Vec) +{ + info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::>()); } -#[change] -fn update(id: u64, body: User) { +#[change(Users)] +fn update(id : u64, body : User) +{ info!("Change User {} to {}", id, body.username); } -#[remove_all] -fn remove_all() { +#[remove_all(Users)] +fn remove_all() +{ info!("Delete all Users"); } -#[remove] -fn remove(id: u64) { +#[remove(Users)] +fn remove(id : u64) +{ info!("Delete User {}", id); } -#[read_all] -fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess { +#[read_all(Auth)] +fn auth_read_all(auth : AuthStatus<()>) -> AuthSuccess +{ match auth { AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)), _ => Err(Forbidden) } } -const ADDR: &str = "127.0.0.1:18080"; +const ADDR : &str = "127.0.0.1:18080"; #[derive(Clone, Default)] struct Handler; -impl AuthHandler for Handler { - fn jwt_secret Option>(&self, _state: &mut State, _decode_data: F) -> Option> { +impl AuthHandler for Handler +{ + fn jwt_secret Option>(&self, _state : &mut State, _decode_data : F) -> Option> + { None } } -fn main() { - pretty_env_logger::init_timed(); - +fn main() +{ + let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S%.3f %Z)} [{l}] {M} - {m}\n"); + let config = Config::builder() + .appender( + Appender::builder() + .build("stdout", Box::new( + ConsoleAppender::builder() + .encoder(Box::new(encoder)) + .build() + ))) + .build(Root::builder().appender("stdout").build(LevelFilter::Info)) + .unwrap(); + log4rs::init_config(config).unwrap(); + let cors = CorsConfig { origin: Origin::Copy, - headers: Headers::List(vec![CONTENT_TYPE]), + headers: vec![CONTENT_TYPE], credentials: true, ..Default::default() }; - + let auth = >::from_source(AuthSource::AuthorizationHeader); let logging = RequestLogger::new(log::Level::Info); - let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).add(logging).add(cors).build()); - - gotham::start( - ADDR, - build_router(chain, pipelines, |route| { - 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"); - route.resource::("auth"); - route.get_openapi("openapi"); - route.swagger_ui(""); - }); - }) + let (chain, pipelines) = single_pipeline( + new_pipeline() + .add(auth) + .add(logging) + .add(cors) + .build() ); + + gotham::start(ADDR, build_router(chain, pipelines, |route| { + 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"); + route.resource::("auth"); + route.get_openapi("openapi"); + }); + })); println!("Gotham started on {} for testing", ADDR); } + diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml deleted file mode 100644 index 782f798..0000000 --- a/openapi_type/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -# -*- eval: (cargo-minor-mode 1) -*- - -[package] -workspace = ".." -name = "openapi_type" -version = "0.1.0-dev" -authors = ["Dominic Meiser "] -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" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs deleted file mode 100644 index d46a922..0000000 --- a/openapi_type/src/impls.rs +++ /dev/null @@ -1,224 +0,0 @@ -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, bits: Option) -> 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) -> 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, NaiveDate => { - str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date)) -}); - -#[cfg(feature = "chrono")] -impl_openapi_type!(DateTime, 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 => { - 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(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 => array_schema::(false)); -impl_openapi_type!(BTreeSet, IndexSet, HashSet => { - array_schema::(true) -}); - -#[inline] -fn map_schema() -> 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, - IndexMap, - HashMap - => map_schema::() -); diff --git a/openapi_type/src/lib.rs b/openapi_type/src/lib.rs deleted file mode 100644 index 2933027..0000000 --- a/openapi_type/src/lib.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![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, - /// 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 -} - -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() - } -} diff --git a/openapi_type/src/private.rs b/openapi_type/src/private.rs deleted file mode 100644 index 892b8e3..0000000 --- a/openapi_type/src/private.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::OpenapiSchema; -use indexmap::IndexMap; - -pub type Dependencies = IndexMap; - -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); - } - } -} diff --git a/openapi_type/tests/custom_types.rs b/openapi_type/tests/custom_types.rs deleted file mode 100644 index 18cca88..0000000 --- a/openapi_type/tests/custom_types.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![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 - }] -}); diff --git a/openapi_type/tests/fail/enum_with_no_variants.rs b/openapi_type/tests/fail/enum_with_no_variants.rs deleted file mode 100644 index d08e223..0000000 --- a/openapi_type/tests/fail/enum_with_no_variants.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -enum Foo {} - -fn main() {} diff --git a/openapi_type/tests/fail/enum_with_no_variants.stderr b/openapi_type/tests/fail/enum_with_no_variants.stderr deleted file mode 100644 index 5c6b1d1..0000000 --- a/openapi_type/tests/fail/enum_with_no_variants.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support enums with no variants - --> $DIR/enum_with_no_variants.rs:4:10 - | -4 | enum Foo {} - | ^^ diff --git a/openapi_type/tests/fail/not_openapitype.rs b/openapi_type/tests/fail/not_openapitype.rs deleted file mode 100644 index 2b5b23c..0000000 --- a/openapi_type/tests/fail/not_openapitype.rs +++ /dev/null @@ -1,12 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -struct Foo { - bar: Bar -} - -struct Bar; - -fn main() { - Foo::schema(); -} diff --git a/openapi_type/tests/fail/not_openapitype.stderr b/openapi_type/tests/fail/not_openapitype.stderr deleted file mode 100644 index f089b15..0000000 --- a/openapi_type/tests/fail/not_openapitype.stderr +++ /dev/null @@ -1,8 +0,0 @@ -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) diff --git a/openapi_type/tests/fail/not_openapitype_generics.rs b/openapi_type/tests/fail/not_openapitype_generics.rs deleted file mode 100644 index 3d2a09d..0000000 --- a/openapi_type/tests/fail/not_openapitype_generics.rs +++ /dev/null @@ -1,12 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -struct Foo { - bar: T -} - -struct Bar; - -fn main() { - >::schema(); -} diff --git a/openapi_type/tests/fail/not_openapitype_generics.stderr b/openapi_type/tests/fail/not_openapitype_generics.stderr deleted file mode 100644 index d33bafe..0000000 --- a/openapi_type/tests/fail/not_openapitype_generics.stderr +++ /dev/null @@ -1,23 +0,0 @@ -error[E0599]: no function or associated item named `schema` found for struct `Foo` in the current scope - --> $DIR/not_openapitype_generics.rs:11:14 - | -4 | struct Foo { - | ------------- - | | - | function or associated item `schema` not found for this - | doesn't satisfy `Foo: OpenapiType` -... -8 | struct Bar; - | ----------- doesn't satisfy `Bar: OpenapiType` -... -11 | >::schema(); - | ^^^^^^ function or associated item not found in `Foo` - | - = note: the method `schema` exists but the following trait bounds were not satisfied: - `Bar: OpenapiType` - which is required by `Foo: OpenapiType` - `Foo: OpenapiType` - which is required by `&Foo: 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` diff --git a/openapi_type/tests/fail/rustfmt.sh b/openapi_type/tests/fail/rustfmt.sh deleted file mode 100755 index a93f958..0000000 --- a/openapi_type/tests/fail/rustfmt.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/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 diff --git a/openapi_type/tests/fail/tuple_struct.rs b/openapi_type/tests/fail/tuple_struct.rs deleted file mode 100644 index 146a236..0000000 --- a/openapi_type/tests/fail/tuple_struct.rs +++ /dev/null @@ -1,6 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -struct Foo(i64, i64); - -fn main() {} diff --git a/openapi_type/tests/fail/tuple_struct.stderr b/openapi_type/tests/fail/tuple_struct.stderr deleted file mode 100644 index b5ceb01..0000000 --- a/openapi_type/tests/fail/tuple_struct.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support tuple structs - --> $DIR/tuple_struct.rs:4:11 - | -4 | struct Foo(i64, i64); - | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/tuple_variant.rs b/openapi_type/tests/fail/tuple_variant.rs deleted file mode 100644 index 92aa8d7..0000000 --- a/openapi_type/tests/fail/tuple_variant.rs +++ /dev/null @@ -1,8 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -enum Foo { - Pair(i64, i64) -} - -fn main() {} diff --git a/openapi_type/tests/fail/tuple_variant.stderr b/openapi_type/tests/fail/tuple_variant.stderr deleted file mode 100644 index 05573cb..0000000 --- a/openapi_type/tests/fail/tuple_variant.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] does not support tuple variants - --> $DIR/tuple_variant.rs:5:6 - | -5 | Pair(i64, i64) - | ^^^^^^^^^^ diff --git a/openapi_type/tests/fail/union.rs b/openapi_type/tests/fail/union.rs deleted file mode 100644 index d011109..0000000 --- a/openapi_type/tests/fail/union.rs +++ /dev/null @@ -1,9 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -union Foo { - signed: i64, - unsigned: u64 -} - -fn main() {} diff --git a/openapi_type/tests/fail/union.stderr b/openapi_type/tests/fail/union.stderr deleted file mode 100644 index f0feb48..0000000 --- a/openapi_type/tests/fail/union.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(OpenapiType)] cannot be used on unions - --> $DIR/union.rs:4:1 - | -4 | union Foo { - | ^^^^^ diff --git a/openapi_type/tests/fail/unknown_attribute.rs b/openapi_type/tests/fail/unknown_attribute.rs deleted file mode 100644 index 70a4785..0000000 --- a/openapi_type/tests/fail/unknown_attribute.rs +++ /dev/null @@ -1,7 +0,0 @@ -use openapi_type::OpenapiType; - -#[derive(OpenapiType)] -#[openapi(pizza)] -struct Foo; - -fn main() {} diff --git a/openapi_type/tests/fail/unknown_attribute.stderr b/openapi_type/tests/fail/unknown_attribute.stderr deleted file mode 100644 index 2558768..0000000 --- a/openapi_type/tests/fail/unknown_attribute.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Unexpected token - --> $DIR/unknown_attribute.rs:4:11 - | -4 | #[openapi(pizza)] - | ^^^^^ diff --git a/openapi_type/tests/std_types.rs b/openapi_type/tests/std_types.rs deleted file mode 100644 index e10fb89..0000000 --- a/openapi_type/tests/std_types.rs +++ /dev/null @@ -1,216 +0,0 @@ -#[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, Date, Date, NaiveDate = { - "type": "string", - "format": "date" -}); - -#[cfg(feature = "chrono")] -test_type!(DateTime, DateTime, DateTime, NaiveDateTime = { - "type": "string", - "format": "date-time" -}); - -// ### some std types - -test_type!(Option = { - "type": "string", - "nullable": true -}); - -test_type!(Vec = { - "type": "array", - "items": { - "type": "string" - } -}); - -test_type!(BTreeSet, IndexSet, HashSet = { - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true -}); - -test_type!(BTreeMap, IndexMap, HashMap = { - "type": "object", - "properties": { - "default": { - "type": "integer" - } - }, - "required": ["default"], - "additionalProperties": { - "type": "string" - } -}); diff --git a/openapi_type/tests/trybuild.rs b/openapi_type/tests/trybuild.rs deleted file mode 100644 index b76b676..0000000 --- a/openapi_type/tests/trybuild.rs +++ /dev/null @@ -1,7 +0,0 @@ -use trybuild::TestCases; - -#[test] -fn trybuild() { - let t = TestCases::new(); - t.compile_fail("tests/fail/*.rs"); -} diff --git a/openapi_type_derive/Cargo.toml b/openapi_type_derive/Cargo.toml deleted file mode 100644 index ab8e932..0000000 --- a/openapi_type_derive/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -# -*- eval: (cargo-minor-mode 1) -*- - -[package] -workspace = ".." -name = "openapi_type_derive" -version = "0.1.0-dev" -authors = ["Dominic Meiser "] -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" diff --git a/openapi_type_derive/src/codegen.rs b/openapi_type_derive/src/codegen.rs deleted file mode 100644 index a56c97c..0000000 --- a/openapi_type_derive/src/codegen.rs +++ /dev/null @@ -1,143 +0,0 @@ -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() - } - ) - ) - } -} diff --git a/openapi_type_derive/src/lib.rs b/openapi_type_derive/src/lib.rs deleted file mode 100644 index 0a81bec..0000000 --- a/openapi_type_derive/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -#![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 { - // 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 - } - } - } - }) -} diff --git a/openapi_type_derive/src/parser.rs b/openapi_type_derive/src/parser.rs deleted file mode 100644 index 350fee2..0000000 --- a/openapi_type_derive/src/parser.rs +++ /dev/null @@ -1,198 +0,0 @@ -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), - Alternatives(Vec), - Unit -} - -fn parse_named_fields(named_fields: &FieldsNamed) -> syn::Result { - 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 { - 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 { - let mut strings: Vec = 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::>>()? - )) - }; - - 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 { - 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, - pub(super) rename_all: Option, - pub(super) tag: Option, - pub(super) content: Option, - pub(super) untagged: bool -} - -pub(super) fn parse_container_attrs( - input: &Attribute, - attrs: &mut ContainerAttributes, - error_on_unknown: bool -) -> syn::Result<()> { - let tokens: Punctuated = 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(()) -} diff --git a/openapi_type_derive/src/util.rs b/openapi_type_derive/src/util.rs deleted file mode 100644 index 2a752e0..0000000 --- a/openapi_type_derive/src/util.rs +++ /dev/null @@ -1,52 +0,0 @@ -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; -} - -impl ExpectLit for Lit { - fn expect_str(self) -> syn::Result { - match self { - Self::Str(str) => Ok(str), - _ => Err(syn::Error::new(self.span(), "Expected string literal")) - } - } -} diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 1cd0ec8..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/src/auth.rs b/src/auth.rs index fdbab63..0888ac3 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,27 +1,29 @@ -use crate::{AuthError, Forbidden}; - +use crate::{AuthError, Forbidden, HeaderName}; use cookie::CookieJar; -use futures_util::{ - future, - future::{FutureExt, TryFutureExt} -}; +use futures_util::{future, future::{FutureExt, TryFutureExt}}; use gotham::{ - anyhow, handler::HandlerFuture, - hyper::header::{HeaderMap, HeaderName, AUTHORIZATION}, - middleware::{cookie::CookieParser, Middleware, NewMiddleware}, + hyper::header::{AUTHORIZATION, HeaderMap}, + middleware::{Middleware, NewMiddleware}, state::{FromState, State} }; -use jsonwebtoken::{errors::ErrorKind, DecodingKey}; +use jsonwebtoken::{ + errors::ErrorKind, + DecodingKey +}; use serde::de::DeserializeOwned; -use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin}; +use std::{ + marker::PhantomData, + panic::RefUnwindSafe, + pin::Pin +}; -#[doc(no_inline)] pub use jsonwebtoken::Validation as AuthValidation; /// The authentication status returned by the auth middleware for each request. #[derive(Debug, StateData)] -pub enum AuthStatus { +pub enum AuthStatus +{ /// The auth status is unknown. Unknown, /// The request has been performed without any kind of authentication. @@ -36,9 +38,10 @@ pub enum AuthStatus { impl Clone for AuthStatus where - T: Clone + Send + 'static + T : Clone + Send + 'static { - fn clone(&self) -> Self { + fn clone(&self) -> Self + { match self { Self::Unknown => Self::Unknown, Self::Unauthenticated => Self::Unauthenticated, @@ -49,10 +52,16 @@ where } } -impl Copy for AuthStatus where T: Copy + Send + 'static {} +impl Copy for AuthStatus +where + T : Copy + Send + 'static +{ +} -impl AuthStatus { - pub fn ok(self) -> Result { +impl AuthStatus +{ + pub fn ok(self) -> Result + { match self { Self::Authenticated(data) => Ok(data), _ => Err(Forbidden) @@ -62,7 +71,8 @@ impl AuthStatus { /// The source of the authentication token in the request. #[derive(Clone, Debug, StateData)] -pub enum AuthSource { +pub enum AuthSource +{ /// Take the token from a cookie with the given name. Cookie(String), /// Take the token from a header with the given name. @@ -77,9 +87,8 @@ pub enum AuthSource { This trait will help the auth middleware to determine the validity of an authentication token. A very basic implementation could look like this: - ``` -# use gotham_restful::{AuthHandler, gotham::state::State}; +# use gotham_restful::{AuthHandler, State}; # const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc"; @@ -91,29 +100,36 @@ impl AuthHandler for CustomAuthHandler { } ``` */ -pub trait AuthHandler { +pub trait AuthHandler +{ /// Return the SHA256-HMAC secret used to verify the JWT token. - fn jwt_secret Option>(&self, state: &mut State, decode_data: F) -> Option>; + fn jwt_secret Option>(&self, state : &mut State, decode_data : F) -> Option>; } -/// 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)] -pub struct StaticAuthHandler { - secret: Vec +pub struct StaticAuthHandler +{ + secret : Vec } -impl StaticAuthHandler { - pub fn from_vec(secret: Vec) -> Self { +impl StaticAuthHandler +{ + pub fn from_vec(secret : Vec) -> Self + { Self { secret } } - - pub fn from_array(secret: &[u8]) -> Self { + + pub fn from_array(secret : &[u8]) -> Self + { Self::from_vec(secret.to_vec()) } } -impl AuthHandler for StaticAuthHandler { - fn jwt_secret Option>(&self, _state: &mut State, _decode_data: F) -> Option> { +impl AuthHandler for StaticAuthHandler +{ + fn jwt_secret Option>(&self, _state : &mut State, _decode_data : F) -> Option> + { Some(self.secret.clone()) } } @@ -138,7 +154,7 @@ struct AuthData { exp: u64 } -#[read_all] +#[read_all(AuthResource)] fn read_all(auth : &AuthStatus) -> Success { format!("{:?}", auth).into() } @@ -157,18 +173,19 @@ fn main() { ``` */ #[derive(Debug)] -pub struct AuthMiddleware { - source: AuthSource, - validation: AuthValidation, - handler: Handler, - _data: PhantomData +pub struct AuthMiddleware +{ + source : AuthSource, + validation : AuthValidation, + handler : Handler, + _data : PhantomData } impl Clone for AuthMiddleware -where - Handler: Clone +where Handler : Clone { - fn clone(&self) -> Self { + fn clone(&self) -> Self + { Self { source: self.source.clone(), validation: self.validation.clone(), @@ -180,10 +197,11 @@ where impl AuthMiddleware where - Data: DeserializeOwned + Send, - Handler: AuthHandler + Default + Data : DeserializeOwned + Send, + Handler : AuthHandler + Default { - pub fn from_source(source: AuthSource) -> Self { + pub fn from_source(source : AuthSource) -> Self + { Self { source, validation: Default::default(), @@ -195,10 +213,11 @@ where impl AuthMiddleware where - Data: DeserializeOwned + Send, - Handler: AuthHandler + Data : DeserializeOwned + Send, + Handler : AuthHandler { - pub fn new(source: AuthSource, validation: AuthValidation, handler: Handler) -> Self { + pub fn new(source : AuthSource, validation : AuthValidation, handler : Handler) -> Self + { Self { source, validation, @@ -206,56 +225,59 @@ where _data: Default::default() } } - - fn auth_status(&self, state: &mut State) -> AuthStatus { + + fn auth_status(&self, state : &mut State) -> AuthStatus + { // extract the provided token, if any let token = match &self.source { - AuthSource::Cookie(name) => CookieJar::try_borrow_from(&state) - .map(|jar| jar.get(&name).map(|cookie| cookie.value().to_owned())) - .unwrap_or_else(|| { - CookieParser::from_state(&state) - .get(&name) - .map(|cookie| cookie.value().to_owned()) - }), - AuthSource::Header(name) => HeaderMap::try_borrow_from(&state) - .and_then(|map| map.get(name)) - .and_then(|header| header.to_str().ok()) - .map(|value| value.to_owned()), - AuthSource::AuthorizationHeader => HeaderMap::try_borrow_from(&state) - .and_then(|map| map.get(AUTHORIZATION)) - .and_then(|header| header.to_str().ok()) - .and_then(|value| value.split_whitespace().nth(1)) - .map(|value| value.to_owned()) + AuthSource::Cookie(name) => { + CookieJar::try_borrow_from(&state) + .and_then(|jar| jar.get(&name)) + .map(|cookie| cookie.value().to_owned()) + }, + AuthSource::Header(name) => { + HeaderMap::try_borrow_from(&state) + .and_then(|map| map.get(name)) + .and_then(|header| header.to_str().ok()) + .map(|value| value.to_owned()) + }, + AuthSource::AuthorizationHeader => { + HeaderMap::try_borrow_from(&state) + .and_then(|map| map.get(AUTHORIZATION)) + .and_then(|header| header.to_str().ok()) + .and_then(|value| value.split_whitespace().nth(1)) + .map(|value| value.to_owned()) + } }; - + // unauthed if no token let token = match token { Some(token) => token, None => return AuthStatus::Unauthenticated }; - + // get the secret from the handler, possibly decoding claims ourselves let secret = self.handler.jwt_secret(state, || { let b64 = token.split('.').nth(1)?; let raw = base64::decode_config(b64, base64::URL_SAFE_NO_PAD).ok()?; serde_json::from_slice(&raw).ok()? }); - + // unknown if no secret let secret = match secret { Some(secret) => secret, None => return AuthStatus::Unknown }; - + // validate the token - let data: Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) { + let data : Data = match jsonwebtoken::decode(&token, &DecodingKey::from_secret(&secret), &self.validation) { Ok(data) => data.claims, Err(e) => match dbg!(e.into_kind()) { ErrorKind::ExpiredSignature => return AuthStatus::Expired, _ => return AuthStatus::Invalid } }; - + // we found a valid token AuthStatus::Authenticated(data) } @@ -263,20 +285,20 @@ where impl Middleware for AuthMiddleware where - Data: DeserializeOwned + Send + 'static, - Handler: AuthHandler + Data : DeserializeOwned + Send + 'static, + Handler : AuthHandler { - fn call(self, mut state: State, chain: Chain) -> Pin> + fn call(self, mut state : State, chain : Chain) -> Pin> where - Chain: FnOnce(State) -> Pin> + Chain : FnOnce(State) -> Pin> { // put the source in our state, required for e.g. openapi state.put(self.source.clone()); - + // put the status in our state let status = self.auth_status(&mut state); state.put(status); - + // call the rest of the chain chain(state).and_then(|(state, res)| future::ok((state, res))).boxed() } @@ -284,41 +306,45 @@ where impl NewMiddleware for AuthMiddleware where - Self: Clone + Middleware + Sync + RefUnwindSafe + Self : Clone + Middleware + Sync + RefUnwindSafe { type Instance = Self; - - fn new_middleware(&self) -> anyhow::Result { - let c: Self = self.clone(); + + fn new_middleware(&self) -> Result + { + let c : Self = self.clone(); Ok(c) } } #[cfg(test)] -mod test { +mod test +{ use super::*; use cookie::Cookie; - use gotham::hyper::header::COOKIE; use std::fmt::Debug; - + // 256-bit random string - const JWT_SECRET: &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW"; - + const JWT_SECRET : &'static [u8; 32] = b"Lyzsfnta0cdxyF0T9y6VGxp3jpgoMUuW"; + // some known tokens const VALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9.8h8Ax-nnykqEQ62t7CxmM3ja6NzUQ4L0MLOOzddjLKk"; const EXPIRED_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjE1Nzc4MzcxMDB9.eV1snaGLYrJ7qUoMk74OvBY3WUU9M0Je5HTU2xtX1v0"; const INVALID_TOKEN : &'static str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtc3JkMCIsInN1YiI6ImdvdGhhbS1yZXN0ZnVsIiwiaWF0IjoxNTc3ODM2ODAwLCJleHAiOjQxMDI0NDQ4MDB9"; - + #[derive(Debug, Deserialize, PartialEq)] - struct TestData { - iss: String, - sub: String, - iat: u64, - exp: u64 + struct TestData + { + iss : String, + sub : String, + iat : u64, + exp : u64 } - - impl Default for TestData { - fn default() -> Self { + + impl Default for TestData + { + fn default() -> Self + { Self { iss: "msrd0".to_owned(), sub: "gotham-restful".to_owned(), @@ -327,17 +353,20 @@ mod test { } } } - + #[derive(Default)] struct NoneAuthHandler; - impl AuthHandler for NoneAuthHandler { - fn jwt_secret Option>(&self, _state: &mut State, _decode_data: F) -> Option> { + impl AuthHandler for NoneAuthHandler + { + fn jwt_secret Option>(&self, _state : &mut State, _decode_data : F) -> Option> + { None } } - + #[test] - fn test_auth_middleware_none_secret() { + fn test_auth_middleware_none_secret() + { let middleware = >::from_source(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -346,21 +375,22 @@ mod test { middleware.auth_status(&mut state); }); } - + #[derive(Default)] struct TestAssertingHandler; impl AuthHandler for TestAssertingHandler - where - T: Debug + Default + PartialEq + where T : Debug + Default + PartialEq { - fn jwt_secret Option>(&self, _state: &mut State, decode_data: F) -> Option> { + fn jwt_secret Option>(&self, _state : &mut State, decode_data : F) -> Option> + { assert_eq!(decode_data(), Some(T::default())); Some(JWT_SECRET.to_vec()) } } - + #[test] - fn test_auth_middleware_decode_data() { + fn test_auth_middleware_decode_data() + { let middleware = >::from_source(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -369,16 +399,16 @@ mod test { middleware.auth_status(&mut state); }); } - - fn new_middleware(source: AuthSource) -> AuthMiddleware - where - T: DeserializeOwned + Send + + fn new_middleware(source : AuthSource) -> AuthMiddleware + where T : DeserializeOwned + Send { AuthMiddleware::new(source, Default::default(), StaticAuthHandler::from_array(JWT_SECRET)) } - + #[test] - fn test_auth_middleware_no_token() { + fn test_auth_middleware_no_token() + { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let status = middleware.auth_status(&mut state); @@ -388,9 +418,10 @@ mod test { }; }); } - + #[test] - fn test_auth_middleware_expired_token() { + fn test_auth_middleware_expired_token() + { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -403,9 +434,10 @@ mod test { }; }); } - + #[test] - fn test_auth_middleware_invalid_token() { + fn test_auth_middleware_invalid_token() + { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -418,9 +450,10 @@ mod test { }; }); } - + #[test] - fn test_auth_middleware_auth_header_token() { + fn test_auth_middleware_auth_header_token() + { let middleware = new_middleware::(AuthSource::AuthorizationHeader); State::with_new(|mut state| { let mut headers = HeaderMap::new(); @@ -433,9 +466,10 @@ mod test { }; }) } - + #[test] - fn test_auth_middleware_header_token() { + fn test_auth_middleware_header_token() + { let header_name = "x-znoiprwmvfexju"; let middleware = new_middleware::(AuthSource::Header(HeaderName::from_static(header_name))); State::with_new(|mut state| { @@ -449,9 +483,10 @@ mod test { }; }) } - + #[test] - fn test_auth_middleware_cookie_token() { + fn test_auth_middleware_cookie_token() + { let cookie_name = "znoiprwmvfexju"; let middleware = new_middleware::(AuthSource::Cookie(cookie_name.to_owned())); State::with_new(|mut state| { @@ -465,20 +500,4 @@ mod test { }; }) } - - #[test] - fn test_auth_middleware_cookie_no_jar() { - let cookie_name = "znoiprwmvfexju"; - let middleware = new_middleware::(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) - }; - }) - } } diff --git a/src/cors.rs b/src/cors.rs index 43ff43b..46af65c 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -1,23 +1,25 @@ +use crate::matcher::AccessControlRequestMethodMatcher; 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 + ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY, + HeaderMap, HeaderName, HeaderValue }, Body, Method, Response, StatusCode }, middleware::Middleware, pipeline::chain::PipelineHandleChain, - router::{ - builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher}, - route::matcher::AccessControlRequestMethodMatcher - }, - state::{FromState, State} + router::builder::*, + state::{FromState, State}, +}; +use itertools::Itertools; +use std::{ + panic::RefUnwindSafe, + pin::Pin }; -use std::{panic::RefUnwindSafe, pin::Pin}; /** Specify the allowed origins of the request. It is up to the browser to check the validity of the @@ -25,7 +27,8 @@ origin. This, when sent to the browser, will indicate whether or not the request allowed to make the request. */ #[derive(Clone, Debug)] -pub enum Origin { +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. @@ -36,15 +39,19 @@ pub enum Origin { Copy } -impl Default for Origin { - fn default() -> Self { +impl Default for Origin +{ + fn default() -> Self + { Self::None } } -impl Origin { +impl Origin +{ /// Get the header value for the `Access-Control-Allow-Origin` header. - fn header_value(&self, state: &State) -> Option { + fn header_value(&self, state : &State) -> Option + { match self { Self::None => None, Self::Star => Some("*".parse().unwrap()), @@ -55,64 +62,17 @@ impl Origin { } } } - - /// Returns true if the `Vary` header has to include `Origin`. - fn varies(&self) -> bool { - matches!(self, Self::Copy) - } -} - -/** -Specify the allowed headers of the request. It is up to the browser to check that only the allowed -headers are sent with the request. -*/ -#[derive(Clone, Debug)] -pub enum Headers { - /// Do not send any `Access-Control-Allow-Headers` headers. - None, - /// Set the `Access-Control-Allow-Headers` header to the following header list. If empty, this - /// is treated as if it was [None]. - List(Vec), - /// 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 { - 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]: +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))] +# use gotham_restful::*; fn main() { let cors = CorsConfig { origin: Origin::Star, @@ -130,7 +90,7 @@ configurations for different scopes, you need to register the middleware inside ```rust,no_run # use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State}; -# use gotham_restful::{*, cors::Origin}; +# use gotham_restful::*; let pipelines = new_pipeline_set(); // The first cors configuration @@ -162,23 +122,27 @@ gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| { }); })); ``` + + [`State`]: ../gotham/state/struct.State.html */ #[derive(Clone, Debug, Default, NewMiddleware, StateData)] -pub struct CorsConfig { +pub struct CorsConfig +{ /// The allowed origins. - pub origin: Origin, + pub origin : Origin, /// The allowed headers. - pub headers: Headers, + pub headers : Vec, /// The amount of seconds that the preflight request can be cached. - pub max_age: u64, + pub max_age : u64, /// Whether or not the request may be made with supplying credentials. - pub credentials: bool + pub credentials : bool } -impl Middleware for CorsConfig { - fn call(self, mut state: State, chain: Chain) -> Pin> +impl Middleware for CorsConfig +{ + fn call(self, mut state : State, chain : Chain) -> Pin> where - Chain: FnOnce(State) -> Pin> + Chain : FnOnce(State) -> Pin> { state.put(self); chain(state) @@ -187,42 +151,45 @@ impl Middleware for CorsConfig { /** Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that -the response is aligned with the `state`'s [CorsConfig]. +the response is aligned with the `state`'s [`CorsConfig`]. -If you are using the [Resource](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. +If you are using the [`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). +For further information on CORS, read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. + + [`CorsConfig`]: ./struct.CorsConfig.html */ -pub fn handle_cors(state: &State, res: &mut Response) { +pub fn handle_cors(state : &State, res : &mut Response) +{ let config = CorsConfig::try_borrow_from(state); - if let Some(cfg) = config { - let headers = res.headers_mut(); + let headers = res.headers_mut(); + + // non-preflight requests require the Access-Control-Allow-Origin header + if let Some(header) = config.and_then(|cfg| cfg.origin.header_value(state)) + { + headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); + } - // 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 matches!(config.map(|cfg| &cfg.origin), Some(Origin::Copy)) + { + let vary = headers.get(VARY).map(|vary| format!("{},Origin", vary.to_str().unwrap())); + headers.insert(VARY, vary.as_deref().unwrap_or("Origin").parse().unwrap()); + } - // if 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")); - } + // if we allow credentials, tell the browser + if config.map(|cfg| cfg.credentials).unwrap_or(false) + { + headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true".parse().unwrap()); } } /// 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::*; @@ -239,62 +206,60 @@ pub fn handle_cors(state: &State, res: &mut Response) { /// ``` pub trait CorsRoute where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ 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); + /// [`CorsConfig`](struct.CorsConfig.html). + fn cors(&mut self, path : &str, method : Method); } -pub(crate) fn cors_preflight_handler(state: State) -> (State, Response) { +fn cors_preflight_handler(state : State) -> (State, Response) +{ 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 = Vec::new(); // copy the request method over to the response - let method = HeaderMap::borrow_from(&state) - .get(ACCESS_CONTROL_REQUEST_METHOD) - .unwrap() - .clone(); + let method = HeaderMap::borrow_from(&state).get(ACCESS_CONTROL_REQUEST_METHOD).unwrap().clone(); headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method); - 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()); + // if we allow any headers, put them in + if let Some(hdrs) = config.map(|cfg| &cfg.headers) + { + if hdrs.len() > 0 + { + // TODO do we want to return all headers or just those asked by the browser? + headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, hdrs.iter().join(",").parse().unwrap()); } } - // make sure the browser knows that this request was based on the method - headers.insert(VARY, vary.join(",").parse().unwrap()); + // set the max age for the preflight cache + if let Some(age) = config.map(|cfg| cfg.max_age) + { + headers.insert(ACCESS_CONTROL_MAX_AGE, age.into()); + } + // make sure the browser knows that this request was based on the method + headers.insert(VARY, "Access-Control-Request-Method".parse().unwrap()); + handle_cors(&state, &mut res); (state, res) } impl CorsRoute for D where - D: DrawRoutes, - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + D : DrawRoutes, + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - fn cors(&mut self, path: &str, method: Method) { + fn cors(&mut self, path : &str, method : Method) + { let matcher = AccessControlRequestMethodMatcher::new(method); - self.options(path).extend_route_matcher(matcher).to(cors_preflight_handler); + self.options(path) + .extend_route_matcher(matcher) + .to(cors_preflight_handler); } } diff --git a/src/endpoint.rs b/src/endpoint.rs deleted file mode 100644 index 2095948..0000000 --- a/src/endpoint.rs +++ /dev/null @@ -1,136 +0,0 @@ -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) -> Result { - 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) {} -} - -// 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 + 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 + 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 { - None - } - - /// The handler for this endpoint. - fn handle<'a>( - state: &'a mut State, - placeholders: Self::Placeholders, - params: Self::Params, - body: Option - ) -> BoxFuture<'a, Self::Output>; -} - -#[cfg(feature = "openapi")] -impl 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 - ) -> BoxFuture<'a, Self::Output> { - E::handle(state, placeholders, params, body) - } -} diff --git a/src/lib.rs b/src/lib.rs index aea56a4..d7aa095 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,52 +1,35 @@ +#![allow(clippy::tabs_in_doc_comments)] #![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))] +#![deny(intra_doc_link_resolution_failure)] /*! 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 +create resources with assigned methods that aim to be a more convenient way of creating handlers for requests. -# Features +# Design Goals - - 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 +This is an opinionated framework on top of [gotham]. Unless your web server handles mostly JSON as +request/response bodies and does that in a RESTful way, this framework is probably a bad fit for +your application. The ultimate goal of gotham-restful is to provide a way to write a RESTful +web server in Rust as convenient as possible with the least amount of boilerplate neccessary. -# Safety +# Methods -This crate is just as safe as you'd expect from anything written in safe Rust - and -`#![forbid(unsafe_code)]` ensures that no unsafe was used. +Assuming you assign `/foobar` to your resource, you can implement the following methods: -# Endpoints +| Method Name | Required Arguments | HTTP Verb | HTTP Path | +| ----------- | ------------------ | --------- | ----------- | +| read_all | | GET | /foobar | +| read | id | GET | /foobar/:id | +| search | query | GET | /foobar/search | +| create | body | POST | /foobar | +| change_all | body | PUT | /foobar | +| change | id, body | PUT | /foobar/:id | +| remove_all | | DELETE | /foobar | +| remove | id | DELETE | /foobar/:id | -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: +Each of those methods has a macro that creates the neccessary boilerplate for the Resource. A +simple example could look like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -58,15 +41,15 @@ simple example looks like this: #[resource(read)] struct FooResource; -/// The return type of the foo read endpoint. +/// The return type of the foo read method. #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo { id: u64 } -/// The foo read endpoint. -#[read] +/// The foo read method handler. +#[read(FooResource)] fn read(id: u64) -> Success { Foo { id }.into() } @@ -77,53 +60,19 @@ fn read(id: u64) -> Success { # } ``` -## 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 { - path.name.into() -} -# fn main() { -# gotham::start("127.0.0.1:8080", build_simple_router(|route| { -# route.resource::("custom"); -# })); -# } -``` - # Arguments -Some endpoints require arguments. Those should be - * **id** Should be a deserializable json-primitive like [`i64`] or [`String`]. +Some methods require arguments. Those should be + * **id** Should be a deserializable json-primitive like `i64` or `String`. * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. * **query** Should be any deserializable object whose variables are json-primitives. It will however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The - type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor). + type needs to implement [`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. +Additionally, non-async handlers may take a reference to gotham's [`State`]. If you need to +have an async handler (that is, the function that the method macro is invoked on is declared +as `async fn`), consider returning the boxed future instead. Since [`State`] does not implement +`Sync` there is unfortunately no more convenient way. # Uploads and Downloads @@ -147,7 +96,7 @@ struct RawImage { content_type: Mime } -#[create] +#[create(ImageResource)] fn create(body : RawImage) -> Raw> { Raw::new(body.content, body.content_type) } @@ -158,60 +107,20 @@ fn create(body : RawImage) -> Raw> { # } ``` -# 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::("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. +when you implement your web server. ## Authentication Feature In order to enable authentication support, enable the `auth` feature gate. This allows you to register a middleware that can automatically check for the existence of an JWT authentication -token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret +token. Besides being supported by the method macros, it supports to lookup the required JWT secret with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. None of this is currently supported by gotham's own JWT middleware. -A simple example that uses only a single secret looks like this: +A simple example that uses only a single secret could look like this: ```rust,no_run # #[macro_use] extern crate gotham_restful_derive; @@ -225,7 +134,7 @@ A simple example that uses only a single secret looks like this: struct SecretResource; #[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Secret { id: u64, intended_for: String @@ -237,7 +146,7 @@ struct AuthData { exp: u64 } -#[read] +#[read(SecretResource)] fn read(auth: AuthStatus, id: u64) -> AuthSuccess { let intended_for = auth.ok()?.sub; Ok(Secret { id, intended_for }) @@ -264,20 +173,20 @@ the `Access-Control-Allow-Methods` header is touched. To change the behaviour, a configuration as a middleware. A simple example that allows authentication from every origin (note that `*` always disallows -authentication), and every content type, looks like this: +authentication), and every content type, could look 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 gotham_restful::*; # use serde::{Deserialize, Serialize}; #[derive(Resource)] #[resource(read_all)] struct FooResource; -#[read_all] +#[read_all(FooResource)] fn read_all() { // your handler } @@ -285,7 +194,7 @@ fn read_all() { fn main() { let cors = CorsConfig { origin: Origin::Copy, - headers: Headers::List(vec![CONTENT_TYPE]), + headers: vec![CONTENT_TYPE], max_age: 0, credentials: true }; @@ -307,7 +216,7 @@ note however that due to the way gotham's diesel middleware implementation, it i to run async code while holding a database connection. If you need to combine async and database, you'll need to borrow the connection from the [`State`] yourself and return a boxed future. -A simple non-async example looks like this: +A simple non-async example could look like this: ```rust,no_run # #[macro_use] extern crate diesel; @@ -331,13 +240,13 @@ A simple non-async example looks like this: struct FooResource; #[derive(Queryable, Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] struct Foo { id: i64, value: String } -#[read_all] +#[read_all(FooResource)] fn read_all(conn: &PgConnection) -> QueryResult> { foo::table.load(conn) } @@ -347,7 +256,7 @@ type Repo = gotham_middleware_diesel::Repo; 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::("foo"); @@ -356,178 +265,132 @@ fn main() { # } ``` -## OpenAPI Feature - -The OpenAPI feature is probably the most powerful one of this crate. Definitely read this section -carefully both as a binary as well as a library author to avoid unwanted suprises. - -In order to automatically create an openapi specification, gotham-restful needs knowledge over -all routes and the types returned. `serde` does a great job at serialization but doesn't give -enough type information, so all types used in the router need to implement -`OpenapiType`[openapi_type::OpenapiType]. This can be derived for almoust any type and there -should be no need to implement it manually. A simple example looks like this: - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# #[cfg(feature = "openapi")] -# mod openapi_feature_enabled { -# use gotham::{router::builder::*, state::State}; -# use gotham_restful::*; -# use openapi_type::OpenapiType; -# use serde::{Deserialize, Serialize}; -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[derive(OpenapiType, Serialize)] -struct Foo { - bar: String -} - -#[read_all] -fn read_all() -> Success { - Foo { 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::("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. +There is a lack of good examples, but there is currently a collection of code in the [example] +directory, that might help you. Any help writing more examples is highly appreciated. + +# License + +Licensed under your option of: + - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) + - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) [diesel]: https://diesel.rs/ [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example [gotham]: https://gotham.rs/ [serde_json]: https://github.com/serde-rs/json#serde-json---- - [`State`]: gotham::state::State + [`CorsRoute`]: trait.CorsRoute.html + [`QueryStringExtractor`]: ../gotham/extractor/trait.QueryStringExtractor.html + [`RequestBody`]: trait.RequestBody.html + [`State`]: ../gotham/state/struct.State.html */ -#[cfg(all(feature = "openapi", feature = "without-openapi"))] -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; +#[macro_use] extern crate gotham_derive; +#[macro_use] extern crate log; +#[macro_use] extern crate serde; #[doc(no_inline)] pub use gotham; #[doc(no_inline)] +pub use gotham::{ + hyper::{header::HeaderName, StatusCode}, + state::{FromState, State} +}; +#[doc(no_inline)] pub use mime::Mime; pub use gotham_restful_derive::*; /// Not public API #[doc(hidden)] -pub mod private { - pub use crate::routing::PathExtractor as IdPlaceholder; - - pub use futures_util::future::{BoxFuture, FutureExt}; - +pub mod export +{ + pub use futures_util::future::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}; +pub use auth::{ + AuthHandler, + AuthMiddleware, + AuthSource, + AuthStatus, + AuthValidation, + StaticAuthHandler +}; #[cfg(feature = "cors")] -pub mod cors; +mod cors; #[cfg(feature = "cors")] -pub use cors::{handle_cors, CorsConfig, CorsRoute}; +pub use cors::{ + handle_cors, + CorsConfig, + CorsRoute, + Origin +}; + +pub mod matcher; #[cfg(feature = "openapi")] mod openapi; #[cfg(feature = "openapi")] -pub use openapi::{builder::OpenapiInfo, router::GetOpenapi}; +pub use openapi::{ + builder::OpenapiInfo, + router::GetOpenapi, + types::{OpenapiSchema, OpenapiType} +}; -mod endpoint; -#[cfg(feature = "openapi")] -pub use endpoint::EndpointWithSchema; -pub use endpoint::{Endpoint, NoopExtractor}; +mod resource; +pub use resource::{ + Resource, + ResourceMethod, + ResourceReadAll, + ResourceRead, + ResourceSearch, + ResourceCreate, + ResourceChangeAll, + ResourceChange, + ResourceRemoveAll, + ResourceRemove +}; mod response; -pub use response::{ - AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent, - Raw, Redirect, Response, Success +pub use response::Response; + +mod result; +pub use result::{ + AuthError, + AuthError::Forbidden, + AuthErrorOrOther, + AuthResult, + AuthSuccess, + IntoResponseError, + NoContent, + Raw, + ResourceResult, + Success }; -#[cfg(feature = "openapi")] -pub use response::{IntoResponseWithSchema, ResponseSchema}; mod routing; -pub use routing::{DrawResourceRoutes, DrawResources}; +pub use routing::{DrawResources, DrawResourceRoutes}; #[cfg(feature = "openapi")] -pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi}; +pub use routing::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(route: D); -} diff --git a/src/matcher/access_control_request_method.rs b/src/matcher/access_control_request_method.rs new file mode 100644 index 0000000..a5e03f2 --- /dev/null +++ b/src/matcher/access_control_request_method.rs @@ -0,0 +1,106 @@ +use gotham::{ + hyper::{header::{ACCESS_CONTROL_REQUEST_METHOD, HeaderMap}, Method, StatusCode}, + router::{non_match::RouteNonMatch, route::matcher::RouteMatcher}, + state::{FromState, State} +}; + +/// A route matcher that checks whether the value of the `Access-Control-Request-Method` header matches the defined value. +/// +/// Usage: +/// +/// ```rust +/// # use gotham::{helpers::http::response::create_empty_response, +/// # hyper::{header::ACCESS_CONTROL_ALLOW_METHODS, Method, StatusCode}, +/// # router::builder::* +/// # }; +/// # use gotham_restful::matcher::AccessControlRequestMethodMatcher; +/// let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); +/// +/// # build_simple_router(|route| { +/// // use the matcher for your request +/// route.options("/foo") +/// .extend_route_matcher(matcher) +/// .to(|state| { +/// // we know that this is a CORS preflight for a PUT request +/// let mut res = create_empty_response(&state, StatusCode::NO_CONTENT); +/// res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap()); +/// (state, res) +/// }); +/// # }); +/// ``` +#[derive(Clone, Debug)] +pub struct AccessControlRequestMethodMatcher +{ + method : Method +} + +impl AccessControlRequestMethodMatcher +{ + /// Construct a new matcher that matches if the `Access-Control-Request-Method` header matches `method`. + /// Note that during matching the method is normalized according to the fetch specification, that is, + /// byte-uppercased. This means that when using a custom `method` instead of a predefined one, make sure + /// it is uppercased or this matcher will never succeed. + pub fn new(method : Method) -> Self + { + Self { method } + } +} + +impl RouteMatcher for AccessControlRequestMethodMatcher +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + // according to the fetch specification, methods should be normalized by byte-uppercase + // https://fetch.spec.whatwg.org/#concept-method + match HeaderMap::borrow_from(state).get(ACCESS_CONTROL_REQUEST_METHOD) + .and_then(|value| value.to_str().ok()) + .and_then(|str| str.to_ascii_uppercase().parse::().ok()) + { + Some(m) if m == self.method => Ok(()), + _ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND)) + } + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + + fn with_state(accept : Option<&str>, block : F) + where F : FnOnce(&mut State) -> () + { + State::with_new(|state| { + let mut headers = HeaderMap::new(); + if let Some(acc) = accept + { + headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap()); + } + state.put(headers); + block(state); + }); + } + + #[test] + fn no_acrm_header() + { + let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); + with_state(None, |state| assert!(matcher.is_match(&state).is_err())); + } + + #[test] + fn correct_acrm_header() + { + let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); + with_state(Some("PUT"), |state| assert!(matcher.is_match(&state).is_ok())); + with_state(Some("put"), |state| assert!(matcher.is_match(&state).is_ok())); + } + + #[test] + fn incorrect_acrm_header() + { + let matcher = AccessControlRequestMethodMatcher::new(Method::PUT); + with_state(Some("DELETE"), |state| assert!(matcher.is_match(&state).is_err())); + } +} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs new file mode 100644 index 0000000..cc7e734 --- /dev/null +++ b/src/matcher/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "cors")] +mod access_control_request_method; +#[cfg(feature = "cors")] +pub use access_control_request_method::AccessControlRequestMethodMatcher; + diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs index 4fa6a0d..bf81caa 100644 --- a/src/openapi/builder.rs +++ b/src/openapi/builder.rs @@ -1,26 +1,29 @@ +use crate::{OpenapiType, OpenapiSchema}; use indexmap::IndexMap; -use openapi_type::OpenapiSchema; use openapiv3::{ - Components, OpenAPI, PathItem, ReferenceOr, - ReferenceOr::{Item, Reference}, - Schema, Server + Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema, + Server }; use std::sync::{Arc, RwLock}; #[derive(Clone, Debug)] -pub struct OpenapiInfo { - pub title: String, - pub version: String, - pub urls: Vec +pub struct OpenapiInfo +{ + pub title : String, + pub version : String, + pub urls : Vec } #[derive(Clone, Debug)] -pub struct OpenapiBuilder { - pub openapi: Arc> +pub struct OpenapiBuilder +{ + pub openapi : Arc> } -impl OpenapiBuilder { - pub fn new(info: OpenapiInfo) -> Self { +impl OpenapiBuilder +{ + pub fn new(info : OpenapiInfo) -> Self + { Self { openapi: Arc::new(RwLock::new(OpenAPI { openapi: "3.0.2".to_string(), @@ -29,22 +32,18 @@ impl OpenapiBuilder { version: info.version, ..Default::default() }, - servers: info - .urls - .into_iter() - .map(|url| Server { - url, - ..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 { + 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, @@ -52,14 +51,16 @@ impl OpenapiBuilder { } } - pub fn add_path(&mut self, path: Path, item: PathItem) { + pub fn add_path(&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) { + 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) => { @@ -73,22 +74,25 @@ impl OpenapiBuilder { }; } - fn add_schema_dependencies(&mut self, dependencies: &mut IndexMap) { - let keys: Vec = dependencies.keys().map(|k| k.to_string()).collect(); - for dep in keys { + fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) + { + let keys : Vec = 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 { + if let Some(dep_schema) = dep_schema + { self.add_schema_impl(dep, dep_schema); } } } - - pub fn add_schema(&mut self, mut schema: OpenapiSchema) -> ReferenceOr { + + pub fn add_schema(&mut self) -> ReferenceOr + { + let mut schema = T::schema(); match schema.name.clone() { Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; self.add_schema_impl(name, schema); reference }, @@ -100,58 +104,59 @@ impl OpenapiBuilder { } } + #[cfg(test)] #[allow(dead_code)] -mod test { +mod test +{ use super::*; - use openapi_type::OpenapiType; - + #[derive(OpenapiType)] - struct Message { - msg: String + struct Message + { + msg : String } - + #[derive(OpenapiType)] - struct Messages { - msgs: Vec + struct Messages + { + msgs : Vec } - - fn info() -> OpenapiInfo { + + 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 { + + fn openapi(builder : OpenapiBuilder) -> OpenAPI + { Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap() } - + #[test] - fn new_builder() { + 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() { + fn add_schema() + { let mut builder = OpenapiBuilder::new(info()); - builder.add_schema(>::schema()); + builder.add_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()) - ); + + 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())); } } diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs index 9762ca6..2054b0d 100644 --- a/src/openapi/handler.rs +++ b/src/openapi/handler.rs @@ -1,39 +1,54 @@ -#![cfg_attr(not(feature = "auth"), allow(unused_imports))] use super::SECURITY_NAME; use futures_util::{future, future::FutureExt}; use gotham::{ - anyhow, + error::Result, 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 - }, + helpers::http::response::create_response, state::State }; use indexmap::IndexMap; -use mime::{APPLICATION_JSON, TEXT_HTML, TEXT_PLAIN}; -use once_cell::sync::Lazy; +use mime::{APPLICATION_JSON, TEXT_PLAIN}; use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme}; -use sha2::{Digest, Sha256}; use std::{ pin::Pin, sync::{Arc, RwLock} }; +#[derive(Clone)] +pub struct OpenapiHandler +{ + openapi : Arc> +} + +impl OpenapiHandler +{ + pub fn new(openapi : Arc>) -> Self + { + Self { openapi } + } +} + +impl NewHandler for OpenapiHandler +{ + type Instance = Self; + + fn new_handler(&self) -> Result + { + Ok(self.clone()) + } +} + #[cfg(feature = "auth")] -fn get_security(state: &mut State) -> IndexMap> { +fn get_security(state : &mut State) -> IndexMap> +{ 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, @@ -48,214 +63,48 @@ fn get_security(state: &mut State) -> IndexMap> = Default::default(); + + let mut security_schemes : IndexMap> = 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> { +fn get_security(state : &mut State) -> (Vec, IndexMap>) +{ Default::default() } -fn create_openapi_response(state: &mut State, openapi: &Arc>) -> Response { - 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> -} - -impl OpenapiHandler { - pub fn new(openapi: Arc>) -> Self { - Self { openapi } - } -} - -impl NewHandler for OpenapiHandler { - type Instance = Self; - - fn new_handler(&self) -> anyhow::Result { - Ok(self.clone()) - } -} - -impl Handler for OpenapiHandler { - fn handle(self, mut state: State) -> Pin> { - let res = create_openapi_response(&mut state, &self.openapi); - future::ok((state, res)).boxed() - } -} - -#[derive(Clone)] -pub struct SwaggerUiHandler { - openapi: Arc> -} - -impl SwaggerUiHandler { - pub fn new(openapi: Arc>) -> Self { - Self { openapi } - } -} - -impl NewHandler for SwaggerUiHandler { - type Instance = Self; - - fn new_handler(&self) -> anyhow::Result { - Ok(self.clone()) - } -} - -impl Handler for SwaggerUiHandler { - fn handle(self, mut state: State) -> Pin> { - 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); +impl Handler for OpenapiHandler +{ + fn handle(self, mut state : State) -> Pin> + { + let openapi = match self.openapi.read() { + Ok(openapi) => openapi, + Err(e) => { + error!("Unable to acquire read lock for the OpenAPI specification: {}", e); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + return future::ok((state, res)).boxed() + } + }; + + let mut openapi = openapi.clone(); + 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, crate::StatusCode::OK, APPLICATION_JSON, body); 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")); + Err(e) => { + error!("Unable to handle OpenAPI request due to error: {}", e); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); 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#" - - - - - - - - -

- - - - "# - }; - Box::leak(Box::new(template.replace("{{script}}", SWAGGER_UI_SCRIPT))) -}); -static SWAGGER_UI_HTML_ETAG: Lazy = 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 = Lazy::new(|| { - let mut hash = Sha256::new(); - hash.update(SWAGGER_UI_SCRIPT); - let hash = hash.finalize(); - base64::encode(hash) -}); diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs index 5eefc1f..141ea22 100644 --- a/src/openapi/mod.rs +++ b/src/openapi/mod.rs @@ -1,6 +1,8 @@ -const SECURITY_NAME: &str = "authToken"; + +const SECURITY_NAME : &str = "authToken"; pub mod builder; pub mod handler; pub mod operation; pub mod router; +pub mod types; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs index 1823b3c..fc06e43 100644 --- a/src/openapi/operation.rs +++ b/src/openapi/operation.rs @@ -1,48 +1,50 @@ +use crate::{ + resource::*, + result::*, + OpenapiSchema, + RequestBody +}; 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 + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, + ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, + StatusCode, Type }; + #[derive(Default)] -struct OperationParams { - path_params: Option, - query_params: Option +struct OperationParams<'a> +{ + path_params : Vec<(&'a str, ReferenceOr)>, + query_params : Option } -impl OperationParams { - fn add_path_params(path_params: Option, params: &mut Vec>) { - 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); +impl<'a> OperationParams<'a> +{ + fn add_path_params(&self, params : &mut Vec>) + { + for param in &self.path_params + { params.push(Item(Parameter::Path { parameter_data: ParameterData { - name, + name: (*param).0.to_string(), description: None, - required, + required: true, deprecated: None, - format: ParameterSchemaOrContent::Schema(schema.unbox()), + format: ParameterSchemaOrContent::Schema((*param).1.clone()), example: None, examples: IndexMap::new() }, - style: Default::default() - })) + style: Default::default(), + })); } } - - fn add_query_params(query_params: Option, params: &mut Vec>) { - let query_params = match query_params { + + fn add_query_params(self, params : &mut Vec>) + { + let query_params = match self.query_params { Some(qp) => qp.schema, None => return }; @@ -50,7 +52,8 @@ impl OperationParams { SchemaKind::Type(Type::Object(ty)) => ty, _ => panic!("Query Parameters needs to be a plain struct") }; - for (name, schema) in query_params.properties { + for (name, schema) in query_params.properties + { let required = query_params.required.contains(&name); params.push(Item(Parameter::Query { parameter_data: ParameterData { @@ -68,56 +71,69 @@ impl OperationParams { })) } } - - fn into_params(self) -> Vec> { - let mut params: Vec> = Vec::new(); - Self::add_path_params(self.path_params, &mut params); - Self::add_query_params(self.query_params, &mut params); + + fn into_params(self) -> Vec> + { + let mut params : Vec> = Vec::new(); + self.add_path_params(&mut params); + self.add_query_params(&mut params); params } } -pub struct OperationDescription { - operation_id: Option, - default_status: gotham::hyper::StatusCode, - accepted_types: Option>, - schema: ReferenceOr, - params: OperationParams, - body_schema: Option>, - supported_types: Option>, - requires_auth: bool +pub struct OperationDescription<'a> +{ + operation_id : Option, + default_status : crate::StatusCode, + accepted_types : Option>, + schema : ReferenceOr, + params : OperationParams<'a>, + body_schema : Option>, + supported_types : Option>, + requires_auth : bool } -impl OperationDescription { - pub fn new(schema: ReferenceOr) -> Self { +impl<'a> OperationDescription<'a> +{ + pub fn new(schema : ReferenceOr) -> Self + { Self { - operation_id: E::operation_id(), - default_status: E::Output::default_status(), - accepted_types: E::Output::accepted_types(), + operation_id: Handler::operation_id(), + default_status: Handler::Res::default_status(), + accepted_types: Handler::Res::accepted_types(), schema, params: Default::default(), body_schema: None, supported_types: None, - requires_auth: E::wants_auth() + requires_auth: Handler::wants_auth() } } - - pub fn set_path_params(&mut self, params: OpenapiSchema) { - self.params.path_params = Some(params); + + pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr) -> Self + { + self.params.path_params.push((name, schema)); + self } - - pub fn set_query_params(&mut self, params: OpenapiSchema) { + + pub fn with_query_params(mut self, params : OpenapiSchema) -> Self + { self.params.query_params = Some(params); + self } - - pub fn set_body(&mut self, schema: ReferenceOr) { + + pub fn with_body(mut self, schema : ReferenceOr) -> Self + { self.body_schema = Some(schema); self.supported_types = Body::supported_types(); + self } - - fn schema_to_content(types: Vec, schema: ReferenceOr) -> IndexMap { - let mut content: IndexMap = IndexMap::new(); - for ty in types { + + + fn schema_to_content(types : Vec, schema : ReferenceOr) -> IndexMap + { + let mut content : IndexMap = IndexMap::new(); + for ty in types + { content.insert(ty.to_string(), MediaType { schema: Some(schema.clone()), ..Default::default() @@ -125,47 +141,36 @@ impl OperationDescription { } content } - - pub fn into_operation(self) -> Operation { + + pub fn into_operation(self) -> Operation + { // this is unfortunately neccessary to prevent rust from complaining about partially moving self let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = ( - self.operation_id, - self.default_status, - self.accepted_types, - self.schema, - self.params, - self.body_schema, - self.supported_types, - self.requires_auth - ); - + self.operation_id, self.default_status, self.accepted_types, self.schema, self.params, self.body_schema, self.supported_types, self.requires_auth); + let content = Self::schema_to_content(accepted_types.or_all_types(), schema); - - let mut responses: IndexMap> = 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 responses : IndexMap> = 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 { + 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, @@ -182,23 +187,27 @@ impl OperationDescription { } } -#[cfg(test)] -mod test { - use super::*; - use crate::{NoContent, Raw, ResponseSchema}; +#[cfg(test)] +mod test +{ + use crate::{OpenapiType, ResourceResult}; + use super::*; + #[test] - fn no_content_schema_to_content() { + fn no_content_schema_to_content() + { let types = NoContent::accepted_types(); - let schema = ::schema(); + let schema = ::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() { + fn raw_schema_to_content() + { let types = Raw::<&str>::accepted_types(); - let schema = as ResponseSchema>::schema(); + let schema = as OpenapiType>::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(); diff --git a/src/openapi/router.rs b/src/openapi/router.rs index e6b3187..6dd7a13 100644 --- a/src/openapi/router.rs +++ b/src/openapi/router.rs @@ -1,38 +1,40 @@ -use super::{ - builder::OpenapiBuilder, - handler::{OpenapiHandler, SwaggerUiHandler}, - operation::OperationDescription +use crate::{ + resource::*, + routing::*, + OpenapiType, +}; +use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription}; +use gotham::{ + pipeline::chain::PipelineHandleChain, + router::builder::* }; -use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema}; -use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; -use once_cell::sync::Lazy; -use openapi_type::OpenapiType; -use regex::{Captures, Regex}; use std::panic::RefUnwindSafe; -/// 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); +/// This trait adds the `get_openapi` method to an OpenAPI-aware router. +pub trait GetOpenapi +{ + fn get_openapi(&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 +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

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - pub fn scope(&mut self, path: &str, callback: F) + pub fn scope(&mut self, path : &str, callback : F) where - F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>) + 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("//", "/")); @@ -46,88 +48,147 @@ macro_rules! implOpenapiRouter { }); } } - + impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>> where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ 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())); + fn get_openapi(&mut self, path : &str) + { + self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone())); } } - - impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>> + + impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>> where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - fn resource(&mut self, path: &str) { + fn resource(&mut self, path : &str) + { R::setup((self, path)); } } - impl<'a, 'b, C, P> DrawResourceRoutesWithSchema for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) + impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str) where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - fn endpoint(&mut self) { - let schema = (self.0).openapi_builder.add_schema(E::Output::schema()); - let mut descr = OperationDescription::new::(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::(body_schema); - } - - static URI_PLACEHOLDER_REGEX: Lazy = - Lazy::new(|| Regex::new(r#"(?P^|/):(?P[^/]+)(?P/|$)"#).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(); + fn read_all(&mut self) + { + let schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); 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) - }; + item.get = Some(OperationDescription::new::(schema).into_operation()); (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).read_all::() + } + + fn read(&mut self) + { + let schema = (self.0).openapi_builder.add_schema::(); + let id_schema = (self.0).openapi_builder.add_schema::(); - (&mut *(self.0).router, self.1).endpoint::() + let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.get = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).read::() + } + + fn search(&mut self) + { + let schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.get = Some(OperationDescription::new::(schema).with_query_params(Handler::Query::schema()).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).search::() + } + + fn create(&mut self) + where + Handler::Res : 'static, + Handler::Body : 'static + { + let schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.post = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).create::() + } + + fn change_all(&mut self) + where + Handler::Res : 'static, + Handler::Body : 'static + { + let schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.put = Some(OperationDescription::new::(schema).with_body::(body_schema).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).change_all::() + } + + fn change(&mut self) + where + Handler::Res : 'static, + Handler::Body : 'static + { + let schema = (self.0).openapi_builder.add_schema::(); + let id_schema = (self.0).openapi_builder.add_schema::(); + let body_schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.put = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).with_body::(body_schema).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).change::() + } + + fn remove_all(&mut self) + { + let schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.delete = Some(OperationDescription::new::(schema).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).remove_all::() + } + + fn remove(&mut self) + { + let schema = (self.0).openapi_builder.add_schema::(); + let id_schema = (self.0).openapi_builder.add_schema::(); + + let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1); + let mut item = (self.0).openapi_builder.remove_path(&path); + item.delete = Some(OperationDescription::new::(schema).add_path_param("id", id_schema).into_operation()); + (self.0).openapi_builder.add_path(path, item); + + (&mut *(self.0).router, self.1).remove::() } } - }; + + } } implOpenapiRouter!(RouterBuilder); diff --git a/src/openapi/types.rs b/src/openapi/types.rs new file mode 100644 index 0000000..c7ff5e4 --- /dev/null +++ b/src/openapi/types.rs @@ -0,0 +1,416 @@ +#[cfg(feature = "chrono")] +use chrono::{ + Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc +}; +use indexmap::IndexMap; +use openapiv3::{ + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr::Item, + ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty +}; +#[cfg(feature = "uuid")] +use uuid::Uuid; +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + hash::BuildHasher +}; + +/** +This struct needs to be available for every type that can be part of an OpenAPI Spec. It is +already implemented for primitive types, String, Vec, Option and the like. To have it available +for your type, simply derive from [`OpenapiType`]. + +[`OpenapiType`]: trait.OpenapiType.html +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema +{ + /// The name of this schema. If it is None, the schema will be inlined. + pub name : Option, + /// 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 +} + +impl OpenapiSchema +{ + /// Create a new schema that has no name. + pub fn new(schema : SchemaKind) -> Self + { + Self { + name: None, + nullable: false, + schema, + dependencies: IndexMap::new() + } + } + + /// Convert this schema to an `openapiv3::Schema` that can be serialized to the OpenAPI Spec. + pub fn into_schema(self) -> Schema + { + Schema { + schema_data: SchemaData { + nullable: self.nullable, + title: self.name, + ..Default::default() + }, + schema_kind: self.schema + } + } +} + +/** +This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives +access to the [`OpenapiSchema`] of this type. It is provided for primitive types, String and the +like. For use on your own types, there is a derive macro: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# +#[derive(OpenapiType)] +struct MyResponse { + message: String +} +``` + +[`OpenapiSchema`]: struct.OpenapiSchema.html +*/ +pub trait OpenapiType +{ + fn schema() -> OpenapiSchema; +} + +impl OpenapiType for () +{ + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Any(false)), + ..Default::default() + }))) + } +} + +impl OpenapiType for bool +{ + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Boolean{})) + } +} + +macro_rules! int_types { + ($($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) + } + } + )*}; + + (unsigned $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; + + (bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + ..Default::default() + }))) + } + } + )*}; + + (unsigned bits = $bits:expr, $($int_ty:ty),*) => {$( + impl OpenapiType for $int_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { + format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)), + minimum: Some(0), + ..Default::default() + }))) + } + } + )*}; +} + +int_types!(isize); +int_types!(unsigned usize); +int_types!(bits = 8, i8); +int_types!(unsigned bits = 8, u8); +int_types!(bits = 16, i16); +int_types!(unsigned bits = 16, u16); +int_types!(bits = 32, i32); +int_types!(unsigned bits = 32, u32); +int_types!(bits = 64, i64); +int_types!(unsigned bits = 64, u64); +int_types!(bits = 128, i128); +int_types!(unsigned bits = 128, u128); + +macro_rules! num_types { + ($($num_ty:ty = $num_fmt:ident),*) => {$( + impl OpenapiType for $num_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType { + format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt), + ..Default::default() + }))) + } + } + )*} +} + +num_types!(f32 = Float, f64 = Double); + +macro_rules! str_types { + ($($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default()))) + } + } + )*}; + + (format = $format:ident, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + use openapiv3::StringFormat; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::$format), + ..Default::default() + }))) + } + } + )*}; + + (format_str = $format:expr, $($str_ty:ty),*) => {$( + impl OpenapiType for $str_ty + { + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Unknown($format.to_string()), + ..Default::default() + }))) + } + } + )*}; +} + +str_types!(String, &str); + +#[cfg(feature = "chrono")] +str_types!(format = Date, Date, Date, Date, NaiveDate); +#[cfg(feature = "chrono")] +str_types!(format = DateTime, DateTime, DateTime, DateTime, NaiveDateTime); + +#[cfg(feature = "uuid")] +str_types!(format_str = "uuid", Uuid); + +impl OpenapiType for Option +{ + 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 OpenapiType for Vec +{ + 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 OpenapiType for BTreeSet +{ + fn schema() -> OpenapiSchema + { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashSet +{ + fn schema() -> OpenapiSchema + { + as OpenapiType>::schema() + } +} + +impl OpenapiType for HashMap +{ + fn schema() -> OpenapiSchema + { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + + let items = Box::new(match schema.name.clone() { + Some(name) => { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + dependencies.insert(name, schema); + reference + }, + None => Item(schema.into_schema()) + }); + + OpenapiSchema { + nullable: false, + name: None, + schema: SchemaKind::Type(Type::Object(ObjectType { + additional_properties: Some(AdditionalProperties::Schema(items)), + ..Default::default() + })), + dependencies + } + } +} + +impl OpenapiType for serde_json::Value +{ + fn schema() -> OpenapiSchema + { + OpenapiSchema { + nullable: true, + name: None, + schema: SchemaKind::Any(Default::default()), + dependencies: Default::default() + } + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + use serde_json::Value; + + type Unit = (); + + macro_rules! assert_schema { + ($ty:ident $(<$($generic:ident),+>)* => $json:expr) => { + paste::item! { + #[test] + fn []() + { + let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema().into_schema(); + let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty))); + assert_eq!(schema_json, $json); + } + } + }; + } + + assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#); + assert_schema!(bool => r#"{"type":"boolean"}"#); + assert_schema!(isize => r#"{"type":"integer"}"#); + assert_schema!(usize => r#"{"type":"integer","minimum":0}"#); + assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#); + assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#); + assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#); + assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#); + assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#); + assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#); + assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#); + assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#); + assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#); + assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#); + assert_schema!(f32 => r#"{"type":"number","format":"float"}"#); + assert_schema!(f64 => r#"{"type":"number","format":"double"}"#); + + assert_schema!(String => r#"{"type":"string"}"#); + #[cfg(feature = "chrono")] + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + #[cfg(feature = "chrono")] + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + #[cfg(feature = "chrono")] + assert_schema!(Date => r#"{"type":"string","format":"date"}"#); + #[cfg(feature = "chrono")] + assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#); + #[cfg(feature = "chrono")] + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + #[cfg(feature = "chrono")] + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + #[cfg(feature = "chrono")] + assert_schema!(DateTime => r#"{"type":"string","format":"date-time"}"#); + #[cfg(feature = "chrono")] + assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#); + #[cfg(feature = "uuid")] + assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#); + + assert_schema!(Option => r#"{"nullable":true,"type":"string"}"#); + assert_schema!(Vec => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(BTreeSet => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(HashSet => r#"{"type":"array","items":{"type":"string"}}"#); + assert_schema!(HashMap => r#"{"type":"object","additionalProperties":{"type":"string"}}"#); + assert_schema!(Value => r#"{"nullable":true}"#); +} diff --git a/src/resource.rs b/src/resource.rs new file mode 100644 index 0000000..daedd3d --- /dev/null +++ b/src/resource.rs @@ -0,0 +1,118 @@ +use crate::{DrawResourceRoutes, RequestBody, ResourceID, ResourceResult, ResourceType}; +use gotham::{ + extractor::QueryStringExtractor, + hyper::Body, + state::State +}; +use std::{ + future::Future, + pin::Pin +}; + +/// This trait must be implemented for every resource. It allows you to register the different +/// methods that can be handled by this resource to be registered with the underlying router. +/// +/// It is not recommended to implement this yourself, rather just use `#[derive(Resource)]`. +pub trait Resource +{ + /// Register all methods handled by this resource with the underlying router. + fn setup(route : D); +} + +/// A common trait for every resource method. It defines the return type as well as some general +/// information about a resource method. +/// +/// It is not recommended to implement this yourself. Rather, just write your handler method and +/// annotate it with `#[(YourResource)]`, where `` is one of the supported +/// resource methods. +pub trait ResourceMethod +{ + type Res : ResourceResult + Send + 'static; + + #[cfg(feature = "openapi")] + fn operation_id() -> Option + { + None + } + + fn wants_auth() -> bool + { + false + } +} + +/// The read_all [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceReadAll : ResourceMethod +{ + /// Handle a GET request on the Resource root. + fn read_all(state : State) -> Pin + Send>>; +} + +/// The read [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceRead : ResourceMethod +{ + /// The ID type to be parsed from the request path. + type ID : ResourceID + 'static; + + /// Handle a GET request on the Resource with an id. + fn read(state : State, id : Self::ID) -> Pin + Send>>; +} + +/// The search [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceSearch : ResourceMethod +{ + /// The Query type to be parsed from the request parameters. + type Query : ResourceType + QueryStringExtractor + Sync; + + /// Handle a GET request on the Resource with additional search parameters. + fn search(state : State, query : Self::Query) -> Pin + Send>>; +} + +/// The create [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceCreate : ResourceMethod +{ + /// The Body type to be parsed from the request body. + type Body : RequestBody; + + /// Handle a POST request on the Resource root. + fn create(state : State, body : Self::Body) -> Pin + Send>>; +} + +/// The change_all [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceChangeAll : ResourceMethod +{ + /// The Body type to be parsed from the request body. + type Body : RequestBody; + + /// Handle a PUT request on the Resource root. + fn change_all(state : State, body : Self::Body) -> Pin + Send>>; +} + +/// The change [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceChange : ResourceMethod +{ + /// The Body type to be parsed from the request body. + type Body : RequestBody; + /// The ID type to be parsed from the request path. + type ID : ResourceID + 'static; + + /// Handle a PUT request on the Resource with an id. + fn change(state : State, id : Self::ID, body : Self::Body) -> Pin + Send>>; +} + +/// The remove_all [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceRemoveAll : ResourceMethod +{ + /// Handle a DELETE request on the Resource root. + fn remove_all(state : State) -> Pin + Send>>; +} + +/// The remove [`ResourceMethod`](trait.ResourceMethod.html). +pub trait ResourceRemove : ResourceMethod +{ + /// The ID type to be parsed from the request path. + type ID : ResourceID + 'static; + + /// Handle a DELETE request on the Resource with an id. + fn remove(state : State, id : Self::ID) -> Pin + Send>>; +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..dbbf8c7 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,64 @@ +use gotham::hyper::{Body, StatusCode}; +use mime::{Mime, APPLICATION_JSON}; + +/// A response, used to create the final gotham response from. +#[derive(Debug)] +pub struct Response +{ + pub status : StatusCode, + pub body : Body, + pub mime : Option +} + +impl Response +{ + /// Create a new `Response` from raw data. + pub fn new>(status : StatusCode, body : B, mime : Option) -> Self + { + Self { + status, + body: body.into(), + mime + } + } + + /// Create a `Response` with mime type json from already serialized data. + pub fn json>(status : StatusCode, body : B) -> Self + { + Self { + status, + body: body.into(), + mime: Some(APPLICATION_JSON) + } + } + + /// Create a _204 No Content_ `Response`. + pub fn no_content() -> Self + { + Self { + status: StatusCode::NO_CONTENT, + body: Body::empty(), + mime: None + } + } + + /// Create an empty _403 Forbidden_ `Response`. + pub fn forbidden() -> Self + { + Self { + status: StatusCode::FORBIDDEN, + body: Body::empty(), + mime: None + } + } + + #[cfg(test)] + pub(crate) fn full_body(mut self) -> Result, ::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()) + } +} \ No newline at end of file diff --git a/src/response/mod.rs b/src/response/mod.rs deleted file mode 100644 index bdf7c66..0000000 --- a/src/response/mod.rs +++ /dev/null @@ -1,282 +0,0 @@ -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; -} - -impl OrAllTypes for Option> { - fn or_all_types(self) -> Vec { - 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, - 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>(status: StatusCode, body: B, mime: Option) -> 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>(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, ::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> { - 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 + 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>; - - /// Return a list of supported mime types. - fn accepted_types() -> Option> { - 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 private::Sealed for R {} - -#[cfg(feature = "openapi")] -impl 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 From for ResourceError { - fn from(message: T) -> Self { - Self { - error: true, - message: message.to_string() - } - } -} - -#[cfg(feature = "errorlog")] -fn errorlog(e: E) { - error!("The handler encountered an error: {}", e); -} - -#[cfg(not(feature = "errorlog"))] -fn errorlog(_e: E) {} - -fn handle_error(e: E) -> Pin> + 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 IntoResponse for Pin + Send>> -where - Res: IntoResponse + 'static -{ - type Err = Res::Err; - - fn into_response(self) -> Pin> + Send>> { - self.then(IntoResponse::into_response).boxed() - } - - fn accepted_types() -> Option> { - Res::accepted_types() - } -} - -#[cfg(feature = "openapi")] -impl ResponseSchema for Pin + 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()); - } -} diff --git a/src/response/no_content.rs b/src/response/no_content.rs deleted file mode 100644 index 3a10b3b..0000000 --- a/src/response/no_content.rs +++ /dev/null @@ -1,159 +0,0 @@ -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(&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` - - /// This will always be a _204 No Content_ together with an empty string. - fn into_response(self) -> Pin> + Send>> { - future::ok(Response::no_content().with_headers(self.headers)).boxed() - } - - fn accepted_types() -> Option> { - 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 IntoResponse for Result -where - E: Display + IntoResponseError -{ - type Err = serde_json::Error; - - fn into_response(self) -> Pin> + Send>> { - match self { - Ok(nc) => nc.into_response(), - Err(e) => handle_error(e) - } - } - - fn accepted_types() -> Option> { - NoContent::accepted_types() - } -} - -#[cfg(feature = "openapi")] -impl ResponseSchema for Result -where - E: Display + IntoResponseError -{ - fn schema() -> OpenapiSchema { - ::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 = 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("*")); - } -} diff --git a/src/response/redirect.rs b/src/response/redirect.rs deleted file mode 100644 index f1edd82..0000000 --- a/src/response/redirect.rs +++ /dev/null @@ -1,151 +0,0 @@ -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> { - 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 { - ::schema() - } -} - -// private type due to parent mod -#[derive(Debug, Error)] -pub enum RedirectError { - #[error("{0}")] - InvalidLocation(#[from] InvalidHeaderValue), - #[error("{0}")] - Other(#[source] E) -} - -#[allow(ambiguous_associated_items)] // an enum variant is not a type. never. -impl IntoResponse for Result -where - E: Display + IntoResponseError, - ::Err: StdError + Sync -{ - type Err = RedirectError<::Err>; - - fn into_response(self) -> BoxFuture<'static, Result> { - 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 ResponseSchema for Result -where - E: Display + IntoResponseError, - ::Err: StdError + Sync -{ - fn default_status() -> StatusCode { - Redirect::default_status() - } - - fn schema() -> OpenapiSchema { - ::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 = 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]); - } -} diff --git a/src/response/result.rs b/src/response/result.rs deleted file mode 100644 index f0ddc91..0000000 --- a/src/response/result.rs +++ /dev/null @@ -1,105 +0,0 @@ -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; -} - -impl IntoResponseError for E { - type Err = serde_json::Error; - - fn into_response_error(self) -> Result { - let err: ResourceError = self.into(); - Ok(Response::json( - StatusCode::INTERNAL_SERVER_ERROR, - serde_json::to_string(&err)? - )) - } -} - -impl IntoResponse for Result -where - R: ResponseBody, - E: Display + IntoResponseError -{ - type Err = E::Err; - - fn into_response(self) -> Pin> + Send>> { - match self { - Ok(r) => Success::from(r).into_response(), - Err(e) => handle_error(e) - } - } - - fn accepted_types() -> Option> { - Some(vec![APPLICATION_JSON]) - } -} - -#[cfg(feature = "openapi")] -impl ResponseSchema for Result -where - R: ResponseBody, - E: Display + IntoResponseError -{ - 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 = 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 = 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!(>::accepted_types() - .or_all_types() - .contains(&APPLICATION_JSON)) - } -} diff --git a/src/response/success.rs b/src/response/success.rs deleted file mode 100644 index 31f9374..0000000 --- a/src/response/success.rs +++ /dev/null @@ -1,130 +0,0 @@ -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 { - let res = MyResponse { message: "I'm always happy" }; - res.into() -} -# } -``` -*/ -#[derive(Clone, Debug, Default)] -pub struct Success { - value: T, - headers: HeaderMap -} - -impl From for Success { - fn from(t: T) -> Self { - Self { - value: t, - headers: HeaderMap::new() - } - } -} - -impl Success { - /// Set a custom HTTP header. If a header with this name was set before, its value is being updated. - pub fn header(&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 Success { - type Err = serde_json::Error; - - fn into_response(self) -> Pin> + 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> { - Some(vec![APPLICATION_JSON]) - } -} - -#[cfg(feature = "openapi")] -impl ResponseSchema for Success { - 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::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!(>::default_status(), StatusCode::OK); - } - - #[test] - fn success_custom_headers() { - let mut success: Success = 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!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) - } -} diff --git a/src/response/auth_result.rs b/src/result/auth_result.rs similarity index 67% rename from src/response/auth_result.rs rename to src/result/auth_result.rs index d8b3300..10f0183 100644 --- a/src/response/auth_result.rs +++ b/src/result/auth_result.rs @@ -1,22 +1,25 @@ 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]. +combination with [`AuthSuccess`] or [`AuthResult`]. + + [`AuthSuccess`]: type.AuthSuccess.html + [`AuthResult`]: type.AuthResult.html */ #[derive(Debug, Clone, Copy, ResourceError)] -pub enum AuthError { +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): +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): ```rust # #[macro_use] extern crate gotham_restful_derive; @@ -33,7 +36,7 @@ Use can look something like this (assuming the `auth` feature is enabled): # #[derive(Clone, Deserialize)] # struct MyAuthData { exp : u64 } # -#[read_all] +#[read_all(MyResource)] fn read_all(auth : AuthStatus) -> AuthSuccess { let auth_data = match auth { AuthStatus::Authenticated(data) => data, @@ -49,10 +52,13 @@ pub type AuthSuccess = Result; /** This is an error type that either yields a _403 Forbidden_ respone if produced from an authentication -error, or delegates to another error type. This type is best used with [AuthResult]. +error, or delegates to another error type. This type is best used with [`AuthResult`]. + + [`AuthResult`]: type.AuthResult.html */ #[derive(Debug, ResourceError)] -pub enum AuthErrorOrOther { +pub enum AuthErrorOrOther +{ #[status(FORBIDDEN)] #[display("Forbidden")] Forbidden, @@ -61,36 +67,31 @@ pub enum AuthErrorOrOther { Other(E) } -impl From for AuthErrorOrOther { - fn from(err: AuthError) -> Self { +impl From for AuthErrorOrOther +{ + fn from(err : AuthError) -> Self + { match err { AuthError::Forbidden => Self::Forbidden } } } -mod private { - use gotham::handler::HandlerError; - pub trait Sealed {} - impl> Sealed for E {} -} - impl From for AuthErrorOrOther where // TODO https://gitlab.com/msrd0/gotham-restful/-/issues/20 - F: private::Sealed + Into + F : std::error::Error + Into { - fn from(err: F) -> Self { + 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): +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; @@ -108,7 +109,7 @@ Use can look something like this (assuming the `auth` feature is enabled): # #[derive(Clone, Deserialize)] # struct MyAuthData { exp : u64 } # -#[read_all] +#[read_all(MyResource)] fn read_all(auth : AuthStatus) -> AuthResult { let auth_data = match auth { AuthStatus::Authenticated(data) => data, diff --git a/src/result/mod.rs b/src/result/mod.rs new file mode 100644 index 0000000..314b9ac --- /dev/null +++ b/src/result/mod.rs @@ -0,0 +1,191 @@ +use crate::Response; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_util::future::FutureExt; +use mime::{Mime, STAR_STAR}; +use serde::Serialize; +use std::{ + error::Error, + future::Future, + fmt::{Debug, Display}, + pin::Pin +}; + +mod auth_result; +pub use auth_result::{AuthError, AuthErrorOrOther, AuthResult, AuthSuccess}; + +mod no_content; +pub use no_content::NoContent; + +mod raw; +pub use raw::Raw; + +#[allow(clippy::module_inception)] +mod result; +pub use result::IntoResponseError; + +mod success; +pub use success::Success; + + +pub(crate) trait OrAllTypes +{ + fn or_all_types(self) -> Vec; +} + +impl OrAllTypes for Option> +{ + fn or_all_types(self) -> Vec + { + self.unwrap_or_else(|| vec![STAR_STAR]) + } +} + + +/// A trait provided to convert a resource's result to json. +pub trait ResourceResult +{ + type Err : Error + Send + 'static; + + /// Turn this into a response that can be returned to the browser. This api will likely + /// change in the future. + fn into_response(self) -> Pin> + Send>>; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> + { + None + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema; + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + crate::StatusCode::OK + } +} + +#[cfg(feature = "openapi")] +impl crate::OpenapiType for Res +{ + fn schema() -> OpenapiSchema + { + Self::schema() + } +} + +/// The default json returned on an 500 Internal Server Error. +#[derive(Debug, Serialize)] +pub(crate) struct ResourceError +{ + error : bool, + message : String +} + +impl From for ResourceError +{ + fn from(message : T) -> Self + { + Self { + error: true, + message: message.to_string() + } + } +} + +fn into_response_helper(create_response : F) -> Pin> + Send>> +where + Err : Send + 'static, + F : FnOnce() -> Result +{ + let res = create_response(); + async move { res }.boxed() +} + +#[cfg(feature = "errorlog")] +fn errorlog(e : E) +{ + error!("The handler encountered an error: {}", e); +} + +#[cfg(not(feature = "errorlog"))] +fn errorlog(_e : E) {} + +fn handle_error(e : E) -> Pin> + Send>> +where + E : Display + IntoResponseError +{ + into_response_helper(|| { + errorlog(&e); + e.into_response_error() + }) +} + + +impl ResourceResult for Pin + Send>> +where + Res : ResourceResult + 'static +{ + type Err = Res::Err; + + fn into_response(self) -> Pin> + Send>> + { + self.then(|result| { + result.into_response() + }).boxed() + } + + fn accepted_types() -> Option> + { + Res::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + Res::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + Res::default_status() + } +} + + + +#[cfg(test)] +mod test +{ + use super::*; + use futures_executor::block_on; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg + { + msg : String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn result_from_future() + { + let nc = NoContent::default(); + let res = block_on(nc.into_response()).unwrap(); + + let fut_nc = async move { NoContent::default() }.boxed(); + let fut_res = block_on(fut_nc.into_response()).unwrap(); + + assert_eq!(res.status, fut_res.status); + assert_eq!(res.mime, fut_res.mime); + assert_eq!(res.full_body().unwrap(), fut_res.full_body().unwrap()); + } +} diff --git a/src/result/no_content.rs b/src/result/no_content.rs new file mode 100644 index 0000000..3377b66 --- /dev/null +++ b/src/result/no_content.rs @@ -0,0 +1,141 @@ +use super::{ResourceResult, handle_error}; +use crate::{IntoResponseError, Response}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, OpenapiType}; +use futures_util::{future, future::FutureExt}; +use mime::Mime; +use std::{ + fmt::Display, + future::Future, + pin::Pin +}; + +/** +This is the return type of a resource that doesn't actually return something. It will result +in a _204 No Content_ answer by default. You don't need to use this type directly if using +the function attributes: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# +# #[derive(Resource)] +# #[resource(read_all)] +# struct MyResource; +# +#[read_all(MyResource)] +fn read_all(_state: &mut State) { + // do something +} +# } +``` +*/ +#[derive(Clone, Copy, Debug, Default)] +pub struct NoContent; + +impl From<()> for NoContent +{ + fn from(_ : ()) -> Self + { + Self {} + } +} + +impl ResourceResult for NoContent +{ + // TODO this shouldn't be a serde_json::Error + type Err = serde_json::Error; // just for easier handling of `Result` + + /// This will always be a _204 No Content_ together with an empty string. + fn into_response(self) -> Pin> + Send>> + { + future::ok(Response::no_content()).boxed() + } + + fn accepted_types() -> Option> + { + Some(Vec::new()) + } + + /// Returns the schema of the `()` type. + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + <()>::schema() + } + + /// This will always be a _204 No Content_ + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + crate::StatusCode::NO_CONTENT + } +} + +impl ResourceResult for Result +where + E : Display + IntoResponseError +{ + type Err = serde_json::Error; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(nc) => nc.into_response(), + Err(e) => handle_error(e) + } + } + + fn accepted_types() -> Option> + { + NoContent::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + ::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> crate::StatusCode + { + NoContent::default_status() + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + use futures_executor::block_on; + use gotham::hyper::StatusCode; + use thiserror::Error; + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn no_content_has_empty_response() + { + let no_content = NoContent::default(); + let res = block_on(no_content.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body().unwrap(), &[] as &[u8]); + } + + #[test] + fn no_content_result() + { + let no_content : Result = 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]); + } +} diff --git a/src/response/raw.rs b/src/result/raw.rs similarity index 55% rename from src/response/raw.rs rename to src/result/raw.rs index 3722146..a44e15a 100644 --- a/src/response/raw.rs +++ b/src/result/raw.rs @@ -1,26 +1,24 @@ -use super::{handle_error, IntoResponse, IntoResponseError}; -use crate::{FromBody, RequestBody, ResourceType, Response}; +use super::{IntoResponseError, ResourceResult, handle_error}; +use crate::{FromBody, RequestBody, ResourceType, Response, StatusCode}; #[cfg(feature = "openapi")] -use crate::{IntoResponseWithSchema, ResponseSchema}; -#[cfg(feature = "openapi")] -use openapi_type::{OpenapiSchema, OpenapiType}; - +use crate::OpenapiSchema; use futures_core::future::Future; use futures_util::{future, future::FutureExt}; -use gotham::hyper::{ - body::{Body, Bytes}, - StatusCode -}; +use gotham::hyper::body::{Body, Bytes}; 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}; +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 +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 @@ -31,7 +29,7 @@ example that simply returns its body: #[resource(create)] struct ImageResource; -#[create] +#[create(ImageResource)] fn create(body : Raw>) -> Raw> { body } @@ -41,39 +39,48 @@ fn create(body : Raw>) -> Raw> { # })); # } ``` + + [`OpenapiType`]: trait.OpenapiType.html */ #[derive(Debug)] -pub struct Raw { - pub raw: T, - pub mime: Mime +pub struct Raw +{ + pub raw : T, + pub mime : Mime } -impl Raw { - pub fn new(raw: T, mime: Mime) -> Self { +impl Raw +{ + pub fn new(raw : T, mime : Mime) -> Self + { Self { raw, mime } } } impl AsMut for Raw where - T: AsMut + T : AsMut { - fn as_mut(&mut self) -> &mut U { + fn as_mut(&mut self) -> &mut U + { self.raw.as_mut() } } impl AsRef for Raw where - T: AsRef + T : AsRef { - fn as_ref(&self) -> &U { + fn as_ref(&self) -> &U + { self.raw.as_ref() } } -impl Clone for Raw { - fn clone(&self) -> Self { +impl Clone for Raw +{ + fn clone(&self) -> Self + { Self { raw: self.raw.clone(), mime: self.mime.clone() @@ -81,19 +88,36 @@ impl Clone for Raw { } } -impl From<&'a [u8]>> FromBody for Raw { +impl From<&'a [u8]>> FromBody for Raw +{ type Err = Infallible; - - fn from_body(body: Bytes, mime: Mime) -> Result { + + fn from_body(body : Bytes, mime : Mime) -> Result + { Ok(Self::new(body.as_ref().into(), mime)) } } -impl RequestBody for Raw where Raw: FromBody + ResourceType {} +impl RequestBody for Raw +where + Raw : FromBody + ResourceType +{ +} -#[cfg(feature = "openapi")] -impl OpenapiType for Raw { - fn schema() -> OpenapiSchema { +impl> ResourceResult for Raw +where + Self : Send +{ + type Err = SerdeJsonError; // just for easier handling of `Result, E>` + + fn into_response(self) -> Pin> + Send>> + { + future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))).boxed() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), ..Default::default() @@ -101,61 +125,39 @@ impl OpenapiType for Raw { } } -impl> IntoResponse for Raw +impl ResourceResult for Result, E> where - Self: Send -{ - type Err = SerdeJsonError; // just for easier handling of `Result, E>` - - fn into_response(self) -> Pin> + Send>> { - future::ok(Response::new(StatusCode::OK, self.raw, Some(self.mime))).boxed() - } -} - -#[cfg(feature = "openapi")] -impl> ResponseSchema for Raw -where - Self: Send -{ - fn schema() -> OpenapiSchema { - ::schema() - } -} - -impl IntoResponse for Result, E> -where - Raw: IntoResponse, - E: Display + IntoResponseError as IntoResponse>::Err> + Raw : ResourceResult, + E : Display + IntoResponseError as ResourceResult>::Err> { type Err = E::Err; - - fn into_response(self) -> Pin> + Send>> { + + fn into_response(self) -> Pin> + Send>> + { match self { Ok(raw) => raw.into_response(), Err(e) => handle_error(e) } } -} - -#[cfg(feature = "openapi")] -impl ResponseSchema for Result, E> -where - Raw: IntoResponseWithSchema, - E: Display + IntoResponseError as IntoResponse>::Err> -{ - fn schema() -> OpenapiSchema { - as ResponseSchema>::schema() + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + as ResourceResult>::schema() } } + #[cfg(test)] -mod test { +mod test +{ use super::*; use futures_executor::block_on; use mime::TEXT_PLAIN; - + #[test] - fn raw_response() { + 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"); diff --git a/src/result/result.rs b/src/result/result.rs new file mode 100644 index 0000000..5de2e44 --- /dev/null +++ b/src/result/result.rs @@ -0,0 +1,106 @@ +use super::{ResourceResult, handle_error, into_response_helper}; +use crate::{ + result::ResourceError, + Response, ResponseBody, StatusCode +}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use futures_core::future::Future; +use mime::{Mime, APPLICATION_JSON}; +use std::{ + error::Error, + fmt::Display, + pin::Pin +}; + +pub trait IntoResponseError +{ + type Err : Error + Send + 'static; + + fn into_response_error(self) -> Result; +} + +impl IntoResponseError for E +{ + type Err = serde_json::Error; + + fn into_response_error(self) -> Result + { + let err : ResourceError = self.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } +} + +impl ResourceResult for Result +where + R : ResponseBody, + E : Display + IntoResponseError +{ + type Err = E::Err; + + fn into_response(self) -> Pin> + Send>> + { + match self { + Ok(r) => into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(&r)?))), + Err(e) => handle_error(e) + } + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + R::schema() + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + use crate::result::OrAllTypes; + use futures_executor::block_on; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg + { + msg : String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn result_ok() + { + let ok : Result = Ok(Msg::default()); + let res = block_on(ok.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn result_err() + { + let err : Result = 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!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) + } +} diff --git a/src/result/success.rs b/src/result/success.rs new file mode 100644 index 0000000..dffd740 --- /dev/null +++ b/src/result/success.rs @@ -0,0 +1,163 @@ +use super::{ResourceResult, into_response_helper}; +use crate::{Response, ResponseBody}; +#[cfg(feature = "openapi")] +use crate::OpenapiSchema; +use gotham::hyper::StatusCode; +use mime::{Mime, APPLICATION_JSON}; +use std::{ + fmt::Debug, + future::Future, + pin::Pin, + ops::{Deref, DerefMut} +}; + +/** +This can be returned from a resource when there is no cause of an error. It behaves similar to a +smart pointer like box, it that it implements `AsRef`, `Deref` and the likes. + +Usage example: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +# +# #[derive(Resource)] +# #[resource(read_all)] +# struct MyResource; +# +#[derive(Deserialize, Serialize)] +# #[cfg_attr(feature = "openapi", derive(OpenapiType))] +struct MyResponse { + message: &'static str +} + +#[read_all(MyResource)] +fn read_all(_state: &mut State) -> Success { + let res = MyResponse { message: "I'm always happy" }; + res.into() +} +# } +``` +*/ +#[derive(Debug)] +pub struct Success(T); + +impl AsMut for Success +{ + fn as_mut(&mut self) -> &mut T + { + &mut self.0 + } +} + +impl AsRef for Success +{ + fn as_ref(&self) -> &T + { + &self.0 + } +} + +impl Deref for Success +{ + type Target = T; + + fn deref(&self) -> &T + { + &self.0 + } +} + +impl DerefMut for Success +{ + fn deref_mut(&mut self) -> &mut T + { + &mut self.0 + } +} + +impl From for Success +{ + fn from(t : T) -> Self + { + Self(t) + } +} + +impl Clone for Success +{ + fn clone(&self) -> Self + { + Self(self.0.clone()) + } +} + +impl Copy for Success +{ +} + +impl Default for Success +{ + fn default() -> Self + { + Self(T::default()) + } +} + +impl ResourceResult for Success +where + Self : Send +{ + type Err = serde_json::Error; + + fn into_response(self) -> Pin> + Send>> + { + into_response_helper(|| Ok(Response::json(StatusCode::OK, serde_json::to_string(self.as_ref())?))) + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + T::schema() + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + use crate::result::OrAllTypes; + use futures_executor::block_on; + + #[derive(Debug, Default, Serialize)] + #[cfg_attr(feature = "openapi", derive(crate::OpenapiType))] + struct Msg + { + msg : String + } + + #[test] + fn success_always_successfull() + { + let success : Success = Msg::default().into(); + let res = block_on(success.into_response()).expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body().unwrap(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn success_accepts_json() + { + assert!(>::accepted_types().or_all_types().contains(&APPLICATION_JSON)) + } +} diff --git a/src/routing.rs b/src/routing.rs index c7cd5a6..5610379 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -1,144 +1,273 @@ +use crate::{ + resource::*, + result::{ResourceError, ResourceResult}, + RequestBody, + Response, + StatusCode +}; +#[cfg(feature = "cors")] +use crate::CorsRoute; #[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 futures_util::{future, future::FutureExt}; use gotham::{ - handler::HandlerError, + handler::{HandlerError, HandlerFuture, IntoHandlerError}, 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}, + builder::*, non_match::RouteNonMatch, route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} }, state::{FromState, State} }; +use gotham::hyper::{ + body::to_bytes, + header::CONTENT_TYPE, + Body, + HeaderMap, + Method +}; use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; -use std::{any::TypeId, panic::RefUnwindSafe}; +use std::{ + future::Future, + panic::RefUnwindSafe, + pin::Pin +}; /// 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 { - pub id: ID +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct PathExtractor +{ + 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 { - fn with_openapi(&mut self, info: OpenapiInfo, block: F) +pub trait WithOpenapi +{ + fn with_openapi(&mut self, info : OpenapiInfo, block : F) where - F: FnOnce(OpenapiRouter<'_, D>); + 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(&mut self, path: &str); +/// any RESTful `Resource` with a path. +pub trait DrawResources +{ + fn resource(&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(&mut self); +/// `Resource::setup` method. +pub trait DrawResourceRoutes +{ + fn read_all(&mut self); + + fn read(&mut self); + + fn search(&mut self); + + fn create(&mut self) + where + Handler::Res : 'static, + Handler::Body : 'static; + + fn change_all(&mut self) + where + Handler::Res : 'static, + Handler::Body : 'static; + + fn change(&mut self) + where + Handler::Res : 'static, + Handler::Body : 'static; + + fn remove_all(&mut self); + + fn remove(&mut self); } -fn response_from(res: Response, state: &State) -> gotham::hyper::Response { +fn response_from(res : Response, state : &State) -> gotham::hyper::Response +{ 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()); + if let Some(mime) = res.mime + { + r.headers_mut().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 { + if method != Method::HEAD + { *r.body_mut() = res.body; } - + #[cfg(feature = "cors")] crate::cors::handle_cors(state, &mut r); - + r } -async fn endpoint_handler(state: &mut State) -> Result, HandlerError> +async fn to_handler_future(state : State, get_result : F) -> Result<(State, gotham::hyper::Response), (State, HandlerError)> where - E: Endpoint, - ::Err: Into + F : FnOnce(State) -> Pin + Send>>, + R : ResourceResult { - 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::() == TypeId::of::() { - 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); - } - } + let (state, res) = get_result(state).await; + let res = res.into_response().await; + match res { + Ok(res) => { + let r = response_from(res, &state); + Ok((state, r)) }, - false => None - }; + Err(e) => Err((state, e.into_handler_error())) + } +} - 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)) +async fn body_to_res(mut state : State, get_result : F) -> (State, Result, HandlerError>) +where + B : RequestBody, + F : FnOnce(State, B) -> Pin + Send>>, + R : ResourceResult +{ + let body = to_bytes(Body::take_from(&mut state)).await; + + let body = match body { + Ok(body) => body, + Err(e) => return (state, Err(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 (state, Ok(res)) + } + }; + + let res = { + let body = match B::from_body(body, content_type) { + Ok(body) => body, + Err(e) => { + let error : ResourceError = e.into(); + let res = match serde_json::to_string(&error) { + Ok(json) => { + let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); + Ok(res) + }, + Err(e) => Err(e.into_handler_error()) + }; + return (state, res) + } + }; + get_result(state, body) + }; + + let (state, res) = res.await; + let res = res.into_response().await; + + let res = match res { + Ok(res) => { + let r = response_from(res, &state); + Ok(r) + }, + Err(e) => Err(e.into_handler_error()) + }; + (state, res) +} + +fn handle_with_body(state : State, get_result : F) -> Pin> +where + B : RequestBody + 'static, + F : FnOnce(State, B) -> Pin + Send>> + Send + 'static, + R : ResourceResult + Send + 'static +{ + body_to_res(state, get_result) + .then(|(state, res)| match res { + Ok(ok) => future::ok((state, ok)), + Err(err) => future::err((state, err)) + }) + .boxed() +} + +fn read_all_handler(state : State) -> Pin> +{ + to_handler_future(state, |state| Handler::read_all(state)).boxed() +} + +fn read_handler(state : State) -> Pin> +{ + let id = { + let path : &PathExtractor = PathExtractor::borrow_from(&state); + path.id.clone() + }; + to_handler_future(state, |state| Handler::read(state, id)).boxed() +} + +fn search_handler(mut state : State) -> Pin> +{ + let query = Handler::Query::take_from(&mut state); + to_handler_future(state, |state| Handler::search(state, query)).boxed() +} + +fn create_handler(state : State) -> Pin> +where + Handler::Res : 'static, + Handler::Body : 'static +{ + handle_with_body::(state, |state, body| Handler::create(state, body)) +} + +fn change_all_handler(state : State) -> Pin> +where + Handler::Res : 'static, + Handler::Body : 'static +{ + handle_with_body::(state, |state, body| Handler::change_all(state, body)) +} + +fn change_handler(state : State) -> Pin> +where + Handler::Res : 'static, + Handler::Body : 'static +{ + let id = { + let path : &PathExtractor = PathExtractor::borrow_from(&state); + path.id.clone() + }; + handle_with_body::(state, |state, body| Handler::change(state, id, body)) +} + +fn remove_all_handler(state : State) -> Pin> +{ + to_handler_future(state, |state| Handler::remove_all(state)).boxed() +} + +fn remove_handler(state : State) -> Pin> +{ + let id = { + let path : &PathExtractor = PathExtractor::borrow_from(&state); + path.id.clone() + }; + to_handler_future(state, |state| Handler::remove(state, id)).boxed() } #[derive(Clone)] -struct MaybeMatchAcceptHeader { - matcher: Option +struct MaybeMatchAcceptHeader +{ + matcher : Option } -impl RouteMatcher for MaybeMatchAcceptHeader { - fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { +impl RouteMatcher for MaybeMatchAcceptHeader +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { match &self.matcher { Some(matcher) => matcher.is_match(state), None => Ok(()) @@ -146,8 +275,10 @@ impl RouteMatcher for MaybeMatchAcceptHeader { } } -impl MaybeMatchAcceptHeader { - fn new(types: Option>) -> Self { +impl From>> for MaybeMatchAcceptHeader +{ + fn from(types : Option>) -> Self + { let types = match types { Some(types) if types.is_empty() => None, types => types @@ -158,19 +289,16 @@ impl MaybeMatchAcceptHeader { } } -impl From>> for MaybeMatchAcceptHeader { - fn from(types: Option>) -> Self { - Self::new(types) - } -} - #[derive(Clone)] -struct MaybeMatchContentTypeHeader { - matcher: Option +struct MaybeMatchContentTypeHeader +{ + matcher : Option } -impl RouteMatcher for MaybeMatchContentTypeHeader { - fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { +impl RouteMatcher for MaybeMatchContentTypeHeader +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { match &self.matcher { Some(matcher) => matcher.is_match(state), None => Ok(()) @@ -178,31 +306,28 @@ impl RouteMatcher for MaybeMatchContentTypeHeader { } } -impl MaybeMatchContentTypeHeader { - fn new(types: Option>) -> Self { +impl From>> for MaybeMatchContentTypeHeader +{ + fn from(types : Option>) -> Self + { Self { matcher: types.map(|types| ContentTypeHeaderRouteMatcher::new(types).allow_no_type()) } } } -impl From>> for MaybeMatchContentTypeHeader { - fn from(types: Option>) -> Self { - Self::new(types) - } -} - macro_rules! implDrawResourceRoutes { ($implType:ident) => { + #[cfg(feature = "openapi")] impl<'a, C, P> WithOpenapi for $implType<'a, C, P> where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - fn with_openapi(&mut self, info: OpenapiInfo, block: F) + fn with_openapi(&mut self, info : OpenapiInfo, block : F) where - F: FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) + F : FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) { let router = OpenapiRouter { router: self, @@ -212,44 +337,120 @@ macro_rules! implDrawResourceRoutes { block(router); } } - + impl<'a, C, P> DrawResources for $implType<'a, C, P> where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - fn resource(&mut self, path: &str) { + fn resource(&mut self, path : &str) + { R::setup((self, path)); } } - + + #[allow(clippy::redundant_closure)] // doesn't work because of type parameters impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str) where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static { - fn endpoint(&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::() - .with_query_string_extractor::() - .to_async_borrowing(endpoint_handler::); + fn read_all(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0.get(&self.1) + .extend_route_matcher(matcher) + .to(|state| read_all_handler::(state)); + } - #[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); - } - }); + fn read(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0.get(&format!("{}/:id", self.1)) + .extend_route_matcher(matcher) + .with_path_extractor::>() + .to(|state| read_handler::(state)); + } + + fn search(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0.get(&format!("{}/search", self.1)) + .extend_route_matcher(matcher) + .with_query_string_extractor::() + .to(|state| search_handler::(state)); + } + + fn create(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static + { + let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + self.0.post(&self.1) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) + .to(|state| create_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&self.1, Method::POST); + } + + fn change_all(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static + { + let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + self.0.put(&self.1) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) + .to(|state| change_all_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&self.1, Method::PUT); + } + + fn change(&mut self) + where + Handler::Res : Send + 'static, + Handler::Body : 'static + { + let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + let path = format!("{}/:id", self.1); + self.0.put(&path) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) + .with_path_extractor::>() + .to(|state| change_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&path, Method::PUT); + } + + fn remove_all(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0.delete(&self.1) + .extend_route_matcher(matcher) + .to(|state| remove_all_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&self.1, Method::DELETE); + } + + fn remove(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let path = format!("{}/:id", self.1); + self.0.delete(&path) + .extend_route_matcher(matcher) + .with_path_extractor::>() + .to(|state| remove_handler::(state)); + #[cfg(feature = "cors")] + self.0.cors(&path, Method::POST); } } - }; + } } implDrawResourceRoutes!(RouterBuilder); diff --git a/src/types.rs b/src/types.rs index 20be58d..576f7e9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,39 +1,54 @@ +#[cfg(feature = "openapi")] +use crate::OpenapiType; + use gotham::hyper::body::Bytes; use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; use serde::{de::DeserializeOwned, Serialize}; -use std::error::Error; +use std::{ + error::Error, + panic::RefUnwindSafe +}; #[cfg(not(feature = "openapi"))] -pub trait ResourceType {} +pub trait ResourceType +{ +} #[cfg(not(feature = "openapi"))] -impl ResourceType for T {} +impl ResourceType for T +{ +} #[cfg(feature = "openapi")] -pub trait ResourceType: OpenapiType {} +pub trait ResourceType : OpenapiType +{ +} #[cfg(feature = "openapi")] -impl ResourceType for T {} +impl 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 {} +/// `OpenapiType`. +pub trait ResponseBody : ResourceType + Serialize +{ +} + +impl ResponseBody for T +{ +} -impl 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: +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; @@ -45,32 +60,39 @@ struct RawImage { 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; + [`Bytes`]: ../bytes/struct.Bytes.html + [`Mime`]: ../mime/struct.Mime.html +*/ +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 `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; + fn from_body(body : Bytes, content_type : Mime) -> Result; } -impl FromBody for T { +impl FromBody for T +{ type Err = serde_json::Error; - - fn from_body(body: Bytes, _content_type: Mime) -> Result { + + fn from_body(body : Bytes, _content_type : Mime) -> Result + { 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]. +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: +[`FromBody`] and optionally a list of supported media types: ```rust # #[macro_use] extern crate gotham_restful; @@ -83,17 +105,33 @@ struct RawImage { } ``` - [OpenapiType]: trait.OpenapiType.html + [`FromBody`]: trait.FromBody.html + [`OpenapiType`]: trait.OpenapiType.html */ -pub trait RequestBody: ResourceType + FromBody { +pub trait RequestBody : ResourceType + FromBody +{ /// Return all types that are supported as content types. Use `None` if all types are supported. - fn supported_types() -> Option> { + fn supported_types() -> Option> + { None } } -impl RequestBody for T { - fn supported_types() -> Option> { +impl RequestBody for T +{ + fn supported_types() -> Option> + { Some(vec![APPLICATION_JSON]) } } + +/// A type than can be used as a parameter to a resource method. Implemented for every type +/// that is deserialize and thread-safe. If the `openapi` feature is used, it must also be of +/// type `OpenapiType`. +pub trait ResourceID : ResourceType + DeserializeOwned + Clone + RefUnwindSafe + Send + Sync +{ +} + +impl ResourceID for T +{ +} diff --git a/tests/async_methods.rs b/tests/async_methods.rs index 9a1669b..42cbc25 100644 --- a/tests/async_methods.rs +++ b/tests/async_methods.rs @@ -1,133 +1,106 @@ -#[macro_use] -extern crate gotham_derive; +#[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}; +mod util { include!("util/mod.rs"); } +use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; + #[derive(Resource)] -#[resource(read_all, read, search, create, change_all, change, remove_all, remove, state_test)] +#[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 +struct FooBody +{ + data : String } -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] +#[derive(Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooSearch { - query: String +struct FooSearch +{ + query : String } -const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; -#[read_all] -async fn read_all() -> Raw<&'static [u8]> { +const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; +#[read_all(FooResource)] +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]> { +const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; +#[read(FooResource)] +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]> { +const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; +#[search(FooResource)] +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]> { +const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; +#[create(FooResource)] +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]> { +const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; +#[change_all(FooResource)] +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]> { +const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; +#[change(FooResource)] +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]> { +const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; +#[remove_all(FooResource)] +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]> { +const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; +#[remove(FooResource)] +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::(); - sleep(Duration::from_nanos(1)).await; - Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN) -} - #[test] -fn async_methods() { - let _ = pretty_env_logger::try_init_timed(); - +fn async_methods() +{ let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); - })) - .unwrap(); - + })).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_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); } diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs index b74e7b3..80ad346 100644 --- a/tests/cors_handling.rs +++ b/tests/cors_handling.rs @@ -5,285 +5,124 @@ use gotham::{ router::builder::*, test::{Server, TestRequest, TestServer} }; -use gotham_restful::{ - change_all, - cors::{Headers, Origin}, - read_all, CorsConfig, DrawResources, Raw, Resource -}; +use gotham_restful::{CorsConfig, DrawResources, Origin, Raw, Resource, change_all, read_all}; +use itertools::Itertools; 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>) {} - -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::("/foo"))).unwrap() +#[read_all(FooResource)] +fn read_all() +{ } -fn test_response(req: TestRequest, origin: Option<&str>, vary: Option<&str>, credentials: bool) -where - TS: Server + 'static, - C: Connect + Clone + Send + Sync + 'static +#[change_all(FooResource)] +fn change_all(_body : Raw>) { - let res = req - .with_header(ORIGIN, "http://example.org".parse().unwrap()) - .perform() - .unwrap(); +} + +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::("/foo") + })).unwrap() +} + +fn test_response(req : TestRequest, 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::>().join(",")); - assert_eq!( - headers - .get(ACCESS_CONTROL_ALLOW_ORIGIN) - .and_then(|value| value.to_str().ok()) - .as_deref(), - origin - ); + println!("{}", headers.keys().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_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") +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(); + .perform().unwrap(); assert_eq!(res.status(), StatusCode::NO_CONTENT); let headers = res.headers(); - println!("{}", headers.keys().map(|name| name.as_str()).collect::>().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 - ); + println!("{}", headers.keys().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) - ); + 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::>().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() { +fn cors_origin_none() +{ let cfg = Default::default(); let server = test_server(cfg); - test_preflight(&server, "PUT", None, "access-control-request-method", false, 0); - + test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 0); + test_response(server.client().get("http://example.org/foo"), None, None, false); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - None, - None, - false - ); + test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false); } #[test] -fn cors_origin_star() { +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_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_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), Some("*"), None, false); } #[test] -fn cors_origin_single() { +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_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() { +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_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() { +fn cors_credentials() +{ let cfg = CorsConfig { origin: Origin::None, credentials: true, @@ -291,19 +130,15 @@ fn cors_credentials() { }; let server = test_server(cfg); - test_preflight(&server, "PUT", None, "access-control-request-method", true, 0); - + test_preflight(&server, "PUT", None, "Access-Control-Request-Method", true, 0); + test_response(server.client().get("http://example.org/foo"), None, None, true); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - None, - None, - true - ); + test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, true); } #[test] -fn cors_max_age() { +fn cors_max_age() +{ let cfg = CorsConfig { origin: Origin::None, max_age: 31536000, @@ -311,13 +146,8 @@ fn cors_max_age() { }; let server = test_server(cfg); - test_preflight(&server, "PUT", None, "access-control-request-method", false, 31536000); - + test_preflight(&server, "PUT", None, "Access-Control-Request-Method", false, 31536000); + test_response(server.client().get("http://example.org/foo"), None, None, false); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - None, - None, - false - ); + test_response(server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), None, None, false); } diff --git a/tests/custom_request_body.rs b/tests/custom_request_body.rs index 66156b6..95fa748 100644 --- a/tests/custom_request_body.rs +++ b/tests/custom_request_body.rs @@ -1,8 +1,13 @@ -use gotham::{hyper::header::CONTENT_TYPE, router::builder::*, test::TestServer}; +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."; + +const RESPONSE : &[u8] = b"This is the only valid response."; #[derive(Resource)] #[resource(create)] @@ -15,25 +20,24 @@ struct Foo { content_type: Mime } -#[create] -fn create(body: Foo) -> Raw> { +#[create(FooResource)] +fn create(body : Foo) -> Raw> { Raw::new(body.content, body.content_type) } + #[test] -fn custom_request_body() { +fn custom_request_body() +{ let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); - })) - .unwrap(); - - let res = server - .client() + })).unwrap(); + + let res = server.client() .post("http://localhost/foo", RESPONSE, TEXT_PLAIN) - .perform() - .unwrap(); + .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(); + let body : &[u8] = res.as_ref(); assert_eq!(body, RESPONSE); } diff --git a/tests/openapi_specification.json b/tests/openapi_specification.json index c3297cd..c9e6c53 100644 --- a/tests/openapi_specification.json +++ b/tests/openapi_specification.json @@ -44,56 +44,6 @@ }, "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", diff --git a/tests/openapi_specification.rs b/tests/openapi_specification.rs index a2d33d4..2171526 100644 --- a/tests/openapi_specification.rs +++ b/tests/openapi_specification.rs @@ -1,11 +1,9 @@ #![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))] -#[macro_use] -extern crate gotham_derive; +#[macro_use] extern crate gotham_derive; use chrono::{NaiveDate, NaiveDateTime}; use gotham::{ - hyper::Method, pipeline::{new_pipeline, single::single_pipeline}, router::builder::*, test::TestServer @@ -15,91 +13,86 @@ use mime::IMAGE_PNG; use serde::{Deserialize, Serialize}; #[allow(dead_code)] -mod util { - include!("util/mod.rs"); -} +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)] +#[resource(read, change)] struct ImageResource; #[derive(FromBody, RequestBody)] #[supported_types(IMAGE_PNG)] struct Image(Vec); -#[read(operation_id = "getImage")] -fn get_image(_id: u64) -> Raw<&'static [u8]> { +#[read(ImageResource, 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) {} +#[change(ImageResource, operation_id = "setImage")] +fn set_image(_id : u64, _image : Image) +{ +} + #[derive(Resource)] -#[resource(read_secret, search_secret)] +#[resource(read, search)] struct SecretResource; #[derive(Deserialize, Clone)] -struct AuthData { - sub: String, - iat: u64, - exp: u64 +struct AuthData +{ + sub : String, + iat : u64, + exp : u64 } type AuthStatus = gotham_restful::AuthStatus; #[derive(OpenapiType, Serialize)] -struct Secret { - code: f32 +struct Secret +{ + code : f32 } #[derive(OpenapiType, Serialize)] -struct Secrets { - secrets: Vec +struct Secrets +{ + secrets : Vec } #[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)] -struct SecretQuery { - date: NaiveDate, - hour: Option, - minute: Option +struct SecretQuery +{ + date : NaiveDate, + hour : Option, + minute : Option } -#[read] -fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess { +#[read(SecretResource)] +fn read_secret(auth : AuthStatus, _id : NaiveDateTime) -> AuthSuccess +{ auth.ok()?; Ok(Secret { code: 4.2 }) } -#[search] -fn search_secret(auth: AuthStatus, _query: SecretQuery) -> AuthSuccess { +#[search(SecretResource)] +fn search_secret(auth : AuthStatus, _query : SecretQuery) -> AuthSuccess +{ 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() { +fn openapi_supports_scope() +{ let info = OpenapiInfo { title: "This is just a test".to_owned(), version: "1.2.3".to_owned(), @@ -114,12 +107,10 @@ fn openapi_specification() { let server = TestServer::new(build_router(chain, pipelines, |router| { router.with_openapi(info, |mut router| { router.resource::("img"); - router.resource::("secret"); - router.resource::("custom"); router.get_openapi("openapi"); + router.resource::("secret"); }); - })) - .unwrap(); - + })).unwrap(); + test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json"); } diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs index 33f240a..d126bb8 100644 --- a/tests/openapi_supports_scope.rs +++ b/tests/openapi_supports_scope.rs @@ -1,27 +1,32 @@ #![cfg(feature = "openapi")] -use gotham::{router::builder::*, test::TestServer}; +use gotham::{ + router::builder::*, + test::TestServer +}; use gotham_restful::*; use mime::TEXT_PLAIN; #[allow(dead_code)] -mod util { - include!("util/mod.rs"); -} +mod util { include!("util/mod.rs"); } use util::{test_get_response, test_openapi_response}; -const RESPONSE: &[u8] = b"This is the only valid response."; + +const RESPONSE : &[u8] = b"This is the only valid response."; #[derive(Resource)] #[resource(read_all)] struct FooResource; -#[read_all] -fn read_all() -> Raw<&'static [u8]> { +#[read_all(FooResource)] +fn read_all() -> Raw<&'static [u8]> +{ Raw::new(RESPONSE, TEXT_PLAIN) } + #[test] -fn openapi_supports_scope() { +fn openapi_supports_scope() +{ let info = OpenapiInfo { title: "Test".to_owned(), version: "1.2.3".to_owned(), @@ -39,9 +44,8 @@ fn openapi_supports_scope() { }); router.resource::("foo4"); }); - })) - .unwrap(); - + })).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); diff --git a/tests/resource_error.rs b/tests/resource_error.rs deleted file mode 100644 index 9325dc5..0000000 --- a/tests/resource_error.rs +++ /dev/null @@ -1,37 +0,0 @@ -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? - } -} diff --git a/tests/sync_methods.rs b/tests/sync_methods.rs index 2b440fa..a13ec19 100644 --- a/tests/sync_methods.rs +++ b/tests/sync_methods.rs @@ -1,17 +1,16 @@ -#[macro_use] -extern crate gotham_derive; +#[macro_use] extern crate gotham_derive; -use gotham::{router::builder::*, test::TestServer}; +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}; +mod util { include!("util/mod.rs"); } +use util::{test_get_response, test_post_response, test_put_response, test_delete_response}; + #[derive(Resource)] #[resource(read_all, read, search, create, change_all, change, remove_all, remove)] @@ -20,98 +19,88 @@ struct FooResource; #[derive(Deserialize)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooBody { - data: String +struct FooBody +{ + data : String } -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] +#[derive(Deserialize, StateData, StaticResponseExtender)] #[cfg_attr(feature = "openapi", derive(OpenapiType))] #[allow(dead_code)] -struct FooSearch { - query: String +struct FooSearch +{ + query : String } -const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; -#[read_all] -fn read_all() -> Raw<&'static [u8]> { +const READ_ALL_RESPONSE : &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; +#[read_all(FooResource)] +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]> { +const READ_RESPONSE : &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; +#[read(FooResource)] +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]> { +const SEARCH_RESPONSE : &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; +#[search(FooResource)] +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]> { +const CREATE_RESPONSE : &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; +#[create(FooResource)] +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]> { +const CHANGE_ALL_RESPONSE : &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; +#[change_all(FooResource)] +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]> { +const CHANGE_RESPONSE : &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; +#[change(FooResource)] +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]> { +const REMOVE_ALL_RESPONSE : &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; +#[remove_all(FooResource)] +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]> { +const REMOVE_RESPONSE : &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; +#[remove(FooResource)] +fn remove(_id : u64) -> Raw<&'static [u8]> +{ Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) } #[test] -fn sync_methods() { - let _ = pretty_env_logger::try_init_timed(); - +fn sync_methods() +{ let server = TestServer::new(build_simple_router(|router| { router.resource::("foo"); - })) - .unwrap(); - + })).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_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); } diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs index 406ae6a..b5a572a 100644 --- a/tests/trybuild_ui.rs +++ b/tests/trybuild_ui.rs @@ -2,9 +2,29 @@ use trybuild::TestCases; #[test] #[ignore] -fn trybuild_ui() { +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"); + + // always enabled + t.compile_fail("tests/ui/from_body_enum.rs"); + t.compile_fail("tests/ui/method_async_state.rs"); + t.compile_fail("tests/ui/method_for_unknown_resource.rs"); + t.compile_fail("tests/ui/method_no_resource.rs"); + t.compile_fail("tests/ui/method_self.rs"); + t.compile_fail("tests/ui/method_too_few_args.rs"); + t.compile_fail("tests/ui/method_too_many_args.rs"); + t.compile_fail("tests/ui/method_unsafe.rs"); + t.compile_fail("tests/ui/resource_unknown_method.rs"); + + // require the openapi feature + if cfg!(feature = "openapi") + { + t.compile_fail("tests/ui/openapi_type_enum_with_fields.rs"); + t.compile_fail("tests/ui/openapi_type_nullable_non_bool.rs"); + t.compile_fail("tests/ui/openapi_type_rename_non_string.rs"); + t.compile_fail("tests/ui/openapi_type_tuple_struct.rs"); + t.compile_fail("tests/ui/openapi_type_union.rs"); + t.compile_fail("tests/ui/openapi_type_unknown_key.rs"); + } } diff --git a/tests/ui/endpoint/async_state.rs b/tests/ui/endpoint/async_state.rs deleted file mode 100644 index d951370..0000000 --- a/tests/ui/endpoint/async_state.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[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() {} diff --git a/tests/ui/endpoint/async_state.stderr b/tests/ui/endpoint/async_state.stderr deleted file mode 100644 index 2ca92b4..0000000 --- a/tests/ui/endpoint/async_state.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: Endpoint handler functions that are async must not take `&State` as an argument, consider taking `&mut State` - --> $DIR/async_state.rs:10:19 - | -10 | async fn read_all(state: &State) {} - | ^^^^^ diff --git a/tests/ui/endpoint/custom_method_invalid_expr.rs b/tests/ui/endpoint/custom_method_invalid_expr.rs deleted file mode 100644 index 9d6450d..0000000 --- a/tests/ui/endpoint/custom_method_invalid_expr.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[endpoint(method = "I like pizza", uri = "custom_read")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/custom_method_invalid_expr.stderr b/tests/ui/endpoint/custom_method_invalid_expr.stderr deleted file mode 100644 index 8375cd1..0000000 --- a/tests/ui/endpoint/custom_method_invalid_expr.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: unexpected token - --> $DIR/custom_method_invalid_expr.rs:8:21 - | -8 | #[endpoint(method = "I like pizza", uri = "custom_read")] - | ^^^^^^^^^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/custom_method_invalid_expr.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/custom_method_invalid_type.rs b/tests/ui/endpoint/custom_method_invalid_type.rs deleted file mode 100644 index b5bc674..0000000 --- a/tests/ui/endpoint/custom_method_invalid_type.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[endpoint(method = "String::new()", uri = "custom_read")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/custom_method_invalid_type.stderr b/tests/ui/endpoint/custom_method_invalid_type.stderr deleted file mode 100644 index 35ad1c3..0000000 --- a/tests/ui/endpoint/custom_method_invalid_type.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0308]: mismatched types - --> $DIR/custom_method_invalid_type.rs:8:21 - | -8 | #[endpoint(method = "String::new()", uri = "custom_read")] - | --------------------^^^^^^^^^^^^^^^----------------------- - | | | - | | expected struct `Method`, found struct `std::string::String` - | expected `Method` because of return type diff --git a/tests/ui/endpoint/custom_method_missing.rs b/tests/ui/endpoint/custom_method_missing.rs deleted file mode 100644 index d0e6855..0000000 --- a/tests/ui/endpoint/custom_method_missing.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[endpoint(uri = "custom_read")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/custom_method_missing.stderr b/tests/ui/endpoint/custom_method_missing.stderr deleted file mode 100644 index df6da97..0000000 --- a/tests/ui/endpoint/custom_method_missing.stderr +++ /dev/null @@ -1,13 +0,0 @@ -error: Missing `method` attribute (e.g. `#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")]`) - --> $DIR/custom_method_missing.rs:8:1 - | -8 | #[endpoint(uri = "custom_read")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/custom_method_missing.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/custom_uri_missing.rs b/tests/ui/endpoint/custom_uri_missing.rs deleted file mode 100644 index 5ec5182..0000000 --- a/tests/ui/endpoint/custom_uri_missing.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/custom_uri_missing.stderr b/tests/ui/endpoint/custom_uri_missing.stderr deleted file mode 100644 index b1584b8..0000000 --- a/tests/ui/endpoint/custom_uri_missing.stderr +++ /dev/null @@ -1,13 +0,0 @@ -error: Missing `uri` attribute (e.g. `#[endpoint(uri = "custom_endpoint")]`) - --> $DIR/custom_uri_missing.rs:8:1 - | -8 | #[endpoint(method = "gotham_restful::gotham::hyper::Method::GET")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/custom_uri_missing.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/invalid_attribute.stderr b/tests/ui/endpoint/invalid_attribute.stderr deleted file mode 100644 index 9c5c86f..0000000 --- a/tests/ui/endpoint/invalid_attribute.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Invalid attribute syntax - --> $DIR/invalid_attribute.rs:8:12 - | -8 | #[read_all(FooResource)] - | ^^^^^^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/invalid_attribute.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/invalid_body_ty.rs b/tests/ui/endpoint/invalid_body_ty.rs deleted file mode 100644 index a3ab10d..0000000 --- a/tests/ui/endpoint/invalid_body_ty.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[macro_use] -extern crate gotham_restful; -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(endpoint)] -struct FooResource; - -#[derive(Debug)] -struct FooBody { - foo: String -} - -#[endpoint(method = "Method::GET", uri = "", body = true)] -fn endpoint(_: FooBody) { - unimplemented!() -} - -fn main() {} diff --git a/tests/ui/endpoint/invalid_body_ty.stderr b/tests/ui/endpoint/invalid_body_ty.stderr deleted file mode 100644 index 5a259e2..0000000 --- a/tests/ui/endpoint/invalid_body_ty.stderr +++ /dev/null @@ -1,24 +0,0 @@ -error[E0277]: the trait bound `FooBody: OpenapiType` is not satisfied - --> $DIR/invalid_body_ty.rs:15:16 - | -15 | fn endpoint(_: FooBody) { - | ^^^^^^^ the trait `OpenapiType` is not implemented for `FooBody` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Body: RequestBody + Send; - | ----------- required by this bound in `gotham_restful::EndpointWithSchema::Body` - -error[E0277]: the trait bound `for<'de> FooBody: serde::de::Deserialize<'de>` is not satisfied - --> $DIR/invalid_body_ty.rs:15:16 - | -15 | fn endpoint(_: FooBody) { - | ^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooBody` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Body: RequestBody + Send; - | ----------- required by this bound in `gotham_restful::EndpointWithSchema::Body` - | - = note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `FooBody` - = note: required because of the requirements on the impl of `gotham_restful::RequestBody` for `FooBody` diff --git a/tests/ui/endpoint/invalid_params_ty.rs b/tests/ui/endpoint/invalid_params_ty.rs deleted file mode 100644 index e08ff91..0000000 --- a/tests/ui/endpoint/invalid_params_ty.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[macro_use] -extern crate gotham_restful; -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(endpoint)] -struct FooResource; - -#[derive(Debug)] -struct FooParams { - foo: String -} - -#[endpoint(method = "Method::GET", uri = "", params = true)] -fn endpoint(_: FooParams) { - unimplemented!() -} - -fn main() {} diff --git a/tests/ui/endpoint/invalid_params_ty.stderr b/tests/ui/endpoint/invalid_params_ty.stderr deleted file mode 100644 index de1597a..0000000 --- a/tests/ui/endpoint/invalid_params_ty.stderr +++ /dev/null @@ -1,54 +0,0 @@ -error[E0277]: the trait bound `FooParams: OpenapiType` is not satisfied - --> $DIR/invalid_params_ty.rs:15:16 - | -15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooParams` - | - ::: $WORKSPACE/src/endpoint.rs - | - | #[openapi_bound("Params: OpenapiType")] - | --------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` - -error[E0277]: the trait bound `for<'de> FooParams: serde::de::Deserialize<'de>` is not satisfied - --> $DIR/invalid_params_ty.rs:15:16 - | -15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooParams` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Params: QueryStringExtractor + Clone + Sync; - | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` - -error[E0277]: the trait bound `FooParams: StateData` is not satisfied - --> $DIR/invalid_params_ty.rs:15:16 - | -15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `StateData` is not implemented for `FooParams` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Params: QueryStringExtractor + Clone + Sync; - | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` - -error[E0277]: the trait bound `FooParams: StaticResponseExtender` is not satisfied - --> $DIR/invalid_params_ty.rs:15:16 - | -15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `StaticResponseExtender` is not implemented for `FooParams` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Params: QueryStringExtractor + Clone + Sync; - | -------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Params` - -error[E0277]: the trait bound `FooParams: Clone` is not satisfied - --> $DIR/invalid_params_ty.rs:15:16 - | -15 | fn endpoint(_: FooParams) { - | ^^^^^^^^^ the trait `Clone` is not implemented for `FooParams` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Params: QueryStringExtractor + Clone + Sync; - | ----- required by this bound in `gotham_restful::EndpointWithSchema::Params` diff --git a/tests/ui/endpoint/invalid_placeholders_ty.rs b/tests/ui/endpoint/invalid_placeholders_ty.rs deleted file mode 100644 index 3ca1d10..0000000 --- a/tests/ui/endpoint/invalid_placeholders_ty.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[macro_use] -extern crate gotham_restful; -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(endpoint)] -struct FooResource; - -#[derive(Debug)] -struct FooPlaceholders { - foo: String -} - -#[endpoint(method = "Method::GET", uri = ":foo")] -fn endpoint(_: FooPlaceholders) { - unimplemented!() -} - -fn main() {} diff --git a/tests/ui/endpoint/invalid_placeholders_ty.stderr b/tests/ui/endpoint/invalid_placeholders_ty.stderr deleted file mode 100644 index 58c8014..0000000 --- a/tests/ui/endpoint/invalid_placeholders_ty.stderr +++ /dev/null @@ -1,54 +0,0 @@ -error[E0277]: the trait bound `FooPlaceholders: OpenapiType` is not satisfied - --> $DIR/invalid_placeholders_ty.rs:15:16 - | -15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `OpenapiType` is not implemented for `FooPlaceholders` - | - ::: $WORKSPACE/src/endpoint.rs - | - | #[openapi_bound("Placeholders: OpenapiType")] - | --------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` - -error[E0277]: the trait bound `for<'de> FooPlaceholders: serde::de::Deserialize<'de>` is not satisfied - --> $DIR/invalid_placeholders_ty.rs:15:16 - | -15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `FooPlaceholders` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Placeholders: PathExtractor + Clone + Sync; - | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` - -error[E0277]: the trait bound `FooPlaceholders: StateData` is not satisfied - --> $DIR/invalid_placeholders_ty.rs:15:16 - | -15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `StateData` is not implemented for `FooPlaceholders` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Placeholders: PathExtractor + Clone + Sync; - | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` - -error[E0277]: the trait bound `FooPlaceholders: StaticResponseExtender` is not satisfied - --> $DIR/invalid_placeholders_ty.rs:15:16 - | -15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `StaticResponseExtender` is not implemented for `FooPlaceholders` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Placeholders: PathExtractor + Clone + Sync; - | ------------------- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` - -error[E0277]: the trait bound `FooPlaceholders: Clone` is not satisfied - --> $DIR/invalid_placeholders_ty.rs:15:16 - | -15 | fn endpoint(_: FooPlaceholders) { - | ^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `FooPlaceholders` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Placeholders: PathExtractor + Clone + Sync; - | ----- required by this bound in `gotham_restful::EndpointWithSchema::Placeholders` diff --git a/tests/ui/endpoint/invalid_return_type.rs b/tests/ui/endpoint/invalid_return_type.rs deleted file mode 100644 index feeaf98..0000000 --- a/tests/ui/endpoint/invalid_return_type.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[macro_use] -extern crate gotham_restful; -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(endpoint)] -struct FooResource; - -struct FooResponse; - -#[endpoint(method = "Method::GET", uri = "")] -fn endpoint() -> FooResponse { - unimplemented!() -} - -fn main() {} diff --git a/tests/ui/endpoint/invalid_return_type.stderr b/tests/ui/endpoint/invalid_return_type.stderr deleted file mode 100644 index 69d5f39..0000000 --- a/tests/ui/endpoint/invalid_return_type.stderr +++ /dev/null @@ -1,21 +0,0 @@ -error[E0277]: the trait bound `FooResponse: ResponseSchema` is not satisfied - --> $DIR/invalid_return_type.rs:12:18 - | -12 | fn endpoint() -> FooResponse { - | ^^^^^^^^^^^ the trait `ResponseSchema` is not implemented for `FooResponse` - | - ::: $WORKSPACE/src/endpoint.rs - | - | #[openapi_bound("Output: crate::ResponseSchema")] - | ------------------------------- required by this bound in `gotham_restful::EndpointWithSchema::Output` - -error[E0277]: the trait bound `FooResponse: gotham_restful::IntoResponse` is not satisfied - --> $DIR/invalid_return_type.rs:12:18 - | -12 | fn endpoint() -> FooResponse { - | ^^^^^^^^^^^ the trait `gotham_restful::IntoResponse` is not implemented for `FooResponse` - | - ::: $WORKSPACE/src/endpoint.rs - | - | type Output: IntoResponse + Send; - | ------------ required by this bound in `gotham_restful::EndpointWithSchema::Output` diff --git a/tests/ui/endpoint/non_custom_body_attribute.rs b/tests/ui/endpoint/non_custom_body_attribute.rs deleted file mode 100644 index e6257d4..0000000 --- a/tests/ui/endpoint/non_custom_body_attribute.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(body = false)] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/non_custom_body_attribute.stderr b/tests/ui/endpoint/non_custom_body_attribute.stderr deleted file mode 100644 index a30e540..0000000 --- a/tests/ui/endpoint/non_custom_body_attribute.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: `body` can only be used on custom endpoints - --> $DIR/non_custom_body_attribute.rs:8:12 - | -8 | #[read_all(body = false)] - | ^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/non_custom_body_attribute.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_method_attribute.rs b/tests/ui/endpoint/non_custom_method_attribute.rs deleted file mode 100644 index c23423c..0000000 --- a/tests/ui/endpoint/non_custom_method_attribute.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(method = "gotham_restful::gotham::hyper::Method::GET")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/non_custom_method_attribute.stderr b/tests/ui/endpoint/non_custom_method_attribute.stderr deleted file mode 100644 index 42a541d..0000000 --- a/tests/ui/endpoint/non_custom_method_attribute.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: `method` can only be used on custom endpoints - --> $DIR/non_custom_method_attribute.rs:8:12 - | -8 | #[read_all(method = "gotham_restful::gotham::hyper::Method::GET")] - | ^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/non_custom_method_attribute.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_params_attribute.rs b/tests/ui/endpoint/non_custom_params_attribute.rs deleted file mode 100644 index 377331b..0000000 --- a/tests/ui/endpoint/non_custom_params_attribute.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(params = true)] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/non_custom_params_attribute.stderr b/tests/ui/endpoint/non_custom_params_attribute.stderr deleted file mode 100644 index 9ca336e..0000000 --- a/tests/ui/endpoint/non_custom_params_attribute.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: `params` can only be used on custom endpoints - --> $DIR/non_custom_params_attribute.rs:8:12 - | -8 | #[read_all(params = true)] - | ^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/non_custom_params_attribute.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/non_custom_uri_attribute.rs b/tests/ui/endpoint/non_custom_uri_attribute.rs deleted file mode 100644 index 1945fce..0000000 --- a/tests/ui/endpoint/non_custom_uri_attribute.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(uri = "custom_read")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/non_custom_uri_attribute.stderr b/tests/ui/endpoint/non_custom_uri_attribute.stderr deleted file mode 100644 index 61018a7..0000000 --- a/tests/ui/endpoint/non_custom_uri_attribute.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: `uri` can only be used on custom endpoints - --> $DIR/non_custom_uri_attribute.rs:8:12 - | -8 | #[read_all(uri = "custom_read")] - | ^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/non_custom_uri_attribute.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/self.stderr b/tests/ui/endpoint/self.stderr deleted file mode 100644 index 51fd32b..0000000 --- a/tests/ui/endpoint/self.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: Didn't expect self parameter - --> $DIR/self.rs:9:13 - | -9 | fn read_all(self) {} - | ^^^^ - -error: `self` parameter is only allowed in associated functions - --> $DIR/self.rs:9:13 - | -9 | fn read_all(self) {} - | ^^^^ not semantically valid as function parameter - | - = note: associated functions are those in `impl` or `trait` definitions - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/self.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/too_few_args.rs b/tests/ui/endpoint/too_few_args.rs deleted file mode 100644 index 963689b..0000000 --- a/tests/ui/endpoint/too_few_args.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read)] -struct FooResource; - -#[read] -fn read() {} - -fn main() {} diff --git a/tests/ui/endpoint/too_few_args.stderr b/tests/ui/endpoint/too_few_args.stderr deleted file mode 100644 index 5a19643..0000000 --- a/tests/ui/endpoint/too_few_args.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Too few arguments - --> $DIR/too_few_args.rs:9:4 - | -9 | fn read() {} - | ^^^^ - -error[E0412]: cannot find type `read___gotham_restful_endpoint` in this scope - --> $DIR/too_few_args.rs:5:12 - | -5 | #[resource(read)] - | ^^^^ not found in this scope diff --git a/tests/ui/endpoint/too_many_args.rs b/tests/ui/endpoint/too_many_args.rs deleted file mode 100644 index f334c3e..0000000 --- a/tests/ui/endpoint/too_many_args.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -fn read_all(_id: u64) {} - -fn main() {} diff --git a/tests/ui/endpoint/too_many_args.stderr b/tests/ui/endpoint/too_many_args.stderr deleted file mode 100644 index e781a97..0000000 --- a/tests/ui/endpoint/too_many_args.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Too many arguments - --> $DIR/too_many_args.rs:9:4 - | -9 | fn read_all(_id: u64) {} - | ^^^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/too_many_args.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/unknown_attribute.rs b/tests/ui/endpoint/unknown_attribute.rs deleted file mode 100644 index d0410ba..0000000 --- a/tests/ui/endpoint/unknown_attribute.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(pineapple = "on pizza")] -fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/unknown_attribute.stderr b/tests/ui/endpoint/unknown_attribute.stderr deleted file mode 100644 index ac0bafe..0000000 --- a/tests/ui/endpoint/unknown_attribute.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Unknown attribute - --> $DIR/unknown_attribute.rs:8:12 - | -8 | #[read_all(pineapple = "on pizza")] - | ^^^^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/unknown_attribute.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/unsafe.rs b/tests/ui/endpoint/unsafe.rs deleted file mode 100644 index 943b54a..0000000 --- a/tests/ui/endpoint/unsafe.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -unsafe fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/unsafe.stderr b/tests/ui/endpoint/unsafe.stderr deleted file mode 100644 index d7366e0..0000000 --- a/tests/ui/endpoint/unsafe.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Endpoint handler methods must not be unsafe - --> $DIR/unsafe.rs:9:1 - | -9 | unsafe fn read_all() {} - | ^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/unsafe.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/wants_auth_non_bool.rs b/tests/ui/endpoint/wants_auth_non_bool.rs deleted file mode 100644 index b341aaf..0000000 --- a/tests/ui/endpoint/wants_auth_non_bool.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(wants_auth = "yes, please")] -async fn read_all() {} - -fn main() {} diff --git a/tests/ui/endpoint/wants_auth_non_bool.stderr b/tests/ui/endpoint/wants_auth_non_bool.stderr deleted file mode 100644 index e752c40..0000000 --- a/tests/ui/endpoint/wants_auth_non_bool.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error: Expected boolean literal - --> $DIR/wants_auth_non_bool.rs:8:25 - | -8 | #[read_all(wants_auth = "yes, please")] - | ^^^^^^^^^^^^^ - -error[E0412]: cannot find type `read_all___gotham_restful_endpoint` in this scope - --> $DIR/wants_auth_non_bool.rs:5:12 - | -5 | #[resource(read_all)] - | ^^^^^^^^ not found in this scope diff --git a/tests/ui/from_body/enum.rs b/tests/ui/from_body/enum.rs deleted file mode 100644 index 6e10178..0000000 --- a/tests/ui/from_body/enum.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(FromBody)] -enum FromBodyEnum { - SomeVariant(Vec), - OtherVariant(String) -} - -fn main() {} diff --git a/tests/ui/from_body_enum.rs b/tests/ui/from_body_enum.rs new file mode 100644 index 0000000..24eb9db --- /dev/null +++ b/tests/ui/from_body_enum.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(FromBody)] +enum FromBodyEnum +{ + SomeVariant(Vec), + OtherVariant(String) +} + +fn main() +{ +} diff --git a/tests/ui/from_body/enum.stderr b/tests/ui/from_body_enum.stderr similarity index 53% rename from tests/ui/from_body/enum.stderr rename to tests/ui/from_body_enum.stderr index f10c2c8..26cab8b 100644 --- a/tests/ui/from_body/enum.stderr +++ b/tests/ui/from_body_enum.stderr @@ -1,5 +1,5 @@ error: #[derive(FromBody)] only works for structs - --> $DIR/enum.rs:5:1 + --> $DIR/from_body_enum.rs:4:1 | -5 | enum FromBodyEnum { +4 | enum FromBodyEnum | ^^^^ diff --git a/tests/ui/method_async_state.rs b/tests/ui/method_async_state.rs new file mode 100644 index 0000000..66b9fc7 --- /dev/null +++ b/tests/ui/method_async_state.rs @@ -0,0 +1,15 @@ +#[macro_use] extern crate gotham_restful; +use gotham_restful::State; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +async fn read_all(state : &State) +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_async_state.stderr b/tests/ui/method_async_state.stderr new file mode 100644 index 0000000..5c02836 --- /dev/null +++ b/tests/ui/method_async_state.stderr @@ -0,0 +1,21 @@ +error: async fn must not take &State as an argument as State is not Sync, consider boxing + --> $DIR/method_async_state.rs:9:19 + | +9 | async fn read_all(state : &State) + | ^^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_async_state.rs:4:10 + | +4 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + +warning: unused import: `gotham_restful::State` + --> $DIR/method_async_state.rs:2:5 + | +2 | use gotham_restful::State; + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default diff --git a/tests/ui/method_for_unknown_resource.rs b/tests/ui/method_for_unknown_resource.rs new file mode 100644 index 0000000..162dc94 --- /dev/null +++ b/tests/ui/method_for_unknown_resource.rs @@ -0,0 +1,10 @@ +#[macro_use] extern crate gotham_restful; + +#[read_all(UnknownResource)] +fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_for_unknown_resource.stderr b/tests/ui/method_for_unknown_resource.stderr new file mode 100644 index 0000000..1e10d24 --- /dev/null +++ b/tests/ui/method_for_unknown_resource.stderr @@ -0,0 +1,5 @@ +error[E0412]: cannot find type `UnknownResource` in this scope + --> $DIR/method_for_unknown_resource.rs:3:12 + | +3 | #[read_all(UnknownResource)] + | ^^^^^^^^^^^^^^^ not found in this scope diff --git a/tests/ui/endpoint/self.rs b/tests/ui/method_no_resource.rs similarity index 50% rename from tests/ui/endpoint/self.rs rename to tests/ui/method_no_resource.rs index 17591d0..f0232b7 100644 --- a/tests/ui/endpoint/self.rs +++ b/tests/ui/method_no_resource.rs @@ -1,11 +1,14 @@ -#[macro_use] -extern crate gotham_restful; +#[macro_use] extern crate gotham_restful; #[derive(Resource)] #[resource(read_all)] struct FooResource; #[read_all] -fn read_all(self) {} +fn read_all() +{ +} -fn main() {} +fn main() +{ +} diff --git a/tests/ui/method_no_resource.stderr b/tests/ui/method_no_resource.stderr new file mode 100644 index 0000000..d4bc1c4 --- /dev/null +++ b/tests/ui/method_no_resource.stderr @@ -0,0 +1,15 @@ +error: Missing Resource struct. Example: #[read_all(MyResource)] + --> $DIR/method_no_resource.rs:7:1 + | +7 | #[read_all] + | ^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_no_resource.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/endpoint/invalid_attribute.rs b/tests/ui/method_self.rs similarity index 53% rename from tests/ui/endpoint/invalid_attribute.rs rename to tests/ui/method_self.rs index 3c321a5..3b19b11 100644 --- a/tests/ui/endpoint/invalid_attribute.rs +++ b/tests/ui/method_self.rs @@ -1,11 +1,14 @@ -#[macro_use] -extern crate gotham_restful; +#[macro_use] extern crate gotham_restful; #[derive(Resource)] #[resource(read_all)] struct FooResource; #[read_all(FooResource)] -fn read_all() {} +fn read_all(self) +{ +} -fn main() {} +fn main() +{ +} diff --git a/tests/ui/method_self.stderr b/tests/ui/method_self.stderr new file mode 100644 index 0000000..d4fea5f --- /dev/null +++ b/tests/ui/method_self.stderr @@ -0,0 +1,13 @@ +error: Didn't expect self parameter + --> $DIR/method_self.rs:8:13 + | +8 | fn read_all(self) + | ^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_self.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_few_args.rs b/tests/ui/method_too_few_args.rs new file mode 100644 index 0000000..6f0309e --- /dev/null +++ b/tests/ui/method_too_few_args.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read)] +struct FooResource; + +#[read(FooResource)] +fn read() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_too_few_args.stderr b/tests/ui/method_too_few_args.stderr new file mode 100644 index 0000000..d8daeab --- /dev/null +++ b/tests/ui/method_too_few_args.stderr @@ -0,0 +1,13 @@ +error: Too few arguments + --> $DIR/method_too_few_args.rs:8:4 + | +8 | fn read() + | ^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read` + --> $DIR/method_too_few_args.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_too_many_args.rs b/tests/ui/method_too_many_args.rs new file mode 100644 index 0000000..5ae83eb --- /dev/null +++ b/tests/ui/method_too_many_args.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all(_id : u64) +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_too_many_args.stderr b/tests/ui/method_too_many_args.stderr new file mode 100644 index 0000000..3f8bd39 --- /dev/null +++ b/tests/ui/method_too_many_args.stderr @@ -0,0 +1,13 @@ +error: Too many arguments + --> $DIR/method_too_many_args.rs:8:13 + | +8 | fn read_all(_id : u64) + | ^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_too_many_args.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/method_unsafe.rs b/tests/ui/method_unsafe.rs new file mode 100644 index 0000000..65a76bc --- /dev/null +++ b/tests/ui/method_unsafe.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_all)] +struct FooResource; + +#[read_all(FooResource)] +unsafe fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/method_unsafe.stderr b/tests/ui/method_unsafe.stderr new file mode 100644 index 0000000..aeb104e --- /dev/null +++ b/tests/ui/method_unsafe.stderr @@ -0,0 +1,13 @@ +error: Resource methods must not be unsafe + --> $DIR/method_unsafe.rs:8:1 + | +8 | unsafe fn read_all() + | ^^^^^^ + +error[E0433]: failed to resolve: use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + --> $DIR/method_unsafe.rs:3:10 + | +3 | #[derive(Resource)] + | ^^^^^^^^ use of undeclared type or module `_gotham_restful_resource_foo_resource_method_read_all` + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/openapi_type_enum_with_fields.rs b/tests/ui/openapi_type_enum_with_fields.rs new file mode 100644 index 0000000..6bc6814 --- /dev/null +++ b/tests/ui/openapi_type_enum_with_fields.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +enum Food +{ + Pasta, + Pizza { pineapple : bool }, + Rice, + Other(String) +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_enum_with_fields.stderr b/tests/ui/openapi_type_enum_with_fields.stderr new file mode 100644 index 0000000..1620970 --- /dev/null +++ b/tests/ui/openapi_type_enum_with_fields.stderr @@ -0,0 +1,11 @@ +error: #[derive(OpenapiType)] does not support enum variants with fields + --> $DIR/openapi_type_enum_with_fields.rs:7:2 + | +7 | Pizza { pineapple : bool }, + | ^^^^^ + +error: #[derive(OpenapiType)] does not support enum variants with fields + --> $DIR/openapi_type_enum_with_fields.rs:9:2 + | +9 | Other(String) + | ^^^^^ diff --git a/tests/ui/openapi_type_nullable_non_bool.rs b/tests/ui/openapi_type_nullable_non_bool.rs new file mode 100644 index 0000000..1e0af28 --- /dev/null +++ b/tests/ui/openapi_type_nullable_non_bool.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo +{ + #[openapi(nullable = "yes, please")] + bar : String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_nullable_non_bool.stderr b/tests/ui/openapi_type_nullable_non_bool.stderr new file mode 100644 index 0000000..0ce2be3 --- /dev/null +++ b/tests/ui/openapi_type_nullable_non_bool.stderr @@ -0,0 +1,5 @@ +error: Expected bool + --> $DIR/openapi_type_nullable_non_bool.rs:6:23 + | +6 | #[openapi(nullable = "yes, please")] + | ^^^^^^^^^^^^^ diff --git a/tests/ui/openapi_type_rename_non_string.rs b/tests/ui/openapi_type_rename_non_string.rs new file mode 100644 index 0000000..0847f14 --- /dev/null +++ b/tests/ui/openapi_type_rename_non_string.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo +{ + #[openapi(rename = 42)] + bar : String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_rename_non_string.stderr b/tests/ui/openapi_type_rename_non_string.stderr new file mode 100644 index 0000000..9bb9dd2 --- /dev/null +++ b/tests/ui/openapi_type_rename_non_string.stderr @@ -0,0 +1,5 @@ +error: Expected string literal + --> $DIR/openapi_type_rename_non_string.rs:6:21 + | +6 | #[openapi(rename = 42)] + | ^^ diff --git a/tests/ui/openapi_type_tuple_struct.rs b/tests/ui/openapi_type_tuple_struct.rs new file mode 100644 index 0000000..0247478 --- /dev/null +++ b/tests/ui/openapi_type_tuple_struct.rs @@ -0,0 +1,8 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo(String); + +fn main() +{ +} diff --git a/tests/ui/openapi_type_tuple_struct.stderr b/tests/ui/openapi_type_tuple_struct.stderr new file mode 100644 index 0000000..2028d18 --- /dev/null +++ b/tests/ui/openapi_type_tuple_struct.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] does not support unnamed fields + --> $DIR/openapi_type_tuple_struct.rs:4:11 + | +4 | struct Foo(String); + | ^^^^^^^^ diff --git a/tests/ui/openapi_type_union.rs b/tests/ui/openapi_type_union.rs new file mode 100644 index 0000000..4bc7355 --- /dev/null +++ b/tests/ui/openapi_type_union.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +union IntOrPointer +{ + int: u64, + pointer: *mut String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_union.stderr b/tests/ui/openapi_type_union.stderr new file mode 100644 index 0000000..52639fe --- /dev/null +++ b/tests/ui/openapi_type_union.stderr @@ -0,0 +1,5 @@ +error: #[derive(OpenapiType)] only works for structs and enums + --> $DIR/openapi_type_union.rs:4:1 + | +4 | union IntOrPointer + | ^^^^^ diff --git a/tests/ui/openapi_type_unknown_key.rs b/tests/ui/openapi_type_unknown_key.rs new file mode 100644 index 0000000..9157e16 --- /dev/null +++ b/tests/ui/openapi_type_unknown_key.rs @@ -0,0 +1,12 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(OpenapiType)] +struct Foo +{ + #[openapi(like = "pizza")] + bar : String +} + +fn main() +{ +} diff --git a/tests/ui/openapi_type_unknown_key.stderr b/tests/ui/openapi_type_unknown_key.stderr new file mode 100644 index 0000000..f8e78b7 --- /dev/null +++ b/tests/ui/openapi_type_unknown_key.stderr @@ -0,0 +1,5 @@ +error: Unknown key + --> $DIR/openapi_type_unknown_key.rs:6:12 + | +6 | #[openapi(like = "pizza")] + | ^^^^ diff --git a/tests/ui/resource/unknown_method.rs b/tests/ui/resource/unknown_method.rs deleted file mode 100644 index 18e8f7b..0000000 --- a/tests/ui/resource/unknown_method.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_any)] -struct FooResource; - -#[read_all] -fn read_all() {} - -fn main() {} diff --git a/tests/ui/resource/unknown_method.stderr b/tests/ui/resource/unknown_method.stderr deleted file mode 100644 index 4a2a67e..0000000 --- a/tests/ui/resource/unknown_method.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0412]: cannot find type `read_any___gotham_restful_endpoint` in this scope - --> $DIR/unknown_method.rs:5:12 - | -5 | #[resource(read_any)] - | ^^^^^^^^ help: a struct with a similar name exists: `read_all___gotham_restful_endpoint` -... -8 | #[read_all] - | ----------- similarly named struct `read_all___gotham_restful_endpoint` defined here diff --git a/tests/ui/resource_unknown_method.rs b/tests/ui/resource_unknown_method.rs new file mode 100644 index 0000000..f246ed1 --- /dev/null +++ b/tests/ui/resource_unknown_method.rs @@ -0,0 +1,14 @@ +#[macro_use] extern crate gotham_restful; + +#[derive(Resource)] +#[resource(read_any)] +struct FooResource; + +#[read_all(FooResource)] +fn read_all() +{ +} + +fn main() +{ +} diff --git a/tests/ui/resource_unknown_method.stderr b/tests/ui/resource_unknown_method.stderr new file mode 100644 index 0000000..3282dbe --- /dev/null +++ b/tests/ui/resource_unknown_method.stderr @@ -0,0 +1,14 @@ +error: Unknown method: `read_any' + --> $DIR/resource_unknown_method.rs:4:12 + | +4 | #[resource(read_any)] + | ^^^^^^^^ + +error[E0277]: the trait bound `FooResource: gotham_restful::Resource` is not satisfied + --> $DIR/resource_unknown_method.rs:7:1 + | +7 | #[read_all(FooResource)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `gotham_restful::Resource` is not implemented for `FooResource` + | + = help: see issue #48214 + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/rustfmt.sh b/tests/ui/rustfmt.sh deleted file mode 100755 index b5724e4..0000000 --- a/tests/ui/rustfmt.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/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 diff --git a/tests/util/mod.rs b/tests/util/mod.rs index 40cae26..8846352 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -2,14 +2,12 @@ use gotham::{ hyper::Body, test::TestServer }; -use log::info; use mime::Mime; #[allow(unused_imports)] use std::{fs::File, io::{Read, Write}, str}; pub fn test_get_response(server : &TestServer, path : &str, expected : &[u8]) { - info!("GET {}", path); let res = server.client().get(path).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -19,7 +17,6 @@ pub fn test_post_response(server : &TestServer, path : &str, body : B, mime : where B : Into { - info!("POST {}", path); let res = server.client().post(path, body, mime).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -29,7 +26,6 @@ pub fn test_put_response(server : &TestServer, path : &str, body : B, mime : where B : Into { - info!("PUT {}", path); let res = server.client().put(path, body, mime).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -37,7 +33,6 @@ where pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8]) { - info!("DELETE {}", path); let res = server.client().delete(path).perform().unwrap().read_body().unwrap(); let body : &[u8] = res.as_ref(); assert_eq!(body, expected); @@ -46,14 +41,12 @@ pub fn test_delete_response(server : &TestServer, path : &str, expected : &[u8]) #[cfg(feature = "openapi")] pub fn test_openapi_response(server : &TestServer, path : &str, output_file : &str) { - info!("GET {}", path); let res = server.client().get(path).perform().unwrap().read_body().unwrap(); let body = serde_json::to_string_pretty(&serde_json::from_slice::(res.as_ref()).unwrap()).unwrap(); match File::open(output_file) { Ok(mut file) => { let mut expected = String::new(); file.read_to_string(&mut expected).unwrap(); - eprintln!("{}", body); assert_eq!(body, expected); }, Err(_) => {