diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3db33c0..1d4bf0e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,143 +1,59 @@ stages: - test - - build - publish 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 before_script: - cargo -V script: - - cargo test --manifest-path openapi_type/Cargo.toml -- --skip trybuild - - cargo test + - cargo test --workspace --lib + - cargo test --workspace --doc cache: - key: cargo-1-49-default paths: - cargo/ - target/ -test-full: +test-all: stage: test - image: rust:1.49-slim + image: msrd0/rust:alpine-tarpaulin before_script: - - apt update -y - - apt install -y --no-install-recommends libpq-dev - cargo -V script: - - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- --skip trybuild - - cargo test --no-default-features --features full - cache: - key: cargo-1-49-all - paths: - - cargo/ - - target/ - -test-tarpaulin: - stage: test - image: rust:slim - before_script: - - apt update -y - - apt install -y --no-install-recommends libpq-dev libssl-dev pkgconf - - cargo -V - - cargo install cargo-tarpaulin - script: - - cargo tarpaulin --target-dir target/tarpaulin --no-default-features --features full --exclude-files 'cargo/*' --exclude-files 'derive/*' --exclude-files 'example/*' --exclude-files 'target/*' --ignore-panics --ignore-tests --out Html --out Xml -v + - cargo test --workspace --all-features --doc + - cargo tarpaulin --all --all-features --exclude-files 'cargo/*' --exclude-files 'gotham_restful_derive/*' --exclude-files 'example/*' --ignore-panics --ignore-tests --out Html -v artifacts: paths: - tarpaulin-report.html - reports: - cobertura: cobertura.xml cache: - key: cargo-stable-all - paths: - - cargo/ - - target/ - -test-trybuild-ui: - stage: test - image: rust:1.50-slim - before_script: - - apt update -y - - apt install -y --no-install-recommends libpq-dev - - cargo -V - script: - - cargo test --manifest-path openapi_type/Cargo.toml --all-features -- trybuild - - cargo test --no-default-features --features full --tests -- --ignored - cache: - key: cargo-1-50-all paths: - 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 -r gotham_restful -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' >public/index.html - artifacts: - paths: - - public + - cd gotham_restful_derive + - cargo publish + - sleep 10s + - cd ../gotham_restful + - cargo publish + - cd .. only: - - master + - 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..ce5a3ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,77 +1,13 @@ # -*- eval: (cargo-minor-mode 1) -*- [workspace] -members = [".", "./derive", "./example", "./openapi_type", "./openapi_type_derive"] - -[package] -name = "gotham_restful" -version = "0.3.0-dev" -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" -readme = "README.md" -repository = "https://gitlab.com/msrd0/gotham-restful" -include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] - -[badges] -gitlab = { repository = "msrd0/gotham-restful", branch = "master" } - -[dependencies] -futures-core = "0.3.7" -futures-util = "0.3.7" -gotham = { git = "https://github.com/gotham-rs/gotham", default-features = false } -gotham_derive = "0.5.0" -gotham_restful_derive = "0.3.0-dev" -log = "0.4.8" -mime = "0.3.16" -serde = { version = "1.0.110", features = ["derive"] } -serde_json = "1.0.58" -thiserror = "1.0" - -# non-feature optional dependencies -base64 = { version = "0.13.0", optional = true } -cookie = { version = "0.15", optional = true } -gotham_middleware_diesel = { git = "https://github.com/gotham-rs/gotham", optional = true } -indexmap = { version = "1.3.2", optional = true } -indoc = { version = "1.0", optional = true } -jsonwebtoken = { version = "7.1.0", optional = true } -once_cell = { version = "1.5", optional = true } -openapiv3 = { version = "=0.3.2", optional = true } -openapi_type = { version = "0.1.0-dev", optional = true } -regex = { version = "1.4", optional = true } -sha2 = { version = "0.9.3", optional = true } - -[dev-dependencies] -diesel = { version = "1.4.4", features = ["postgres"] } -futures-executor = "0.3.5" -paste = "1.0" -pretty_env_logger = "0.4" -tokio = { version = "1.0", features = ["time"], default-features = false } -thiserror = "1.0.18" -trybuild = "1.0.27" - -[features] -default = ["cors", "errorlog", "without-openapi"] -full = ["auth", "cors", "database", "errorlog", "openapi"] - -auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] -cors = [] -database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] -errorlog = [] - -# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4 -without-openapi = [] -openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "openapi_type", "regex", "sha2"] - -[package.metadata.docs.rs] -no-default-features = true -features = ["full"] +members = [ + "gotham_restful", + "gotham_restful_derive", + "example" +] [patch.crates-io] -gotham_restful = { path = "." } -gotham_restful_derive = { path = "./derive" } -openapi_type = { path = "./openapi_type" } -openapi_type_derive = { path = "./openapi_type_derive" } +gotham_restful = { path = "./gotham_restful" } +gotham_restful_derive = { path = "./gotham_restful_derive" } +openapiv3 = { git = "https://github.com/glademiller/openapiv3", rev = "4c3bd95c966a3f9d59bb494c3d8e30c5c3068bdb" } \ No newline at end of file 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..7af32f1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,108 @@ -# Moved to GitHub +
+

gotham-restful

+
+
+ + pipeline status + + + coverage report + + + crates.io + + + docs.rs + + + Build with Rust + + + Minimum Rust Version + +
+
-This project has moved to GitHub: https://github.com/msrd0/gotham_restful +This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to +have several RESTful resources that can be added to the gotham router. This crate will take care +of everything else, like parsing path/query parameters, request bodies, and writing response +bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)serializing. If you +enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful +resources. +## Usage + +A basic server with only one resource, handling a simple `GET` request, could look like this: + +```rust +/// Our RESTful Resource. +#[derive(Resource)] +#[rest_resource(read_all)] +struct UsersResource; + +/// Our return type. +#[derive(Deserialize, Serialize)] +struct User { + id: i64, + username: String, + email: String +} + +/// Our handler method. +#[rest_read_all(UsersResource)] +fn read_all(_state: &mut State) -> Success> { + vec![User { + id: 1, + username: "h4ck3r".to_string(), + email: "h4ck3r@example.org".to_string() + }].into() +} + +/// Our main method. +fn main() { + gotham::start("127.0.0.1:8080", build_simple_router(|route| { + route.resource::("users"); + })); +} +``` + +Uploads and Downloads can also be handled, but you need to specify the mime type manually: + +```rust +#[derive(Resource)] +#[rest_resource(create)] +struct ImageResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage(Vec); + +#[rest_create(ImageResource)] +fn create(_state : &mut State, body : RawImage) -> Raw> { + Raw::new(body.0, mime::APPLICATION_OCTET_STREAM) +} +``` + +Look at the [example] for more methods and usage with the `openapi` feature. + +## Known Issues + +These are currently known major issues. For a complete list please see +[the issue tracker](https://gitlab.com/msrd0/gotham-restful/issues). +If you encounter any issues that aren't yet reported, please report them +[here](https://gitlab.com/msrd0/gotham-restful/issues/new). + + - Enabling the `openapi` feature might break code ([#4](https://gitlab.com/msrd0/gotham-restful/issues/4)) + - For `chrono`'s `DateTime` types, the format is `date-time` instead of `datetime` ([openapiv3#14](https://github.com/glademiller/openapiv3/pull/14)) + +## License + +Licensed under your option of: + - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) + - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) + + +[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example +[gotham]: https://gotham.rs/ +[serde]: https://github.com/serde-rs/serde#serde----- +[serde_json]: https://github.com/serde-rs/json#serde-json---- diff --git a/README.tpl b/README.tpl index 1769315..a148bef 100644 --- a/README.tpl +++ b/README.tpl @@ -1,61 +1,26 @@ -
-
- +
+

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 - {{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/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/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 deleted file mode 100644 index a4ec7b2..0000000 --- a/derive/src/from_body.rs +++ /dev/null @@ -1,122 +0,0 @@ -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use std::cmp::min; -use syn::{spanned::Spanned, Data, DeriveInput, Error, Field, Fields, Ident, Result, Type}; - -struct ParsedFields { - fields: Vec<(Ident, Type)>, - named: bool -} - -impl ParsedFields { - fn from_named(fields: I) -> Self - where - I: Iterator - { - let fields = fields.map(|field| (field.ident.unwrap(), field.ty)).collect(); - Self { fields, named: true } - } - - fn from_unnamed(fields: I) -> Self - where - I: Iterator - { - let fields = fields - .enumerate() - .map(|(i, field)| (format_ident!("arg{}", i), field.ty)) - .collect(); - Self { fields, named: false } - } - - fn from_unit() -> Self { - Self { - fields: Vec::new(), - named: false - } - } -} - -pub fn expand_from_body(input: DeriveInput) -> Result { - let krate = super::krate(); - let ident = input.ident; - let generics = input.generics; - - let strukt = match input.data { - Data::Enum(inum) => Err(inum.enum_token.span()), - Data::Struct(strukt) => Ok(strukt), - Data::Union(uni) => Err(uni.union_token.span()) - } - .map_err(|span| Error::new(span, "#[derive(FromBody)] only works for structs"))?; - - let fields = match strukt.fields { - Fields::Named(named) => ParsedFields::from_named(named.named.into_iter()), - Fields::Unnamed(unnamed) => ParsedFields::from_unnamed(unnamed.unnamed.into_iter()), - Fields::Unit => ParsedFields::from_unit() - }; - - let mut where_clause = quote!(); - let mut block = quote!(); - let mut body_ident = format_ident!("_body"); - let mut type_ident = format_ident!("_type"); - - if let Some(body_field) = fields.fields.get(0) { - body_ident = body_field.0.clone(); - let body_ty = &body_field.1; - where_clause = quote! { - #where_clause - #body_ty : for<'a> From<&'a [u8]>, - }; - block = quote! { - #block - let #body_ident : &[u8] = &#body_ident; - let #body_ident : #body_ty = #body_ident.into(); - }; - } - - if let Some(type_field) = fields.fields.get(1) { - type_ident = type_field.0.clone(); - let type_ty = &type_field.1; - where_clause = quote! { - #where_clause - #type_ty : From<#krate::Mime>, - }; - block = quote! { - #block - let #type_ident : #type_ty = #type_ident.into(); - }; - } - - for field in &fields.fields[min(2, fields.fields.len())..] { - let field_ident = &field.0; - let field_ty = &field.1; - where_clause = quote! { - #where_clause - #field_ty : Default, - }; - block = quote! { - #block - let #field_ident : #field_ty = Default::default(); - }; - } - - let field_names: Vec<&Ident> = fields.fields.iter().map(|field| &field.0).collect(); - let ctor = if fields.named { - quote!(Self { #(#field_names),* }) - } else { - quote!(Self ( #(#field_names),* )) - }; - - Ok(quote! { - impl #generics #krate::FromBody for #ident #generics - where #where_clause - { - type Err = ::std::convert::Infallible; - - fn from_body(#body_ident : #krate::gotham::hyper::body::Bytes, #type_ident : #krate::Mime) -> Result - { - #block - Ok(#ctor) - } - } - }) -} diff --git a/derive/src/lib.rs b/derive/src/lib.rs deleted file mode 100644 index 59ee8b6..0000000 --- a/derive/src/lib.rs +++ /dev/null @@ -1,129 +0,0 @@ -#![warn(missing_debug_implementations, rust_2018_idioms)] -#![deny(broken_intra_doc_links)] -#![forbid(unsafe_code)] - -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; -use syn::{parse_macro_input, parse_macro_input::ParseMacroInput, DeriveInput, Result}; - -mod util; - -mod endpoint; -use endpoint::{expand_endpoint, EndpointType}; - -mod from_body; -use from_body::expand_from_body; - -mod request_body; -use request_body::expand_request_body; - -mod resource; -use resource::expand_resource; - -mod resource_error; -use resource_error::expand_resource_error; - -mod private_openapi_trait; -use private_openapi_trait::expand_private_openapi_trait; - -#[inline] -fn print_tokens(tokens: TokenStream2) -> TokenStream { - // eprintln!("{}", tokens); - tokens.into() -} - -#[inline] -fn expand_derive(input: TokenStream, expand: F) -> TokenStream -where - F: FnOnce(DeriveInput) -> Result -{ - 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 -where - 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())) -} - -#[inline] -fn krate() -> TokenStream2 { - quote!(::gotham_restful) -} - -#[proc_macro_derive(FromBody)] -pub fn derive_from_body(input: TokenStream) -> TokenStream { - expand_derive(input, expand_from_body) -} - -#[proc_macro_derive(RequestBody, attributes(supported_types))] -pub fn derive_request_body(input: TokenStream) -> TokenStream { - expand_derive(input, expand_request_body) -} - -#[proc_macro_derive(Resource, attributes(resource))] -pub fn derive_resource(input: TokenStream) -> TokenStream { - expand_derive(input, expand_resource) -} - -#[proc_macro_derive(ResourceError, attributes(display, from, status))] -pub fn derive_resource_error(input: TokenStream) -> TokenStream { - expand_derive(input, expand_resource_error) -} - -#[proc_macro_attribute] -pub fn endpoint(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::custom(), attr, item)) -} - -#[proc_macro_attribute] -pub fn read_all(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::ReadAll, attr, item)) -} - -#[proc_macro_attribute] -pub fn read(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Read, attr, item)) -} - -#[proc_macro_attribute] -pub fn search(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Search, attr, item)) -} - -#[proc_macro_attribute] -pub fn create(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Create, attr, item)) -} - -#[proc_macro_attribute] -pub fn change_all(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::UpdateAll, attr, item)) -} - -#[proc_macro_attribute] -pub fn change(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Update, attr, item)) -} - -#[proc_macro_attribute] -pub fn remove_all(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::DeleteAll, attr, item)) -} - -#[proc_macro_attribute] -pub fn remove(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, |attr, item| expand_endpoint(EndpointType::Delete, attr, item)) -} - -/// PRIVATE MACRO - DO NOT USE -#[doc(hidden)] -#[proc_macro_attribute] -pub fn _private_openapi_trait(attr: TokenStream, item: TokenStream) -> TokenStream { - expand_macro(attr, item, expand_private_openapi_trait) -} 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 deleted file mode 100644 index c543dfa..0000000 --- a/derive/src/request_body.rs +++ /dev/null @@ -1,97 +0,0 @@ -use crate::util::CollectToResult; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use std::iter; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - spanned::Spanned, - DeriveInput, Error, Generics, Path, Result, Token -}; - -struct MimeList(Punctuated); - -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 { - quote!() -} - -#[cfg(feature = "openapi")] -fn impl_openapi_type(ident: &Ident, generics: &Generics) -> TokenStream { - let krate = super::krate(); - let openapi = quote!(#krate::private::openapi); - - quote! { - impl #generics #krate::private::OpenapiType for #ident #generics - { - fn schema() -> #krate::private::OpenapiSchema - { - #krate::private::OpenapiSchema::new( - #openapi::SchemaKind::Type( - #openapi::Type::String( - #openapi::StringType { - format: #openapi::VariantOrUnknownOrEmpty::Item( - #openapi::StringFormat::Binary - ), - .. ::std::default::Default::default() - } - ) - ) - ) - } - } - } -} - -pub fn expand_request_body(input: DeriveInput) -> Result { - let krate = super::krate(); - let ident = input.ident; - let generics = input.generics; - - let types = input - .attrs - .into_iter() - .filter(|attr| { - attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string()) - }) - .flat_map(|attr| { - let span = attr.span(); - attr.parse_args::() - .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)]" - )); - Box::new(iter::once(Err(err))) - }) - }) - .collect_to_result()?; - - let types = match types { - ref types if types.is_empty() => quote!(None), - types => quote!(Some(vec![#(#types),*])) - }; - - let impl_openapi_type = impl_openapi_type(&ident, &generics); - - Ok(quote! { - impl #generics #krate::RequestBody for #ident #generics - where #ident #generics : #krate::FromBody - { - fn supported_types() -> Option> - { - #types - } - } - - #impl_openapi_type - }) -} diff --git a/derive/src/resource.rs b/derive/src/resource.rs deleted file mode 100644 index 8e30275..0000000 --- a/derive/src/resource.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - endpoint::endpoint_ident, - util::{CollectToResult, PathEndsWith} -}; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use std::iter; -use syn::{ - parenthesized, - parse::{Parse, ParseStream}, - punctuated::Punctuated, - DeriveInput, Result, Token -}; - -struct MethodList(Punctuated); - -impl Parse for MethodList { - fn parse(input: ParseStream<'_>) -> Result { - let content; - let _paren = parenthesized!(content in input); - let list = Punctuated::parse_separated_nonempty(&content)?; - Ok(Self(list)) - } -} - -pub fn expand_resource(input: DeriveInput) -> Result { - 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! { - impl #krate::Resource for #ident - { - fn setup(mut route : D) - { - #(#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 deleted file mode 100644 index 9239c80..0000000 --- a/derive/src/resource_error.rs +++ /dev/null @@ -1,331 +0,0 @@ -use crate::util::{remove_parens, CollectToResult}; -use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote}; -use std::iter; -use syn::{ - spanned::Spanned, Attribute, Data, DeriveInput, Error, Fields, GenericParam, LitStr, Path, PathSegment, Result, Type, - Variant -}; - -struct ErrorVariantField { - attrs: Vec, - ident: Ident, - ty: Type -} - -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 - }; - - let mut is_named = false; - let mut fields = Vec::new(); - match variant.fields { - Fields::Named(named) => { - is_named = true; - for field in named.named { - let span = field.span(); - fields.push(ErrorVariantField { - attrs: field.attrs, - ident: field - .ident - .ok_or_else(|| Error::new(span, "Missing ident for this enum variant field"))?, - ty: field.ty - }); - } - }, - Fields::Unnamed(unnamed) => { - for (i, field) in unnamed.unnamed.into_iter().enumerate() { - fields.push(ErrorVariantField { - attrs: field.attrs, - ident: format_ident!("arg{}", i), - ty: field.ty - }) - } - }, - Fields::Unit => {} - } - - let from_ty = fields - .iter() - .enumerate() - .find(|(_, field)| { - field - .attrs - .iter() - .any(|attr| attr.path.segments.last().map(|segment| segment.ident.to_string()) == Some("from".to_string())) - }) - .map(|(i, field)| (i, field.ty.clone())); - - let display = match variant.attrs.iter().find(|attr| { - attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("display".to_string()) - }) { - Some(attr) => Some(syn::parse2(remove_parens(attr.tokens.clone()))?), - None => None - }; - - Ok(ErrorVariant { - ident: variant.ident, - status, - is_named, - fields, - from_ty, - display - }) -} - -fn path_segment(name: &str) -> PathSegment { - PathSegment { - ident: format_ident!("{}", name), - arguments: Default::default() - } -} - -impl ErrorVariant { - fn fields_pat(&self) -> TokenStream { - let mut fields = self.fields.iter().map(|field| &field.ident).peekable(); - if fields.peek().is_none() { - quote!() - } else if self.is_named { - quote!( { #( #fields ),* } ) - } else { - quote!( ( #( #fields ),* ) ) - } - } - - fn to_display_match_arm(&self, formatter_ident: &Ident, enum_ident: &Ident) -> Result { - let ident = &self.ident; - let display = self - .display - .as_ref() - .ok_or_else(|| Error::new(self.ident.span(), "Missing display string for this variant"))?; - - // lets find all required format parameters - let display_str = display.value(); - let mut params: Vec<&str> = Vec::new(); - let len = display_str.len(); - let mut start = len; - let mut iter = display_str.chars().enumerate().peekable(); - while let Some((i, c)) = iter.next() { - // we found a new opening brace - if start == len && c == '{' { - start = i + 1; - } - // we found a duplicate opening brace - else if start == i && c == '{' { - start = len; - } - // we found a closing brace - else if start < i && c == '}' { - match iter.peek() { - Some((_, '}')) => { - return Err(Error::new( - display.span(), - "Error parsing format string: curly braces not allowed inside parameter name" - )) - }, - _ => params.push(&display_str[start..i]) - }; - start = len; - } - // we found a closing brace without content - else if start == i && c == '}' { - return Err(Error::new( - display.span(), - "Error parsing format string: parameter name must not be empty" - )); - } - } - if start != len { - return Err(Error::new( - display.span(), - "Error parsing format string: Unmatched opening brace" - )); - } - let params = params - .into_iter() - .map(|name| format_ident!("{}{}", if self.is_named { "" } else { "arg" }, name)); - - let fields_pat = self.fields_pat(); - Ok(quote! { - #enum_ident::#ident #fields_pat => write!(#formatter_ident, #display #(, #params = #params)*) - }) - } - - fn into_match_arm(self, krate: &TokenStream, enum_ident: &Ident) -> Result { - let ident = &self.ident; - let fields_pat = self.fields_pat(); - let status = self.status.map(|status| { - // the status might be relative to StatusCode, so let's fix that - if status.leading_colon.is_none() && status.segments.len() < 2 { - let status_ident = status.segments.first().cloned().unwrap_or_else(|| path_segment("OK")); - Path { - leading_colon: Some(Default::default()), - segments: vec![ - path_segment("gotham_restful"), - path_segment("gotham"), - path_segment("hyper"), - path_segment("StatusCode"), - status_ident, - ] - .into_iter() - .collect() - } - } else { - status - } - }); - - // the response will come directly from the from_ty if present - let res = match (self.from_ty, status) { - (Some((from_index, _)), None) => { - let from_field = &self.fields[from_index].ident; - quote!(#from_field.into_response_error()) - }, - (Some(_), Some(_)) => return Err(Error::new(ident.span(), "When #[from] is used, #[status] must not be used!")), - (None, Some(status)) => quote!(Ok(#krate::Response::new( - { #status }.into(), - #krate::gotham::hyper::Body::empty(), - None - ))), - (None, None) => return Err(Error::new(ident.span(), "Missing #[status(code)] for this variant")) - }; - - Ok(quote! { - #enum_ident::#ident #fields_pat => #res - }) - } - - fn were(&self) -> Option { - match self.from_ty.as_ref() { - Some((_, ty)) => Some(quote!( #ty : ::std::error::Error )), - None => None - } - } -} - -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 { - let were = generics.params.iter().filter_map(|param| match param { - GenericParam::Type(ty) => { - let ident = &ty.ident; - Some(quote!(#ident : ::std::fmt::Display)) - }, - _ => None - }); - let formatter_ident = format_ident!("resource_error_display_formatter"); - let match_arms = variants - .iter() - .map(|v| v.to_display_match_arm(&formatter_ident, &ident)) - .collect_to_result()?; - Some(quote! { - impl #generics ::std::fmt::Display for #ident #generics - where #( #were ),* - { - fn fmt(&self, #formatter_ident: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result - { - match self { - #( #match_arms ),* - } - } - } - }) - }; - - let mut from_impls: Vec = Vec::new(); - - for var in &variants { - let var_ident = &var.ident; - let (from_index, from_ty) = match var.from_ty.as_ref() { - Some(f) => f, - None => continue - }; - let from_ident = &var.fields[*from_index].ident; - - let fields_pat = var.fields_pat(); - let fields_where = var - .fields - .iter() - .enumerate() - .filter(|(i, _)| i != from_index) - .map(|(_, field)| { - let ty = &field.ty; - quote!( #ty : Default ) - }) - .chain(iter::once(quote!( #from_ty : ::std::error::Error ))); - let fields_let = var - .fields - .iter() - .enumerate() - .filter(|(i, _)| i != from_index) - .map(|(_, field)| { - let id = &field.ident; - let ty = &field.ty; - quote!( let #id : #ty = Default::default(); ) - }); - - from_impls.push(quote! { - impl #generics ::std::convert::From<#from_ty> for #ident #generics - where #( #fields_where ),* - { - fn from(#from_ident : #from_ty) -> Self - { - #( #fields_let )* - Self::#var_ident #fields_pat - } - } - }); - } - - let were = variants.iter().filter_map(|variant| variant.were()).collect::>(); - let variants = variants - .into_iter() - .map(|variant| variant.into_match_arm(&krate, &ident)) - .collect_to_result()?; - - Ok(quote! { - #display_impl - - impl #generics #krate::IntoResponseError for #ident #generics - where #( #were ),* - { - type Err = #krate::private::serde_json::Error; - - fn into_response_error(self) -> Result<#krate::Response, Self::Err> - { - match self { - #( #variants ),* - } - } - } - - #( #from_impls )* - }) -} diff --git a/derive/src/util.rs b/derive/src/util.rs deleted file mode 100644 index ef55659..0000000 --- a/derive/src/util.rs +++ /dev/null @@ -1,74 +0,0 @@ -use proc_macro2::{Delimiter, TokenStream, TokenTree}; -use std::iter; -use syn::{Error, Lit, LitBool, LitStr, Path, Result}; - -pub(crate) trait CollectToResult { - type Item; - - fn collect_to_result(self) -> Result>; -} - -impl CollectToResult for I -where - 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) - } - }) - } -} - -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 { - let iter = input.into_iter().flat_map(|tt| { - if let TokenTree::Group(group) = &tt { - if group.delimiter() == Delimiter::Parenthesis { - return Box::new(group.stream().into_iter()) as Box>; - } - } - Box::new(iter::once(tt)) - }); - iter.collect() -} diff --git a/example/Cargo.toml b/example/Cargo.toml index 398a5ef..b7f0eba 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -2,23 +2,28 @@ [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 } -log = "0.4.8" -pretty_env_logger = "0.4" -serde = "1.0.110" +fake = "2.2" +gotham = "0.4" +gotham_derive = "0.4" +gotham_restful = { version = "0.0.4", features = ["auth", "openapi"] } +hyper = "0.12" +log = "0.4" +log4rs = { version = "0.8", features = ["console_appender"], default-features = false } +serde = "1" + +[dev-dependencies] +fake = "2.2" +log = "0.4" +log4rs = { version = "0.8", features = ["console_appender"], default-features = false } diff --git a/example/src/main.rs b/example/src/main.rs index e85f911..3d9a249 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,34 +1,43 @@ -#[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::{ - hyper::header::CONTENT_TYPE, middleware::logger::RequestLogger, pipeline::{new_pipeline, single::single_pipeline}, 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 +#[rest_resource(ReadAll, Read, Search, Create, DeleteAll, Delete, Update, UpdateAll)] +struct Users +{ } -#[read_all] -fn read_all() -> Success>> { +#[derive(Resource)] +#[rest_resource(ReadAll)] +struct Auth +{ +} + +#[derive(Deserialize, OpenapiType, Serialize, StateData, StaticResponseExtender)] +struct User +{ + username : String +} + +#[rest_read_all(Users)] +fn read_all(_state : &mut State) -> Success>> +{ vec![Username().fake(), Username().fake()] .into_iter() .map(|username| Some(User { username })) @@ -36,95 +45,102 @@ fn read_all() -> Success>> { .into() } -#[read] -fn read(id: u64) -> Success { - let username: String = Username().fake(); - User { - username: format!("{}{}", username, id) - } - .into() +#[rest_read(Users)] +fn read(_state : &mut State, id : u64) -> Success +{ + let username : String = Username().fake(); + User { username: format!("{}{}", username, id) }.into() } -#[search] -fn search(query: User) -> Success { +#[rest_search(Users)] +fn search(_state : &mut State, query : User) -> Success +{ query.into() } -#[create] -fn create(body: User) { +#[rest_create(Users)] +fn create(_state : &mut State, 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::>() - ); +#[rest_update_all(Users)] +fn update_all(_state : &mut State, body : Vec) +{ + info!("Changing all Users to {:?}", body.into_iter().map(|u| u.username).collect::>()); } -#[change] -fn update(id: u64, body: User) { +#[rest_update(Users)] +fn update(_state : &mut State, id : u64, body : User) +{ info!("Change User {} to {}", id, body.username); } -#[remove_all] -fn remove_all() { +#[rest_delete_all(Users)] +fn delete_all(_state : &mut State) +{ info!("Delete all Users"); } -#[remove] -fn remove(id: u64) { +#[rest_delete(Users)] +fn delete(_state : &mut State, id : u64) +{ info!("Delete User {}", id); } -#[read_all] -fn auth_read_all(auth: AuthStatus<()>) -> AuthSuccess { - match auth { - AuthStatus::Authenticated(data) => Ok(format!("{:?}", data)), - _ => Err(Forbidden) - } +#[rest_read_all(Auth)] +fn auth_read_all(auth : AuthStatus<()>) -> AuthResult> +{ + let str : Success = match auth { + AuthStatus::Authenticated(data) => format!("{:?}", data).into(), + _ => return AuthErr + }; + str.into() } -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(); - - let cors = CorsConfig { - origin: Origin::Copy, - headers: Headers::List(vec![CONTENT_TYPE]), - credentials: true, - ..Default::default() - }; - +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 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) + .build() ); + + gotham::start(ADDR, build_router(chain, pipelines, |route| { + route.with_openapi("Users Example".to_owned(), "0.0.1".to_owned(), format!("http://{}", ADDR), |mut route| { + route.resource::("users"); + route.resource::("auth"); + route.get_openapi("openapi"); + }); + })); println!("Gotham started on {} for testing", ADDR); } + diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml new file mode 100644 index 0000000..5f7b2f7 --- /dev/null +++ b/gotham_restful/Cargo.toml @@ -0,0 +1,48 @@ +# -*- eval: (cargo-minor-mode 1) -*- + +[package] +name = "gotham_restful" +version = "0.0.4" +authors = ["Dominic Meiser "] +edition = "2018" +description = "RESTful additions for Gotham" +keywords = ["gotham", "rest", "restful"] +license = "EPL-2.0 OR Apache-2.0" +readme = "README.md" +repository = "https://gitlab.com/msrd0/gotham-restful" + +[badges] +gitlab = { repository = "msrd0/gotham-restful", branch = "master" } + +[dependencies] +base64 = { version = ">=0.10.1, <0.12", optional = true } +chrono = { version = "0.4.10", optional = true } +cookie = { version = "0.12", optional = true } +futures = "0.1.29" +gotham = "0.4" +gotham_derive = "0.4" +gotham_middleware_diesel = { version = "0.1", optional = true } +gotham_restful_derive = { version = "0.0.3" } +hyper = "0.12.35" +indexmap = { version = "1.3.0", optional = true } +jsonwebtoken = { version = "6.0.1", optional = true } +log = { version = "0.4.8", optional = true } +mime = "0.3.16" +openapiv3 = { version = "0.3", optional = true } +serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0.45" +uuid = { version = ">= 0.1, < 0.9", optional = true } + +[dev-dependencies] +paste = "0.1.10" +thiserror = "1" + +[features] +default = [] +auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] +errorlog = [] +database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] +openapi = ["gotham_restful_derive/openapi", "indexmap", "log", "openapiv3"] + +[package.metadata.docs.rs] +all-features = true diff --git a/gotham_restful/LICENSE-Apache b/gotham_restful/LICENSE-Apache new file mode 120000 index 0000000..0cd69a3 --- /dev/null +++ b/gotham_restful/LICENSE-Apache @@ -0,0 +1 @@ +../LICENSE-Apache \ No newline at end of file diff --git a/gotham_restful/LICENSE-EPL b/gotham_restful/LICENSE-EPL new file mode 120000 index 0000000..2004d06 --- /dev/null +++ b/gotham_restful/LICENSE-EPL @@ -0,0 +1 @@ +../LICENSE-EPL \ No newline at end of file diff --git a/gotham_restful/LICENSE.md b/gotham_restful/LICENSE.md new file mode 120000 index 0000000..7eabdb1 --- /dev/null +++ b/gotham_restful/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/gotham_restful/README.md b/gotham_restful/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/gotham_restful/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/src/auth.rs b/gotham_restful/src/auth.rs similarity index 66% rename from src/auth.rs rename to gotham_restful/src/auth.rs index fdbab63..54c6a89 100644 --- a/src/auth.rs +++ b/gotham_restful/src/auth.rs @@ -1,27 +1,25 @@ -use crate::{AuthError, Forbidden}; - +use crate::HeaderName; use cookie::CookieJar; -use futures_util::{ - future, - future::{FutureExt, TryFutureExt} -}; +use futures::{future, future::Future}; use gotham::{ - anyhow, handler::HandlerFuture, - hyper::header::{HeaderMap, HeaderName, AUTHORIZATION}, - middleware::{cookie::CookieParser, Middleware, NewMiddleware}, + middleware::{Middleware, NewMiddleware}, state::{FromState, State} }; -use jsonwebtoken::{errors::ErrorKind, DecodingKey}; +use hyper::header::{AUTHORIZATION, HeaderMap}; +use jsonwebtoken::errors::ErrorKind; use serde::de::DeserializeOwned; -use std::{marker::PhantomData, panic::RefUnwindSafe, pin::Pin}; +use std::{ + marker::PhantomData, + panic::RefUnwindSafe +}; -#[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 +34,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,20 +48,10 @@ where } } -impl Copy for AuthStatus where T: Copy + Send + 'static {} - -impl AuthStatus { - pub fn ok(self) -> Result { - match self { - Self::Authenticated(data) => Ok(data), - _ => Err(Forbidden) - } - } -} - /// The source of the authentication token in the request. -#[derive(Clone, Debug, StateData)] -pub enum AuthSource { +#[derive(Clone, StateData)] +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 +66,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::{export::State, AuthHandler}; # const SECRET : &'static [u8; 32] = b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc"; @@ -91,29 +79,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()) } } @@ -129,7 +124,7 @@ simply add it to your pipeline and request it inside your handler: # use serde::{Deserialize, Serialize}; # #[derive(Resource)] -#[resource(read_all)] +#[rest_resource(read_all)] struct AuthResource; #[derive(Debug, Deserialize, Clone)] @@ -138,7 +133,7 @@ struct AuthData { exp: u64 } -#[read_all] +#[rest_read_all(AuthResource)] fn read_all(auth : &AuthStatus) -> Success { format!("{:?}", auth).into() } @@ -156,19 +151,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 +175,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 +191,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,119 +203,126 @@ 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 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, &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) + return AuthStatus::Authenticated(data); } } 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) -> Box where - Chain: FnOnce(State) -> Pin> + Chain : FnOnce(State) -> Box { // 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() + Box::new(chain(state).and_then(|(state, res)| future::ok((state, res)))) } } 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 +331,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 +353,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 +377,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 +396,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 +412,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 +428,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 +444,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 +461,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 +478,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/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs new file mode 100644 index 0000000..a30ba32 --- /dev/null +++ b/gotham_restful/src/lib.rs @@ -0,0 +1,181 @@ +/*! +This crate is an extension to the popular [gotham web framework][gotham] for Rust. The idea is to +have several RESTful resources that can be added to the gotham router. This crate will take care +of everything else, like parsing path/query parameters, request bodies, and writing response +bodies, relying on [`serde`][serde] and [`serde_json`][serde_json] for (de)serializing. If you +enable the `openapi` feature, you can also generate an OpenAPI Specification from your RESTful +resources. + +# Usage + +A basic server with only one resource, handling a simple `GET` request, could look like this: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::{router::builder::*, state::State}; +# use gotham_restful::{DrawResources, Resource, Success}; +# use serde::{Deserialize, Serialize}; +/// Our RESTful Resource. +#[derive(Resource)] +#[rest_resource(read_all)] +struct UsersResource; + +/// Our return type. +#[derive(Deserialize, Serialize)] +# #[derive(OpenapiType)] +struct User { + id: i64, + username: String, + email: String +} + +/// Our handler method. +#[rest_read_all(UsersResource)] +fn read_all(_state: &mut State) -> Success> { + vec![User { + id: 1, + username: "h4ck3r".to_string(), + email: "h4ck3r@example.org".to_string() + }].into() +} + +/// Our main method. +fn main() { + gotham::start("127.0.0.1:8080", build_simple_router(|route| { + route.resource::("users"); + })); +} +``` + +Uploads and Downloads can also be handled, but you need to specify the mime type manually: + +```rust,no_run +# #[macro_use] extern crate gotham_restful_derive; +# use gotham::{router::builder::*, state::State}; +# use gotham_restful::{DrawResources, Raw, Resource, Success}; +# use serde::{Deserialize, Serialize}; +#[derive(Resource)] +#[rest_resource(create)] +struct ImageResource; + +#[derive(FromBody, RequestBody)] +#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] +struct RawImage(Vec); + +#[rest_create(ImageResource)] +fn create(_state : &mut State, body : RawImage) -> Raw> { + Raw::new(body.0, mime::APPLICATION_OCTET_STREAM) +} +# fn main() { +# gotham::start("127.0.0.1:8080", build_simple_router(|route| { +# route.resource::("image"); +# })); +# } +``` + +Look at the [example] for more methods and usage with the `openapi` feature. + +# Known Issues + +These are currently known major issues. For a complete list please see +[the issue tracker](https://gitlab.com/msrd0/gotham-restful/issues). +If you encounter any issues that aren't yet reported, please report them +[here](https://gitlab.com/msrd0/gotham-restful/issues/new). + + - Enabling the `openapi` feature might break code ([#4](https://gitlab.com/msrd0/gotham-restful/issues/4)) + - For `chrono`'s `DateTime` types, the format is `date-time` instead of `datetime` ([openapiv3#14](https://github.com/glademiller/openapiv3/pull/14)) + +# License + +Licensed under your option of: + - [Apache License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-Apache) + - [Eclipse Public License Version 2.0](https://gitlab.com/msrd0/gotham-restful/blob/master/LICENSE-EPL) + + +[example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example +[gotham]: https://gotham.rs/ +[serde]: https://github.com/serde-rs/serde#serde----- +[serde_json]: https://github.com/serde-rs/json#serde-json---- +*/ + +// weird proc macro issue +extern crate self as gotham_restful; + +#[macro_use] extern crate gotham_derive; +#[macro_use] extern crate serde; + +#[doc(no_inline)] +pub use hyper::{header::HeaderName, Chunk, StatusCode}; +#[doc(no_inline)] +pub use mime::Mime; + +pub use gotham_restful_derive::*; + +/// Not public API +#[doc(hidden)] +pub mod export +{ + pub use futures::future::Future; + pub use gotham::state::{FromState, State}; + + #[cfg(feature = "database")] + pub use gotham_middleware_diesel::Repo; + + #[cfg(feature = "openapi")] + pub use indexmap::IndexMap; + #[cfg(feature = "openapi")] + pub use openapiv3 as openapi; +} + +#[cfg(feature = "auth")] +mod auth; +#[cfg(feature = "auth")] +pub use auth::{ + AuthHandler, + AuthMiddleware, + AuthSource, + AuthStatus, + AuthValidation, + StaticAuthHandler +}; + +#[cfg(feature = "openapi")] +mod openapi; +#[cfg(feature = "openapi")] +pub use openapi::{ + router::{GetOpenapi, OpenapiRouter}, + types::{OpenapiSchema, OpenapiType} +}; + +mod resource; +pub use resource::{ + Resource, + ResourceMethod, + ResourceReadAll, + ResourceRead, + ResourceSearch, + ResourceCreate, + ResourceUpdateAll, + ResourceUpdate, + ResourceDeleteAll, + ResourceDelete +}; + +mod result; +pub use result::{ + AuthResult, + AuthResult::AuthErr, + NoContent, + Raw, + ResourceResult, + Response, + Success +}; + +mod routing; +pub use routing::{DrawResources, DrawResourceRoutes}; +#[cfg(feature = "openapi")] +pub use routing::WithOpenapi; + +mod types; +pub use types::*; diff --git a/gotham_restful/src/openapi/mod.rs b/gotham_restful/src/openapi/mod.rs new file mode 100644 index 0000000..5c19494 --- /dev/null +++ b/gotham_restful/src/openapi/mod.rs @@ -0,0 +1,3 @@ + +pub mod router; +pub mod types; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs new file mode 100644 index 0000000..1789c80 --- /dev/null +++ b/gotham_restful/src/openapi/router.rs @@ -0,0 +1,557 @@ +use crate::{ + resource::*, + result::*, + routing::*, + OpenapiSchema, + OpenapiType, + RequestBody +}; +use futures::future::ok; +use gotham::{ + handler::{Handler, HandlerFuture, NewHandler}, + helpers::http::response::create_response, + pipeline::chain::PipelineHandleChain, + router::builder::*, + state::State +}; +use indexmap::IndexMap; +use log::error; +use mime::{Mime, APPLICATION_JSON, STAR_STAR, TEXT_PLAIN}; +use openapiv3::{ + APIKeyLocation, Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, + ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema, + SchemaKind, SecurityScheme, Server, StatusCode, Type +}; +use std::panic::RefUnwindSafe; + +/** +This type is required to build routes while adding them to the generated OpenAPI Spec at the +same time. There is no need to use this type directly. See [`WithOpenapi`] on how to do this. + +[`WithOpenapi`]: trait.WithOpenapi.html +*/ +pub struct OpenapiRouter(OpenAPI); + +impl OpenapiRouter +{ + pub fn new(title : String, version : String, url : String) -> Self + { + Self(OpenAPI { + openapi: "3.0.2".to_string(), + info: openapiv3::Info { + title, version, + ..Default::default() + }, + servers: vec![Server { + url, + ..Default::default() + }], + ..Default::default() + }) + } + + /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to + /// modify the path and add it back after the modification + fn remove_path(&mut self, path : &str) -> PathItem + { + match self.0.paths.swap_remove(path) { + Some(Item(item)) => item, + _ => PathItem::default() + } + } + + fn add_path(&mut self, path : Path, item : PathItem) + { + self.0.paths.insert(path.to_string(), Item(item)); + } + + fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) + { + self.add_schema_dependencies(&mut schema.dependencies); + + match &mut self.0.components { + Some(comp) => { + comp.schemas.insert(name, Item(schema.into_schema())); + }, + None => { + let mut comp = Components::default(); + comp.schemas.insert(name, Item(schema.into_schema())); + self.0.components = Some(comp); + } + }; + } + + fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) + { + 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 + { + self.add_schema_impl(dep, dep_schema); + } + } + } + + 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) }; + self.add_schema_impl(name, schema); + reference + }, + None => { + self.add_schema_dependencies(&mut schema.dependencies); + Item(schema.into_schema()) + } + } + } +} + +#[derive(Clone)] +struct OpenapiHandler(OpenAPI); + +impl OpenapiHandler +{ + fn new(openapi : &OpenapiRouter) -> Self + { + Self(openapi.0.clone()) + } +} + +impl NewHandler for OpenapiHandler +{ + type Instance = Self; + + fn new_handler(&self) -> gotham::error::Result + { + Ok(self.clone()) + } +} + +#[cfg(feature = "auth")] +const SECURITY_NAME : &'static str = "authToken"; + +#[cfg(feature = "auth")] +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, + name: name.to_string() + }, + AuthSource::Header(name) => SecurityScheme::APIKey { + location: APIKeyLocation::Header, + name: name.to_string() + }, + AuthSource::AuthorizationHeader => SecurityScheme::HTTP { + scheme: "bearer".to_owned(), + bearer_format: Some("JWT".to_owned()) + } + }; + + let mut security_schemes : IndexMap> = Default::default(); + security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); + + security_schemes +} + +#[cfg(not(feature = "auth"))] +fn get_security(state : &mut State) -> (Vec, IndexMap>) +{ + Default::default() +} + +impl Handler for OpenapiHandler +{ + fn handle(self, mut state : State) -> Box + { + let mut openapi = self.0; + let security_schemes = get_security(&mut state); + let mut components = openapi.components.unwrap_or_default(); + components.security_schemes = security_schemes; + openapi.components = Some(components); + + match serde_json::to_string(&openapi) { + Ok(body) => { + let res = create_response(&state, hyper::StatusCode::OK, APPLICATION_JSON, body); + Box::new(ok((state, res))) + }, + Err(e) => { + error!("Unable to handle OpenAPI request due to error: {}", e); + let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + Box::new(ok((state, res))) + } + } + } +} + +/// This trait adds the `get_openapi` method to an OpenAPI-aware router. +pub trait GetOpenapi +{ + fn get_openapi(&mut self, path : &str); +} + +fn schema_to_content(types : Vec, 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() + }); + } + content +} + +#[derive(Default)] +struct OperationParams<'a> +{ + path_params : Vec<&'a str>, + query_params : Option +} + +impl<'a> OperationParams<'a> +{ + fn new(path_params : Vec<&'a str>, query_params : Option) -> Self + { + Self { path_params, query_params } + } + + fn from_path_params(path_params : Vec<&'a str>) -> Self + { + Self::new(path_params, None) + } + + fn from_query_params(query_params : OpenapiSchema) -> Self + { + Self::new(Vec::new(), Some(query_params)) + } + + fn add_path_params(&self, params : &mut Vec>) + { + for param in &self.path_params + { + params.push(Item(Parameter::Path { + parameter_data: ParameterData { + name: param.to_string(), + description: None, + required: true, + deprecated: None, + format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())), + example: None, + examples: IndexMap::new() + }, + style: Default::default(), + })); + } + } + + fn add_query_params(self, params : &mut Vec>) + { + let query_params = match self.query_params { + Some(qp) => qp.schema, + None => return + }; + let query_params = match query_params { + SchemaKind::Type(Type::Object(ty)) => ty, + _ => panic!("Query Parameters needs to be a plain struct") + }; + for (name, schema) in query_params.properties + { + let required = query_params.required.contains(&name); + params.push(Item(Parameter::Query { + parameter_data: ParameterData { + name, + description: None, + required, + deprecated: None, + format: ParameterSchemaOrContent::Schema(schema.unbox()), + example: None, + examples: IndexMap::new() + }, + allow_reserved: false, + style: Default::default(), + allow_empty_value: None + })) + } + } + + fn into_params(self) -> Vec> + { + let mut params : Vec> = Vec::new(); + self.add_path_params(&mut params); + self.add_query_params(&mut params); + params + } +} + +fn new_operation( + operation_id : Option, + default_status : hyper::StatusCode, + accepted_types : Option>, + schema : ReferenceOr, + params : OperationParams, + body_schema : Option>, + supported_types : Option>, + requires_auth : bool +) -> Operation +{ + let content = schema_to_content(accepted_types.unwrap_or_else(|| vec![STAR_STAR]), 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(), + headers: IndexMap::new(), + content, + links: IndexMap::new() + })); + + let request_body = body_schema.map(|schema| Item(OARequestBody { + description: None, + content: schema_to_content(supported_types.unwrap_or_else(|| vec![STAR_STAR]), schema), + required: true + })); + + let mut security = Vec::new(); + if requires_auth + { + let mut sec = IndexMap::new(); + sec.insert(SECURITY_NAME.to_owned(), Vec::new()); + security.push(sec); + } + + Operation { + tags: Vec::new(), + operation_id, + parameters: params.into_params(), + request_body, + responses: Responses { + default: None, + responses + }, + deprecated: false, + security, + ..Default::default() + } +} + +macro_rules! implOpenapiRouter { + ($implType:ident) => { + + impl<'a, C, P> GetOpenapi for (&mut $implType<'a, C, P>, &mut OpenapiRouter) + where + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + fn get_openapi(&mut self, path : &str) + { + self.0.get(path).to_new_handler(OpenapiHandler::new(&self.1)); + } + } + + impl<'a, C, P> DrawResources for (&mut $implType<'a, C, P>, &mut OpenapiRouter) + where + C : PipelineHandleChain

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

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + fn read_all(&mut self) + { + let schema = (self.0).1.add_schema::(); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).read_all::() + } + + fn read(&mut self) + { + let schema = (self.0).1.add_schema::(); + + let path = format!("/{}/{{id}}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).read::() + } + + fn search(&mut self) + { + let schema = (self.0).1.add_schema::(); + + let path = format!("/{}/search", &self.1); + let mut item = (self.0).1.remove_path(&self.1); + item.get = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_query_params(Handler::Query::schema()), None, None, Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).search::() + } + + fn create(&mut self) + { + let schema = (self.0).1.add_schema::(); + let body_schema = (self.0).1.add_schema::(); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.post = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).create::() + } + + fn update_all(&mut self) + { + let schema = (self.0).1.add_schema::(); + let body_schema = (self.0).1.add_schema::(); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).update_all::() + } + + fn update(&mut self) + { + let schema = (self.0).1.add_schema::(); + let body_schema = (self.0).1.add_schema::(); + + let path = format!("/{}/{{id}}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.put = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Handler::Body::supported_types(), Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).update::() + } + + fn delete_all(&mut self) + { + let schema = (self.0).1.add_schema::(); + + let path = format!("/{}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::default(), None, None, Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).delete_all::() + } + + fn delete(&mut self) + { + let schema = (self.0).1.add_schema::(); + + let path = format!("/{}/{{id}}", &self.1); + let mut item = (self.0).1.remove_path(&path); + item.delete = Some(new_operation(Handler::operation_id(), Handler::Res::default_status(), Handler::Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Handler::wants_auth())); + (self.0).1.add_path(path, item); + + (&mut *(self.0).0, self.1).delete::() + } + } + + } +} + +implOpenapiRouter!(RouterBuilder); +implOpenapiRouter!(ScopeBuilder); + + +#[cfg(test)] +mod test +{ + use crate::ResourceResult; + use super::*; + + #[derive(OpenapiType)] + #[allow(dead_code)] + struct QueryParams + { + id : isize + } + + #[test] + fn params_empty() + { + let op_params = OperationParams::default(); + let params = op_params.into_params(); + assert!(params.is_empty()); + } + + #[test] + fn params_from_path_params() + { + let name = "id"; + let op_params = OperationParams::from_path_params(vec![name]); + let params = op_params.into_params(); + let json = serde_json::to_string(¶ms).unwrap(); + assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}}]"#, name)); + } + + #[test] + fn params_from_query_params() + { + let op_params = OperationParams::from_query_params(QueryParams::schema()); + let params = op_params.into_params(); + let json = serde_json::to_string(¶ms).unwrap(); + assert_eq!(json, r#"[{"in":"query","name":"id","required":true,"schema":{"type":"integer"},"style":"form"}]"#); + } + + #[test] + fn params_both() + { + let name = "id"; + let op_params = OperationParams::new(vec![name], Some(QueryParams::schema())); + let params = op_params.into_params(); + let json = serde_json::to_string(¶ms).unwrap(); + assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name)); + } + + #[test] + fn no_content_schema_to_content() + { + let types = NoContent::accepted_types(); + let schema = ::schema(); + let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema())); + assert!(content.is_empty()); + } + + #[test] + fn raw_schema_to_content() + { + let types = Raw::<&str>::accepted_types(); + let schema = as OpenapiType>::schema(); + let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema())); + assert_eq!(content.len(), 1); + let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); + assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); + } +} diff --git a/gotham_restful/src/openapi/types.rs b/gotham_restful/src/openapi/types.rs new file mode 100644 index 0000000..5cbacce --- /dev/null +++ b/gotham_restful/src/openapi/types.rs @@ -0,0 +1,384 @@ +#[cfg(feature = "chrono")] +use chrono::{ + Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc +}; +use indexmap::IndexMap; +use openapiv3::{ + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr::Item, + ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty +}; +#[cfg(feature = "uuid")] +use uuid::Uuid; +use std::collections::{BTreeSet, HashSet}; + +/** +This struct needs to be available for every type that can be part of an OpenAPI Spec. It is +already implemented for primitive types, String, Vec, Option and the like. To have it available +for your type, simply derive from [`OpenapiType`]. + +[`OpenapiType`]: trait.OpenapiType.html +*/ +#[derive(Debug, Clone, PartialEq)] +pub struct OpenapiSchema +{ + /// The name of this schema. If it is None, the schema will be inlined. + pub name : Option, + /// 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 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!(Value => r#"{"nullable":true}"#); +} diff --git a/gotham_restful/src/resource.rs b/gotham_restful/src/resource.rs new file mode 100644 index 0000000..fd1c7e6 --- /dev/null +++ b/gotham_restful/src/resource.rs @@ -0,0 +1,97 @@ +use crate::{DrawResourceRoutes, RequestBody, ResourceResult, ResourceType}; +use gotham::{ + extractor::QueryStringExtractor, + state::State +}; +use hyper::Body; +use serde::de::DeserializeOwned; +use std::panic::RefUnwindSafe; + +/// This trait must be implemented by every RESTful Resource. It will +/// allow you to register the different methods for this Resource. +pub trait Resource +{ + /// The name of this resource. Must be unique. + fn name() -> String; + + /// Setup all routes of this resource. Take a look at the rest_resource! + /// macro if you don't feel like caring yourself. + fn setup(route : D); +} + +pub trait ResourceMethod +{ + type Res : ResourceResult; + + #[cfg(feature = "openapi")] + fn operation_id() -> Option + { + None + } + + fn wants_auth() -> bool + { + false + } +} + +/// Handle a GET request on the Resource root. +pub trait ResourceReadAll : ResourceMethod +{ + fn read_all(state : &mut State) -> Self::Res; +} + +/// Handle a GET request on the Resource with an id. +pub trait ResourceRead : ResourceMethod +{ + type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; + + fn read(state : &mut State, id : Self::ID) -> Self::Res; +} + +/// Handle a GET request on the Resource with additional search parameters. +pub trait ResourceSearch : ResourceMethod +{ + type Query : ResourceType + QueryStringExtractor + Sync; + + fn search(state : &mut State, query : Self::Query) -> Self::Res; +} + +/// Handle a POST request on the Resource root. +pub trait ResourceCreate : ResourceMethod +{ + type Body : RequestBody; + + fn create(state : &mut State, body : Self::Body) -> Self::Res; +} + +/// Handle a PUT request on the Resource root. +pub trait ResourceUpdateAll : ResourceMethod +{ + type Body : RequestBody; + + fn update_all(state : &mut State, body : Self::Body) -> Self::Res; +} + +/// Handle a PUT request on the Resource with an id. +pub trait ResourceUpdate : ResourceMethod +{ + type Body : RequestBody; + type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; + + fn update(state : &mut State, id : Self::ID, body : Self::Body) -> Self::Res; +} + +/// Handle a DELETE request on the Resource root. +pub trait ResourceDeleteAll : ResourceMethod +{ + fn delete_all(state : &mut State) -> Self::Res; +} + +/// Handle a DELETE request on the Resource with an id. +pub trait ResourceDelete : ResourceMethod +{ + type ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static; + + fn delete(state : &mut State, id : Self::ID) -> Self::Res; +} diff --git a/gotham_restful/src/result.rs b/gotham_restful/src/result.rs new file mode 100644 index 0000000..55f4c3c --- /dev/null +++ b/gotham_restful/src/result.rs @@ -0,0 +1,588 @@ +use crate::{ResponseBody, StatusCode}; +#[cfg(feature = "openapi")] +use crate::{OpenapiSchema, OpenapiType}; +use hyper::Body; +#[cfg(feature = "errorlog")] +use log::error; +use mime::{Mime, APPLICATION_JSON, STAR_STAR}; +#[cfg(feature = "openapi")] +use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; +use serde::Serialize; +use serde_json::error::Error as SerdeJsonError; +use std::{ + error::Error, + fmt::Debug +}; + +/// A response, used to create the final gotham response from. +pub struct Response +{ + pub status : StatusCode, + pub body : Body, + pub mime : Option +} + +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)] + fn full_body(self) -> Vec + { + use futures::{future::Future, stream::Stream}; + + let bytes : &[u8] = &self.body.concat2().wait().unwrap().into_bytes(); + bytes.to_vec() + } +} + +/// A trait provided to convert a resource's result to json. +pub trait ResourceResult +{ + /// Turn this into a response that can be returned to the browser. This api will likely + /// change in the future. + fn into_response(self) -> Result; + + /// Return a list of supported mime types. + fn accepted_types() -> Option> + { + None + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema; + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode + { + 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 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) {} + +impl ResourceResult for Result +{ + fn into_response(self) -> Result + { + Ok(match self { + Ok(r) => Response::json(StatusCode::OK, serde_json::to_string(&r)?), + Err(e) => { + errorlog(&e); + let err : ResourceError = e.into(); + Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?) + } + }) + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + R::schema() + } +} + +/** +This can be returned from a resource when there is no cause of an error. For example: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::{Deserialize, Serialize}; +# +# #[derive(Resource)] +# struct MyResource; +# +#[derive(Deserialize, Serialize)] +# #[derive(OpenapiType)] +struct MyResponse { + message: String +} + +#[rest_read_all(MyResource)] +fn read_all(_state: &mut State) -> Success { + let res = MyResponse { message: "I'm always happy".to_string() }; + res.into() +} +# } +``` +*/ +pub struct Success(T); + +impl From for Success +{ + fn from(t : T) -> Self + { + Self(t) + } +} + +impl Clone for Success +{ + fn clone(&self) -> Self + { + Self(self.0.clone()) + } +} + +impl Debug for Success +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Success({:?})", self.0) + } +} + +impl ResourceResult for Success +{ + fn into_response(self) -> Result + { + Ok(Response::json(StatusCode::OK, serde_json::to_string(&self.0)?)) + } + + fn accepted_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + T::schema() + } +} + +/** +This return type can be used to map another `ResourceResult` that can only be returned if the +client is authenticated. Otherwise, an empty _403 Forbidden_ response will be issued. Use can +look something like this (assuming the `auth` feature is enabled): + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# use serde::Deserialize; +# +# #[derive(Resource)] +# struct MyResource; +# +# #[derive(Clone, Deserialize)] +# struct MyAuthData { exp : u64 } +# +#[rest_read_all(MyResource)] +fn read_all(auth : AuthStatus) -> AuthResult { + let auth_data = match auth { + AuthStatus::Authenticated(data) => data, + _ => return AuthErr + }; + // do something + NoContent::default().into() +} +# } +``` +*/ +pub enum AuthResult +{ + Ok(T), + AuthErr +} + +impl AuthResult +{ + pub fn is_ok(&self) -> bool + { + match self { + Self::Ok(_) => true, + _ => false + } + } + + pub fn unwrap(self) -> T + { + match self { + Self::Ok(data) => data, + _ => panic!() + } + } +} + +impl From for AuthResult +{ + fn from(t : T) -> Self + { + Self::Ok(t) + } +} + +impl Clone for AuthResult +{ + fn clone(&self) -> Self + { + match self { + Self::Ok(t) => Self::Ok(t.clone()), + Self::AuthErr => Self::AuthErr + } + } +} + +impl Debug for AuthResult +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ok(t) => write!(f, "Ok({:?})", t), + Self::AuthErr => write!(f, "AuthErr") + } + } +} + +impl ResourceResult for AuthResult +{ + fn into_response(self) -> Result + { + match self + { + Self::Ok(res) => res.into_response(), + Self::AuthErr => Ok(Response::forbidden()) + } + } + + fn accepted_types() -> Option> + { + T::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + T::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode + { + T::default_status() + } +} + +/** +This is the return type of a resource that doesn't actually return something. It will result +in a _204 No Content_ answer by default. You don't need to use this type directly if using +the function attributes: + +``` +# #[macro_use] extern crate gotham_restful_derive; +# mod doc_tests_are_broken { +# use gotham::state::State; +# use gotham_restful::*; +# +# #[derive(Resource)] +# struct MyResource; +# +#[rest_read_all(MyResource)] +fn read_all(_state: &mut State) { + // do something +} +# } +``` +*/ +#[derive(Default)] +pub struct NoContent; + +impl From<()> for NoContent +{ + fn from(_ : ()) -> Self + { + Self {} + } +} + +impl ResourceResult for NoContent +{ + /// This will always be a _204 No Content_ together with an empty string. + fn into_response(self) -> Result + { + Ok(Response::no_content()) + } + + /// Returns the schema of the `()` type. + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + <()>::schema() + } + + /// This will always be a _204 No Content_ + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode + { + StatusCode::NO_CONTENT + } +} + +impl ResourceResult for Result +{ + fn into_response(self) -> Result + { + match self { + Ok(nc) => nc.into_response(), + Err(e) => { + let err : ResourceError = e.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } + } + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + ::schema() + } + + #[cfg(feature = "openapi")] + fn default_status() -> StatusCode + { + NoContent::default_status() + } +} + +pub struct Raw +{ + pub raw : T, + pub mime : Mime +} + +impl Raw +{ + pub fn new(raw : T, mime : Mime) -> Self + { + Self { raw, mime } + } +} + +impl Clone for Raw +{ + fn clone(&self) -> Self + { + Self { + raw: self.raw.clone(), + mime: self.mime.clone() + } + } +} + +impl Debug for Raw +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Raw({:?}, {:?})", self.raw, self.mime) + } +} + +impl> ResourceResult for Raw +{ + fn into_response(self) -> Result + { + Ok(Response::new(StatusCode::OK, self.raw, Some(self.mime.clone()))) + } + + fn accepted_types() -> Option> + { + Some(vec![STAR_STAR]) + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) + } +} + +impl ResourceResult for Result, E> +where + Raw : ResourceResult +{ + fn into_response(self) -> Result + { + match self { + Ok(raw) => raw.into_response(), + Err(e) => { + let err : ResourceError = e.into(); + Ok(Response::json(StatusCode::INTERNAL_SERVER_ERROR, serde_json::to_string(&err)?)) + } + } + } + + fn accepted_types() -> Option> + { + as ResourceResult>::accepted_types() + } + + #[cfg(feature = "openapi")] + fn schema() -> OpenapiSchema + { + as ResourceResult>::schema() + } +} + + +#[cfg(test)] +mod test +{ + use super::*; + use mime::TEXT_PLAIN; + use thiserror::Error; + + #[derive(Debug, Default, Deserialize, Serialize)] + #[cfg_attr(feature = "openapi", derive(OpenapiType))] + struct Msg + { + msg : String + } + + #[derive(Debug, Default, Error)] + #[error("An Error")] + struct MsgError; + + #[test] + fn resource_result_ok() + { + let ok : Result = Ok(Msg::default()); + let res = ok.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn resource_result_err() + { + let err : Result = Err(MsgError::default()); + let res = err.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), format!(r#"{{"error":true,"message":"{}"}}"#, MsgError::default()).as_bytes()); + } + + #[test] + fn success_always_successfull() + { + let success : Success = Msg::default().into(); + let res = success.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(APPLICATION_JSON)); + assert_eq!(res.full_body(), r#"{"msg":""}"#.as_bytes()); + } + + #[test] + fn no_content_has_empty_response() + { + let no_content = NoContent::default(); + let res = no_content.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body(), &[] as &[u8]); + } + + #[test] + fn no_content_result() + { + let no_content : Result = Ok(NoContent::default()); + let res = no_content.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::NO_CONTENT); + assert_eq!(res.mime, None); + assert_eq!(res.full_body(), &[] as &[u8]); + } + + #[test] + fn raw_response() + { + let msg = "Test"; + let raw = Raw::new(msg, TEXT_PLAIN); + let res = raw.into_response().expect("didn't expect error response"); + assert_eq!(res.status, StatusCode::OK); + assert_eq!(res.mime, Some(TEXT_PLAIN)); + assert_eq!(res.full_body(), msg.as_bytes()); + } +} diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs new file mode 100644 index 0000000..c5a866c --- /dev/null +++ b/gotham_restful/src/routing.rs @@ -0,0 +1,377 @@ +use crate::{ + resource::*, + result::{ResourceError, ResourceResult, Response}, + RequestBody, + StatusCode +}; +#[cfg(feature = "openapi")] +use crate::OpenapiRouter; + +use futures::{ + future::{Future, err, ok}, + stream::Stream +}; +use gotham::{ + handler::{HandlerFuture, IntoHandlerError}, + helpers::http::response::{create_empty_response, create_response}, + pipeline::chain::PipelineHandleChain, + router::{ + builder::*, + non_match::RouteNonMatch, + route::matcher::{ + content_type::ContentTypeHeaderRouteMatcher, + AcceptHeaderRouteMatcher, + RouteMatcher + } + }, + state::{FromState, State} +}; +use hyper::{ + header::CONTENT_TYPE, + Body, + HeaderMap, + Method +}; +use mime::{Mime, APPLICATION_JSON}; +use std::panic::RefUnwindSafe; + +/// Allow us to extract an id from a path. +#[derive(Deserialize, StateData, StaticResponseExtender)] +struct PathExtractor +{ + id : 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, title : String, version : String, server_url : String, block : F) + where + F : FnOnce((&mut D, &mut OpenapiRouter)); +} + +/// This trait adds the `resource` method to gotham's routing. It allows you to register +/// any RESTful `Resource` with a path. +pub trait DrawResources +{ + fn resource(&mut self, path : &str); +} + +/// This trait allows to draw routes within an resource. Use this only inside the +/// `Resource::setup` method. +pub trait DrawResourceRoutes +{ + fn read_all(&mut self); + fn read(&mut self); + fn search(&mut self); + fn create(&mut self); + fn update_all(&mut self); + fn update(&mut self); + fn delete_all(&mut self); + fn delete(&mut self); +} + +fn response_from(res : Response, state : &State) -> hyper::Response +{ + let mut r = create_empty_response(state, res.status); + if let Some(mime) = res.mime + { + r.headers_mut().insert(CONTENT_TYPE, mime.as_ref().parse().unwrap()); + } + if Method::borrow_from(state) != Method::HEAD + { + *r.body_mut() = res.body; + } + r +} + +fn to_handler_future(mut state : State, get_result : F) -> Box +where + F : FnOnce(&mut State) -> R, + R : ResourceResult +{ + let res = get_result(&mut state).into_response(); + match res { + Ok(res) => { + let r = response_from(res, &state); + Box::new(ok((state, r))) + }, + Err(e) => Box::new(err((state, e.into_handler_error()))) + } +} + +fn handle_with_body(mut state : State, get_result : F) -> Box +where + Body : RequestBody, + F : FnOnce(&mut State, Body) -> R + Send + 'static, + R : ResourceResult +{ + let f = hyper::Body::take_from(&mut state) + .concat2() + .then(|body| { + + let body = match body { + Ok(body) => body, + Err(e) => return err((state, e.into_handler_error())) + }; + + let content_type : Mime = match HeaderMap::borrow_from(&state).get(CONTENT_TYPE) { + Some(content_type) => content_type.to_str().unwrap().parse().unwrap(), + None => { + let res = create_empty_response(&state, StatusCode::UNSUPPORTED_MEDIA_TYPE); + return ok((state, res)) + } + }; + + let body = match Body::from_body(body, content_type) { + Ok(body) => body, + Err(e) => return { + let error : ResourceError = e.into(); + match serde_json::to_string(&error) { + Ok(json) => { + let res = create_response(&state, StatusCode::BAD_REQUEST, APPLICATION_JSON, json); + ok((state, res)) + }, + Err(e) => err((state, e.into_handler_error())) + } + } + }; + + let res = get_result(&mut state, body).into_response(); + match res { + Ok(res) => { + let r = response_from(res, &state); + ok((state, r)) + }, + Err(e) => err((state, e.into_handler_error())) + } + + }); + + Box::new(f) +} + +fn read_all_handler(state : State) -> Box +{ + to_handler_future(state, |state| Handler::read_all(state)) +} + +fn read_handler(state : State) -> Box +{ + let id = { + let path : &PathExtractor = PathExtractor::borrow_from(&state); + path.id.clone() + }; + to_handler_future(state, |state| Handler::read(state, id)) +} + +fn search_handler(mut state : State) -> Box +{ + let query = Handler::Query::take_from(&mut state); + to_handler_future(state, |state| Handler::search(state, query)) +} + +fn create_handler(state : State) -> Box +{ + handle_with_body::(state, |state, body| Handler::create(state, body)) +} + +fn update_all_handler(state : State) -> Box +{ + handle_with_body::(state, |state, body| Handler::update_all(state, body)) +} + +fn update_handler(state : State) -> Box +{ + let id = { + let path : &PathExtractor = PathExtractor::borrow_from(&state); + path.id.clone() + }; + handle_with_body::(state, |state, body| Handler::update(state, id, body)) +} + +fn delete_all_handler(state : State) -> Box +{ + to_handler_future(state, |state| Handler::delete_all(state)) +} + +fn delete_handler(state : State) -> Box +{ + let id = { + let path : &PathExtractor = PathExtractor::borrow_from(&state); + path.id.clone() + }; + to_handler_future(state, |state| Handler::delete(state, id)) +} + +#[derive(Clone)] +struct MaybeMatchAcceptHeader +{ + matcher : Option +} + +impl RouteMatcher for MaybeMatchAcceptHeader +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + match &self.matcher { + Some(matcher) => matcher.is_match(state), + None => Ok(()) + } + } +} + +impl From>> for MaybeMatchAcceptHeader +{ + fn from(types : Option>) -> Self + { + Self { + matcher: types.map(AcceptHeaderRouteMatcher::new) + } + } +} + +#[derive(Clone)] +struct MaybeMatchContentTypeHeader +{ + matcher : Option +} + +impl RouteMatcher for MaybeMatchContentTypeHeader +{ + fn is_match(&self, state : &State) -> Result<(), RouteNonMatch> + { + match &self.matcher { + Some(matcher) => matcher.is_match(state), + None => Ok(()) + } + } +} + +impl From>> for MaybeMatchContentTypeHeader +{ + fn from(types : Option>) -> Self + { + Self { + matcher: types.map(ContentTypeHeaderRouteMatcher::new) + } + } +} + +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 + { + fn with_openapi(&mut self, title : String, version : String, server_url : String, block : F) + where + F : FnOnce((&mut Self, &mut OpenapiRouter)) + { + let mut router = OpenapiRouter::new(title, version, server_url); + block((self, &mut router)); + } + } + + impl<'a, C, P> DrawResources for $implType<'a, C, P> + where + C : PipelineHandleChain

+ Copy + Send + Sync + 'static, + P : RefUnwindSafe + Send + Sync + 'static + { + 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 + { + 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)); + } + + 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) + { + 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)); + } + + fn update_all(&mut self) + { + let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + self.0.put(&self.1) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) + .to(|state| update_all_handler::(state)); + } + + fn update(&mut self) + { + let accept_matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + let content_matcher : MaybeMatchContentTypeHeader = Handler::Body::supported_types().into(); + self.0.put(&format!("{}/:id", self.1)) + .extend_route_matcher(accept_matcher) + .extend_route_matcher(content_matcher) + .with_path_extractor::>() + .to(|state| update_handler::(state)); + } + + fn delete_all(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0.delete(&self.1) + .extend_route_matcher(matcher) + .to(|state| delete_all_handler::(state)); + } + + fn delete(&mut self) + { + let matcher : MaybeMatchAcceptHeader = Handler::Res::accepted_types().into(); + self.0.delete(&format!("{}/:id", self.1)) + .extend_route_matcher(matcher) + .with_path_extractor::>() + .to(|state| delete_handler::(state)); + } + } + } +} + +implDrawResourceRoutes!(RouterBuilder); +implDrawResourceRoutes!(ScopeBuilder); diff --git a/gotham_restful/src/types.rs b/gotham_restful/src/types.rs new file mode 100644 index 0000000..6764595 --- /dev/null +++ b/gotham_restful/src/types.rs @@ -0,0 +1,80 @@ +#[cfg(feature = "openapi")] +use crate::OpenapiType; +use crate::result::ResourceError; + +use hyper::Chunk; +use mime::{Mime, APPLICATION_JSON}; +use serde::{de::DeserializeOwned, Serialize}; + +#[cfg(not(feature = "openapi"))] +pub trait ResourceType +{ +} + +#[cfg(not(feature = "openapi"))] +impl ResourceType for T +{ +} + +#[cfg(feature = "openapi")] +pub trait ResourceType : OpenapiType +{ +} + +#[cfg(feature = "openapi")] +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`. +pub trait ResponseBody : ResourceType + Serialize +{ +} + +impl ResponseBody for T +{ +} + + +/// This trait must be implemented by every type that can be used as a request body. It allows +/// to create the type from a hyper body chunk and it's content type. +pub trait FromBody : Sized +{ + type Err : Into; + + /// Create the request body from a raw body and the content type. + fn from_body(body : Chunk, content_type : Mime) -> Result; +} + +impl FromBody for T +{ + type Err = serde_json::Error; + + fn from_body(body : Chunk, _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`. +pub trait RequestBody : ResourceType + FromBody +{ + /// Return all types that are supported as content types. + fn supported_types() -> Option> + { + None + } +} + +impl RequestBody for T +{ + fn supported_types() -> Option> + { + Some(vec![APPLICATION_JSON]) + } +} diff --git a/derive/Cargo.toml b/gotham_restful_derive/Cargo.toml similarity index 55% rename from derive/Cargo.toml rename to gotham_restful_derive/Cargo.toml index 58c877e..c1f460e 100644 --- a/derive/Cargo.toml +++ b/gotham_restful_derive/Cargo.toml @@ -2,14 +2,13 @@ [package] name = "gotham_restful_derive" -version = "0.3.0-dev" +version = "0.0.3" 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 Gotham - Derive" +keywords = ["gotham", "rest", "restful", "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" -proc-macro2 = "1.0.13" -quote = "1.0.6" -regex = "1.4" -syn = { version = "1.0.22", features = ["full"] } +heck = "0.3.1" +proc-macro2 = "1.0.8" +quote = "1.0.2" +syn = { version = "1.0.14", features = ["extra-traits", "full"] } [features] default = [] diff --git a/gotham_restful_derive/LICENSE-Apache b/gotham_restful_derive/LICENSE-Apache new file mode 120000 index 0000000..0cd69a3 --- /dev/null +++ b/gotham_restful_derive/LICENSE-Apache @@ -0,0 +1 @@ +../LICENSE-Apache \ No newline at end of file diff --git a/gotham_restful_derive/LICENSE-EPL b/gotham_restful_derive/LICENSE-EPL new file mode 120000 index 0000000..2004d06 --- /dev/null +++ b/gotham_restful_derive/LICENSE-EPL @@ -0,0 +1 @@ +../LICENSE-EPL \ No newline at end of file diff --git a/gotham_restful_derive/LICENSE.md b/gotham_restful_derive/LICENSE.md new file mode 120000 index 0000000..7eabdb1 --- /dev/null +++ b/gotham_restful_derive/LICENSE.md @@ -0,0 +1 @@ +../LICENSE.md \ No newline at end of file diff --git a/gotham_restful_derive/src/from_body.rs b/gotham_restful_derive/src/from_body.rs new file mode 100644 index 0000000..78ac7d0 --- /dev/null +++ b/gotham_restful_derive/src/from_body.rs @@ -0,0 +1,68 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + spanned::Spanned, + Error, + Fields, + ItemStruct, + parse_macro_input +}; + +pub fn expand_from_body(tokens : TokenStream) -> TokenStream +{ + expand(tokens) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +fn expand(tokens : TokenStream) -> Result +{ + let krate = super::krate(); + let input = parse_macro_input::parse::(tokens)?; + let ident = input.ident; + let generics = input.generics; + + let (were, body) = match input.fields { + Fields::Named(named) => { + let fields = named.named; + match fields.len() { + 0 => (quote!(), quote!(Self{})), + 1 => { + let field = fields.first().unwrap(); + let field_ident = field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self { #field_ident: body.into() })) + }, + _ => return Err(Error::new(fields.into_iter().nth(1).unwrap().span(), "FromBody can only be derived for structs with at most one field")) + } + }, + Fields::Unnamed(unnamed) => { + let fields = unnamed.unnamed; + match fields.len() { + 0 => (quote!(), quote!(Self{})), + 1 => { + let field = fields.first().unwrap(); + let field_ty = &field.ty; + (quote!(where #field_ty : for<'a> From<&'a [u8]>), quote!(Self(body.into()))) + }, + _ => return Err(Error::new(fields.into_iter().nth(1).unwrap().span(), "FromBody can only be derived for structs with at most one field")) + } + }, + Fields::Unit => (quote!(), quote!(Self{})) + }; + + Ok(quote! { + impl #generics #krate::FromBody for #ident #generics + #were + { + type Err = String; + + fn from_body(body : #krate::Chunk, _content_type : #krate::Mime) -> Result + { + let body : &[u8] = &body; + Ok(#body) + } + } + }) +} diff --git a/gotham_restful_derive/src/lib.rs b/gotham_restful_derive/src/lib.rs new file mode 100644 index 0000000..71017d9 --- /dev/null +++ b/gotham_restful_derive/src/lib.rs @@ -0,0 +1,103 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; + +mod util; + +mod from_body; +use from_body::expand_from_body; +mod method; +use method::{expand_method, Method}; +mod request_body; +use request_body::expand_request_body; +mod resource; +use resource::expand_resource; +#[cfg(feature = "openapi")] +mod openapi_type; + +#[inline] +fn print_tokens(tokens : TokenStream) -> TokenStream +{ + //eprintln!("{}", tokens); + tokens +} + +fn krate() -> TokenStream2 +{ + quote!(::gotham_restful) +} + +#[proc_macro_derive(FromBody)] +pub fn derive_from_body(tokens : TokenStream) -> TokenStream +{ + print_tokens(expand_from_body(tokens)) +} + +#[cfg(feature = "openapi")] +#[proc_macro_derive(OpenapiType, attributes(openapi))] +pub fn derive_openapi_type(tokens : TokenStream) -> TokenStream +{ + print_tokens(openapi_type::expand(tokens)) +} + +#[proc_macro_derive(RequestBody, attributes(supported_types))] +pub fn derive_request_body(tokens : TokenStream) -> TokenStream +{ + print_tokens(expand_request_body(tokens)) +} + +#[proc_macro_derive(Resource, attributes(rest_resource))] +pub fn derive_resource(tokens : TokenStream) -> TokenStream +{ + print_tokens(expand_resource(tokens)) +} + +#[proc_macro_attribute] +pub fn rest_read_all(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::ReadAll, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_read(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::Read, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_search(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::Search, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_create(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::Create, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_update_all(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::UpdateAll, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_update(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::Update, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_delete_all(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::DeleteAll, attr, item)) +} + +#[proc_macro_attribute] +pub fn rest_delete(attr : TokenStream, item : TokenStream) -> TokenStream +{ + print_tokens(expand_method(Method::Delete, attr, item)) +} diff --git a/gotham_restful_derive/src/method.rs b/gotham_restful_derive/src/method.rs new file mode 100644 index 0000000..852755b --- /dev/null +++ b/gotham_restful_derive/src/method.rs @@ -0,0 +1,459 @@ +use crate::util::CollectToResult; +use heck::{CamelCase, SnakeCase}; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{ + spanned::Spanned, + Attribute, + AttributeArgs, + Error, + FnArg, + ItemFn, + Lit, + LitBool, + Meta, + NestedMeta, + PatType, + ReturnType, + Type, + parse_macro_input +}; +use std::str::FromStr; + +pub enum Method +{ + ReadAll, + Read, + Search, + Create, + UpdateAll, + Update, + DeleteAll, + Delete +} + +impl FromStr for Method +{ + type Err = String; + fn from_str(str : &str) -> Result + { + match str { + "ReadAll" | "read_all" => Ok(Self::ReadAll), + "Read" | "read" => Ok(Self::Read), + "Search" | "search" => Ok(Self::Search), + "Create" | "create" => Ok(Self::Create), + "UpdateAll" | "update_all" => Ok(Self::UpdateAll), + "Update" | "update" => Ok(Self::Update), + "DeleteAll" | "delete_all" => Ok(Self::DeleteAll), + "Delete" | "delete" => Ok(Self::Delete), + _ => Err(format!("Unknown method: `{}'", str)) + } + } +} + +impl Method +{ + pub fn type_names(&self) -> Vec<&'static str> + { + use Method::*; + + match self { + ReadAll => vec![], + Read => vec!["ID"], + Search => vec!["Query"], + Create => vec!["Body"], + UpdateAll => vec!["Body"], + Update => vec!["ID", "Body"], + DeleteAll => vec![], + Delete => vec!["ID"] + } + } + + pub fn trait_ident(&self) -> Ident + { + use Method::*; + + let name = match self { + ReadAll => "ReadAll", + Read => "Read", + Search => "Search", + Create => "Create", + UpdateAll => "UpdateAll", + Update => "Update", + DeleteAll => "DeleteAll", + Delete => "Delete" + }; + format_ident!("Resource{}", name) + } + + pub fn fn_ident(&self) -> Ident + { + use Method::*; + + let name = match self { + ReadAll => "read_all", + Read => "read", + Search => "search", + Create => "create", + UpdateAll => "update_all", + Update => "update", + DeleteAll => "delete_all", + Delete => "delete" + }; + format_ident!("{}", name) + } + + pub fn mod_ident(&self, resource : &str) -> Ident + { + format_ident!("_gotham_restful_resource_{}_method_{}", resource.to_snake_case(), self.fn_ident()) + } + + pub fn handler_struct_ident(&self, resource : &str) -> Ident + { + format_ident!("{}{}Handler", resource.to_camel_case(), self.trait_ident()) + } + + pub fn setup_ident(&self, resource : &str) -> Ident + { + format_ident!("{}_{}_setup_impl", resource.to_snake_case(), self.fn_ident()) + } +} + +enum MethodArgumentType +{ + StateRef, + StateMutRef, + MethodArg(Type), + DatabaseConnection(Type), + AuthStatus(Type), + AuthStatusRef(Type) +} + +impl MethodArgumentType +{ + fn is_method_arg(&self) -> bool + { + match self { + Self::MethodArg(_) => true, + _ => false, + } + } + + fn is_database_conn(&self) -> bool + { + match self { + Self::DatabaseConnection(_) => true, + _ => false + } + } + + fn is_auth_status(&self) -> bool + { + match self { + Self::AuthStatus(_) | Self::AuthStatusRef(_) => true, + _ => false + } + } + + fn quote_ty(&self) -> Option + { + match self { + Self::MethodArg(ty) => Some(quote!(#ty)), + Self::DatabaseConnection(ty) => Some(quote!(#ty)), + Self::AuthStatus(ty) => Some(quote!(#ty)), + Self::AuthStatusRef(ty) => Some(quote!(#ty)), + _ => None + } + } +} + +struct MethodArgument +{ + ident : Ident, + ident_span : Span, + ty : MethodArgumentType +} + +impl Spanned for MethodArgument +{ + fn span(&self) -> Span + { + self.ident_span + } +} + +fn interpret_arg_ty(index : usize, attrs : &[Attribute], name : &str, ty : Type) -> Result +{ + let attr = attrs.into_iter() + .filter(|arg| arg.path.segments.iter().filter(|path| &path.ident.to_string() == "rest_arg").nth(0).is_some()) + .nth(0) + .map(|arg| arg.tokens.to_string()); + + if cfg!(feature = "auth") && (attr.as_deref() == Some("auth") || (attr.is_none() && name == "auth")) + { + return Ok(match ty { + Type::Reference(ty) => MethodArgumentType::AuthStatusRef(*ty.elem), + ty => MethodArgumentType::AuthStatus(ty) + }); + } + + if cfg!(feature = "database") && (attr.as_deref() == Some("connection") || attr.as_deref() == Some("conn") || (attr.is_none() && name == "conn")) + { + return Ok(MethodArgumentType::DatabaseConnection(match ty { + Type::Reference(ty) => *ty.elem, + ty => ty + })); + } + + if index == 0 + { + return match ty { + Type::Reference(ty) => Ok(if ty.mutability.is_none() { MethodArgumentType::StateRef } else { MethodArgumentType::StateMutRef }), + _ => Err(Error::new(ty.span(), "The first argument, unless some feature is used, has to be a (mutable) reference to gotham::state::State")) + }; + } + + Ok(MethodArgumentType::MethodArg(ty)) +} + +fn interpret_arg(index : usize, arg : &PatType) -> Result +{ + let pat = &arg.pat; + let ident = format_ident!("arg{}", index); + let orig_name = quote!(#pat); + let ty = interpret_arg_ty(index, &arg.attrs, &orig_name.to_string(), *arg.ty.clone())?; + + Ok(MethodArgument { ident, ident_span: arg.pat.span(), ty }) +} + +#[cfg(feature = "openapi")] +fn expand_operation_id(attrs : &AttributeArgs) -> TokenStream2 +{ + let mut operation_id : Option<&Lit> = None; + for meta in attrs + { + match meta { + NestedMeta::Meta(Meta::NameValue(kv)) => { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("operation_id".to_owned()) + { + operation_id = Some(&kv.lit) + } + }, + _ => {} + } + } + + match operation_id { + Some(operation_id) => quote! { + fn operation_id() -> Option + { + Some(#operation_id.to_string()) + } + }, + None => quote!() + } +} + +#[cfg(not(feature = "openapi"))] +fn expand_operation_id(_ : &AttributeArgs) -> TokenStream2 +{ + quote!() +} + +fn expand_wants_auth(attrs : &AttributeArgs, default : bool) -> TokenStream2 +{ + let default_lit = Lit::Bool(LitBool { value: default, span: Span::call_site() }); + let mut wants_auth = &default_lit; + for meta in attrs + { + match meta { + NestedMeta::Meta(Meta::NameValue(kv)) => { + if kv.path.segments.last().map(|p| p.ident.to_string()) == Some("wants_auth".to_owned()) + { + wants_auth = &kv.lit + } + }, + _ => {} + } + } + + quote! { + fn wants_auth() -> bool + { + #wants_auth + } + } +} + +fn expand(method : Method, attrs : TokenStream, item : TokenStream) -> Result +{ + let krate = super::krate(); + + // parse attributes + let mut method_attrs = parse_macro_input::parse::(attrs)?; + let resource_path = match method_attrs.remove(0) { + NestedMeta::Meta(Meta::Path(path)) => path, + p => return Err(Error::new(p.span(), "Expected name of the Resource struct this method belongs to")) + }; + let resource_name = resource_path.segments.last().map(|s| s.ident.to_string()) + .ok_or_else(|| Error::new(resource_path.span(), "Resource name must not be empty"))?; + + let fun = parse_macro_input::parse::(item)?; + let fun_ident = &fun.sig.ident; + let fun_vis = &fun.vis; + + let trait_ident = method.trait_ident(); + let method_ident = method.fn_ident(); + let mod_ident = method.mod_ident(&resource_name); + let handler_ident = method.handler_struct_ident(&resource_name); + let setup_ident = method.setup_ident(&resource_name); + + let (ret, is_no_content) = match &fun.sig.output { + ReturnType::Default => (quote!(#krate::NoContent), true), + ReturnType::Type(_, ty) => (quote!(#ty), false) + }; + + // some default idents we'll need + let state_ident = format_ident!("state"); + let repo_ident = format_ident!("repo"); + let conn_ident = format_ident!("conn"); + let auth_ident = format_ident!("auth"); + + // extract arguments into pattern, ident and type + let args = fun.sig.inputs.iter() + .enumerate() + .map(|(i, arg)| match arg { + FnArg::Typed(arg) => interpret_arg(i, arg), + FnArg::Receiver(_) => Err(Error::new(arg.span(), "Didn't expect self parameter")) + }) + .collect_to_result()?; + + // extract the generic parameters to use + let ty_names = method.type_names(); + let ty_len = ty_names.len(); + let generics_args : Vec<&MethodArgument> = args.iter() + .filter(|arg| (*arg).ty.is_method_arg()) + .collect(); + if generics_args.len() > ty_len + { + return Err(Error::new(generics_args[ty_len].span(), "Too many arguments")); + } + else if generics_args.len() < ty_len + { + return Err(Error::new(fun_ident.span(), "Too few arguments")); + } + let generics : Vec = 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!(#state_ident : &mut #krate::export::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!(#state_ident), + (MethodArgumentType::MethodArg(_), ident) => quote!(#ident), + (MethodArgumentType::DatabaseConnection(_), _) => quote!(&#conn_ident), + (MethodArgumentType::AuthStatus(_), _) => quote!(#auth_ident), + (MethodArgumentType::AuthStatusRef(_), _) => quote!(&#auth_ident) + }).collect(); + + // prepare the method block + let mut block = quote!(#fun_ident(#(#args_pass),*)); + if is_no_content + { + block = quote!(#block; Default::default()) + } + if let Some(arg) = args.iter().filter(|arg| (*arg).ty.is_database_conn()).nth(0) + { + let conn_ty = arg.ty.quote_ty(); + block = quote! { + let #repo_ident = <#krate::export::Repo<#conn_ty>>::borrow_from(&#state_ident).clone(); + #repo_ident.run::<_, #ret, ()>(move |#conn_ident| { + Ok({#block}) + }).wait().unwrap() + }; + } + if let Some(arg) = args.iter().filter(|arg| (*arg).ty.is_auth_status()).nth(0) + { + let auth_ty = arg.ty.quote_ty(); + block = quote! { + let #auth_ident : #auth_ty = <#auth_ty>::borrow_from(#state_ident).clone(); + #block + }; + } + + // prepare the where clause + let mut where_clause = quote!(#resource_path : #krate::Resource,); + for arg in args.iter().filter(|arg| (*arg).ty.is_auth_status()) + { + let auth_ty = arg.ty.quote_ty(); + where_clause = quote!(#where_clause #auth_ty : Clone,); + } + + // attribute generated code + let operation_id = expand_operation_id(&method_attrs); + let wants_auth = expand_wants_auth(&method_attrs, args.iter().any(|arg| (*arg).ty.is_auth_status())); + + // put everything together + Ok(quote! { + #fun + + #fun_vis mod #mod_ident + { + use super::*; + + struct #handler_ident; + + impl #krate::ResourceMethod for #handler_ident + { + type Res = #ret; + + #operation_id + #wants_auth + } + + impl #krate::#trait_ident for #handler_ident + where #where_clause + { + #(#generics)* + + fn #method_ident(#(#args_def),*) -> #ret + { + #[allow(unused_imports)] + use #krate::export::{Future, FromState}; + + #block + } + } + + #[deny(dead_code)] + pub fn #setup_ident(route : &mut D) + { + route.#method_ident::<#handler_ident>(); + } + + } + }) +} + +pub fn expand_method(method : Method, attrs : TokenStream, item : TokenStream) -> TokenStream +{ + expand(method, attrs, item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/gotham_restful_derive/src/openapi_type.rs b/gotham_restful_derive/src/openapi_type.rs new file mode 100644 index 0000000..e95ab26 --- /dev/null +++ b/gotham_restful_derive/src/openapi_type.rs @@ -0,0 +1,310 @@ +use crate::util::CollectToResult; +use proc_macro::TokenStream; +use proc_macro2::{ + Delimiter, + TokenStream as TokenStream2, + TokenTree +}; +use quote::quote; +use std::{iter, iter::FromIterator}; +use syn::{ + spanned::Spanned, + Attribute, + AttributeArgs, + Error, + Field, + Fields, + Generics, + GenericParam, + Item, + ItemEnum, + ItemStruct, + Lit, + Meta, + NestedMeta, + Variant, + parse_macro_input +}; + +pub fn expand(tokens : TokenStream) -> TokenStream +{ + let input = parse_macro_input!(tokens as Item); + + let output = match input { + Item::Enum(item) => expand_enum(item), + Item::Struct(item) => expand_struct(item), + _ => Err(Error::new(input.span(), "derive(OpenapiType) not supported for this context")) + }; + output + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +fn expand_where(generics : &Generics) -> TokenStream2 +{ + if generics.params.is_empty() + { + quote!() + } + else + { + let krate = super::krate(); + let idents = generics.params.iter() + .map(|param| match param { + GenericParam::Type(ty) => Some(ty.ident.clone()), + _ => None + }) + .filter(|param| param.is_some()) + .map(|param| param.unwrap()); + + quote! { + where #(#idents : #krate::OpenapiType),* + } + } +} + +#[derive(Debug, Default)] +struct Attrs +{ + nullable : bool, + rename : Option +} + +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 remove_parens(input : TokenStream2) -> TokenStream2 +{ + let iter = input.into_iter().flat_map(|tt| { + if let TokenTree::Group(group) = &tt + { + if group.delimiter() == Delimiter::Parenthesis + { + return Box::new(group.stream().into_iter()) as Box>; + } + } + Box::new(iter::once(tt)) + }); + let output = TokenStream2::from_iter(iter); + output +} + +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()); + 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(), "Enum Variants with Fields not supported")); + } + + let ident = &variant.ident; + + let attrs = parse_attributes(&variant.attrs)?; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + Ok(quote! { + enumeration.push(#name.to_string()); + }) +} + +fn expand_enum(input : ItemEnum) -> Result +{ + let krate = super::krate(); + let ident = input.ident; + let generics = input.generics; + let where_clause = expand_where(&generics); + + let attrs = parse_attributes(&input.attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + let variants = input.variants.iter() + .map(expand_variant) + .collect_to_result()?; + + Ok(quote! { + impl #generics #krate::OpenapiType for #ident #generics + #where_clause + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{export::openapi::*, OpenapiSchema}; + + let mut enumeration : Vec = 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(), "Fields without ident are not supported")) + }; + let ty = &field.ty; + + let attrs = parse_attributes(&field.attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + Ok(quote! {{ + let mut schema = <#ty>::schema(); + + if schema.nullable + { + schema.nullable = false; + } + else if !#nullable + { + required.push(stringify!(#ident).to_string()); + } + + let keys : Vec = schema.dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = schema.dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + dependencies.insert(dep, dep_schema); + } + } + + match schema.name.clone() { + Some(schema_name) => { + properties.insert( + #name.to_string(), + ReferenceOr::Reference { reference: format!("#/components/schemas/{}", schema_name) } + ); + dependencies.insert(schema_name, schema); + }, + None => { + properties.insert( + #name.to_string(), + ReferenceOr::Item(Box::new(schema.into_schema())) + ); + } + } + }}) +} + +pub fn expand_struct(input : ItemStruct) -> Result +{ + let krate = super::krate(); + let ident = input.ident; + let generics = input.generics; + let where_clause = expand_where(&generics); + + let attrs = parse_attributes(&input.attrs)?; + let nullable = attrs.nullable; + let name = match attrs.rename { + Some(rename) => rename, + None => ident.to_string() + }; + + let fields : Vec = match input.fields { + Fields::Named(named_fields) => { + named_fields.named.iter() + .map(expand_field) + .collect_to_result()? + }, + Fields::Unnamed(fields) => return Err(Error::new(fields.span(), "Unnamed fields are not supported")), + Fields::Unit => Vec::new() + }; + + Ok(quote!{ + impl #generics #krate::OpenapiType for #ident #generics + #where_clause + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{export::{openapi::*, IndexMap}, OpenapiSchema}; + + let mut properties : IndexMap>> = 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/gotham_restful_derive/src/request_body.rs b/gotham_restful_derive/src/request_body.rs new file mode 100644 index 0000000..6c6f473 --- /dev/null +++ b/gotham_restful_derive/src/request_body.rs @@ -0,0 +1,100 @@ +use crate::util::CollectToResult; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use std::iter; +use syn::{ + parse::{Parse, ParseStream, Result as SynResult}, + punctuated::Punctuated, + token::Comma, + Error, + Generics, + Ident, + ItemStruct, + Path, + parenthesized, + parse_macro_input +}; + +struct MimeList(Punctuated); + +impl Parse for MimeList +{ + fn parse(input: ParseStream) -> SynResult + { + let content; + let _paren = parenthesized!(content in input); + let list : Punctuated = Punctuated::parse_separated_nonempty(&content)?; + Ok(Self(list)) + } +} + +#[cfg(not(feature = "openapi"))] +fn impl_openapi_type(_ident : &Ident, _generics : &Generics) -> TokenStream2 +{ + quote!() +} + +#[cfg(feature = "openapi")] +fn impl_openapi_type(ident : &Ident, generics : &Generics) -> TokenStream2 +{ + let krate = super::krate(); + + quote! { + impl #generics #krate::OpenapiType for #ident #generics + { + fn schema() -> #krate::OpenapiSchema + { + use #krate::{export::openapi::*, OpenapiSchema}; + + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), + ..Default::default() + }))) + } + } + } +} + +fn expand(tokens : TokenStream) -> Result +{ + let krate = super::krate(); + let input = parse_macro_input::parse::(tokens)?; + let ident = input.ident; + let generics = input.generics; + + let types = input.attrs.into_iter() + .filter(|attr| attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("supported_types".to_string())) + .flat_map(|attr| + syn::parse2::(attr.tokens) + .map(|list| Box::new(list.0.into_iter().map(|mime| Ok(mime))) as Box>>) + .unwrap_or_else(|err| Box::new(iter::once(Err(err))))) + .collect_to_result()?; + + let types = match types { + ref types if types.is_empty() => quote!(None), + types => quote!(Some(vec![#(#types),*])) + }; + + let impl_openapi_type = impl_openapi_type(&ident, &generics); + + Ok(quote! { + impl #generics #krate::RequestBody for #ident #generics + where #ident #generics : #krate::FromBody + { + fn supported_types() -> Option> + { + #types + } + } + + #impl_openapi_type + }) +} + +pub fn expand_request_body(tokens : TokenStream) -> TokenStream +{ + expand(tokens) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/gotham_restful_derive/src/resource.rs b/gotham_restful_derive/src/resource.rs new file mode 100644 index 0000000..fe2f47d --- /dev/null +++ b/gotham_restful_derive/src/resource.rs @@ -0,0 +1,75 @@ +use crate::{ + method::Method, + util::CollectToResult +}; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::Comma, + Error, + Ident, + ItemStruct, + parenthesized, + parse_macro_input +}; +use std::{iter, str::FromStr}; + +struct MethodList(Punctuated); + +impl Parse for MethodList +{ + fn parse(input: ParseStream) -> Result + { + let content; + let _paren = parenthesized!(content in input); + let list : Punctuated = Punctuated::parse_separated_nonempty(&content)?; + Ok(Self(list)) + } +} + +fn expand(tokens : TokenStream) -> Result +{ + let krate = super::krate(); + let input = parse_macro_input::parse::(tokens)?; + let ident = input.ident; + let name = ident.to_string(); + + let methods = input.attrs.into_iter().filter(|attr| + attr.path.segments.iter().last().map(|segment| segment.ident.to_string()) == Some("rest_resource".to_string()) // TODO wtf + ).map(|attr| { + syn::parse2(attr.tokens).map(|m : MethodList| m.0.into_iter()) + }).flat_map(|list| match list { + Ok(iter) => Box::new(iter.map(|method| { + let method = Method::from_str(&method.to_string()).map_err(|err| Error::new(method.span(), err))?; + let mod_ident = method.mod_ident(&name); + let ident = method.setup_ident(&name); + Ok(quote!(#mod_ident::#ident(&mut route);)) + })) as Box>>, + Err(err) => Box::new(iter::once(Err(err))) + }).collect_to_result()?; + + Ok(quote! { + impl #krate::Resource for #ident + { + fn name() -> String + { + stringify!(#ident).to_string() + } + + fn setup(mut route : D) + { + #(#methods)* + } + } + }) +} + +pub fn expand_resource(tokens : TokenStream) -> TokenStream +{ + expand(tokens) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/gotham_restful_derive/src/util.rs b/gotham_restful_derive/src/util.rs new file mode 100644 index 0000000..319830a --- /dev/null +++ b/gotham_restful_derive/src/util.rs @@ -0,0 +1,27 @@ +use syn::Error; + +pub trait CollectToResult +{ + type Item; + + fn collect_to_result(self) -> Result, Error>; +} + +impl CollectToResult for I +where + I : Iterator> +{ + type Item = Item; + + 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) } + } + }) + } +} 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/cors.rs b/src/cors.rs deleted file mode 100644 index 43ff43b..0000000 --- a/src/cors.rs +++ /dev/null @@ -1,300 +0,0 @@ -use gotham::{ - handler::HandlerFuture, - helpers::http::response::create_empty_response, - hyper::{ - header::{ - HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, - ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE, - ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY - }, - Body, Method, Response, StatusCode - }, - middleware::Middleware, - pipeline::chain::PipelineHandleChain, - router::{ - builder::{DefineSingleRoute, DrawRoutes, ExtendRouteMatcher}, - route::matcher::AccessControlRequestMethodMatcher - }, - state::{FromState, State} -}; -use std::{panic::RefUnwindSafe, pin::Pin}; - -/** -Specify the allowed origins of the request. It is up to the browser to check the validity of the -origin. This, when sent to the browser, will indicate whether or not the request's origin was -allowed to make the request. -*/ -#[derive(Clone, Debug)] -pub enum Origin { - /// Do not send any `Access-Control-Allow-Origin` headers. - None, - /// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials. - Star, - /// Set the `Access-Control-Allow-Origin` header to a single origin. - Single(String), - /// Copy the `Origin` header into the `Access-Control-Allow-Origin` header. - Copy -} - -impl Default for Origin { - fn default() -> Self { - Self::None - } -} - -impl Origin { - /// Get the header value for the `Access-Control-Allow-Origin` header. - fn header_value(&self, state: &State) -> Option { - match self { - Self::None => None, - Self::Star => Some("*".parse().unwrap()), - Self::Single(origin) => Some(origin.parse().unwrap()), - Self::Copy => { - let headers = HeaderMap::borrow_from(state); - headers.get(ORIGIN).map(Clone::clone) - } - } - } - - /// Returns true if the `Vary` header has to include `Origin`. - fn varies(&self) -> bool { - matches!(self, Self::Copy) - } -} - -/** -Specify the allowed headers of the request. It is up to the browser to check that only the allowed -headers are sent with the request. -*/ -#[derive(Clone, Debug)] -pub enum Headers { - /// Do not send any `Access-Control-Allow-Headers` headers. - None, - /// Set the `Access-Control-Allow-Headers` header to the following header list. If empty, this - /// is treated as if it was [None]. - List(Vec), - /// 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]: - -```rust,no_run -# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; -# use gotham_restful::{*, cors::Origin}; -# #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_doctest_main))] -fn main() { - let cors = CorsConfig { - origin: Origin::Star, - ..Default::default() - }; - let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - // your routing logic - })); -} -``` - -This easy approach allows you to have one global cors configuration. If you prefer to have separate -configurations for different scopes, you need to register the middleware inside your routing logic: - -```rust,no_run -# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State}; -# use gotham_restful::{*, cors::Origin}; -let pipelines = new_pipeline_set(); - -// The first cors configuration -let cors_a = CorsConfig { - origin: Origin::Star, - ..Default::default() -}; -let (pipelines, chain_a) = pipelines.add( - new_pipeline().add(cors_a).build() -); - -// The second cors configuration -let cors_b = CorsConfig { - origin: Origin::Copy, - ..Default::default() -}; -let (pipelines, chain_b) = pipelines.add( - new_pipeline().add(cors_b).build() -); - -let pipeline_set = finalize_pipeline_set(pipelines); -gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| { - // routing without any cors config - route.with_pipeline_chain((chain_a, ()), |route| { - // routing with cors config a - }); - route.with_pipeline_chain((chain_b, ()), |route| { - // routing with cors config b - }); -})); -``` -*/ -#[derive(Clone, Debug, Default, NewMiddleware, StateData)] -pub struct CorsConfig { - /// The allowed origins. - pub origin: Origin, - /// The allowed headers. - pub headers: Headers, - /// The amount of seconds that the preflight request can be cached. - pub max_age: u64, - /// Whether or not the request may be made with supplying credentials. - pub credentials: bool -} - -impl Middleware for CorsConfig { - fn call(self, mut state: State, chain: Chain) -> Pin> - where - Chain: FnOnce(State) -> Pin> - { - state.put(self); - chain(state) - } -} - -/** -Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that -the response is aligned with the `state`'s [CorsConfig]. - -If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never -have to call this method. However, if you are writing your own handler method, you might want to -call this after your request to add the required CORS headers. - -For further information on CORS, read -[https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). -*/ -pub fn handle_cors(state: &State, res: &mut Response) { - let config = CorsConfig::try_borrow_from(state); - if let Some(cfg) = config { - let headers = res.headers_mut(); - - // non-preflight requests require the Access-Control-Allow-Origin header - if let Some(header) = cfg.origin.header_value(state) { - headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header); - } - - // if the origin is copied over, we should tell the browser by specifying the Vary header - if cfg.origin.varies() { - let vary = headers.get(VARY).map(|vary| format!("{},origin", vary.to_str().unwrap())); - headers.insert(VARY, vary.as_deref().unwrap_or("origin").parse().unwrap()); - } - - // if we allow credentials, tell the browser - if cfg.credentials { - headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true")); - } - } -} - -/// Add CORS routing for your path. This is required for handling preflight requests. -/// -/// Example: -/// -/// ```rust,no_run -/// # use gotham::{hyper::{Body, Method, Response}, router::builder::*}; -/// # use gotham_restful::*; -/// build_simple_router(|router| { -/// // The handler that needs preflight handling -/// router.post("/foo").to(|state| { -/// let mut res : Response = unimplemented!(); -/// handle_cors(&state, &mut res); -/// (state, res) -/// }); -/// // Add preflight handling -/// router.cors("/foo", Method::POST); -/// }); -/// ``` -pub trait CorsRoute -where - 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); -} - -pub(crate) 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(); - headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method); - vary.push(ACCESS_CONTROL_REQUEST_METHOD); - - if let Some(cfg) = config { - // if we allow any headers, copy them over - if let Some(header) = cfg.headers.header_value(&state) { - headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, header); - } - - // if the headers are copied over, we should tell the browser by specifying the Vary header - if cfg.headers.varies() { - vary.push(ACCESS_CONTROL_REQUEST_HEADERS); - } - - // set the max age for the preflight cache - if let Some(age) = config.map(|cfg| cfg.max_age) { - headers.insert(ACCESS_CONTROL_MAX_AGE, age.into()); - } - } - - // make sure the browser knows that this request was based on the method - headers.insert(VARY, vary.join(",").parse().unwrap()); - - handle_cors(&state, &mut res); - (state, res) -} - -impl CorsRoute for D -where - D: DrawRoutes, - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static -{ - fn cors(&mut self, path: &str, method: Method) { - let matcher = AccessControlRequestMethodMatcher::new(method); - self.options(path).extend_route_matcher(matcher).to(cors_preflight_handler); - } -} 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 deleted file mode 100644 index aea56a4..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,533 +0,0 @@ -#![warn(missing_debug_implementations, rust_2018_idioms)] -#![forbid(unsafe_code)] -// can we have a lint for spaces in doc comments please? -#![cfg_attr(feature = "cargo-clippy", allow(clippy::tabs_in_doc_comments))] -// intra-doc links only fully work when OpenAPI is enabled -#![cfg_attr(feature = "openapi", deny(broken_intra_doc_links))] -#![cfg_attr(not(feature = "openapi"), allow(broken_intra_doc_links))] -/*! -This crate is an extension to the popular [gotham web framework][gotham] for Rust. It allows you to -create resources with assigned endpoints that aim to be a more convenient way of creating handlers -for requests. - -# Features - - - Automatically parse **JSON** request and produce response bodies - - Allow using **raw** request and response bodies - - Convenient **macros** to create responses that can be registered with gotham's router - - Auto-Generate an **OpenAPI** specification for your API - - Manage **CORS** headers so you don't have to - - Manage **Authentication** with JWT - - Integrate diesel connection pools for easy **database** integration - -# Safety - -This crate is just as safe as you'd expect from anything written in safe Rust - and -`#![forbid(unsafe_code)]` ensures that no unsafe was used. - -# Endpoints - -There are a set of pre-defined endpoints that should cover the majority of REST APIs. However, -it is also possible to define your own endpoints. - -## Pre-defined Endpoints - -Assuming you assign `/foobar` to your resource, the following pre-defined endpoints exist: - -| Endpoint Name | Required Arguments | HTTP Verb | HTTP Path | -| ------------- | ------------------ | --------- | -------------- | -| read_all | | GET | /foobar | -| read | id | GET | /foobar/:id | -| search | query | GET | /foobar/search | -| create | body | POST | /foobar | -| change_all | body | PUT | /foobar | -| change | id, body | PUT | /foobar/:id | -| remove_all | | DELETE | /foobar | -| remove | id | DELETE | /foobar/:id | - -Each of those endpoints has a macro that creates the neccessary boilerplate for the Resource. A -simple example looks like this: - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# use gotham::router::builder::*; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -/// Our RESTful resource. -#[derive(Resource)] -#[resource(read)] -struct FooResource; - -/// The return type of the foo read endpoint. -#[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] -struct Foo { - id: u64 -} - -/// The foo read endpoint. -#[read] -fn read(id: u64) -> Success { - Foo { id }.into() -} -# fn main() { -# gotham::start("127.0.0.1:8080", build_simple_router(|route| { -# route.resource::("foo"); -# })); -# } -``` - -## Custom Endpoints - -Defining custom endpoints is done with the `#[endpoint]` macro. The syntax is similar to that -of the pre-defined endpoints, but you need to give it more context: - -```rust,no_run -# #[macro_use] extern crate gotham_derive; -# #[macro_use] extern crate gotham_restful_derive; -# use gotham::router::builder::*; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -use gotham_restful::gotham::hyper::Method; - -#[derive(Resource)] -#[resource(custom_endpoint)] -struct CustomResource; - -/// This type is used to parse path parameters. -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] -struct CustomPath { - name: String -} - -#[endpoint(uri = "custom/:name/read", method = "Method::GET", params = false, body = false)] -fn custom_endpoint(path: CustomPath) -> Success { - 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`]. - * **body** Should be any deserializable object, or any type implementing [`RequestBody`]. - * **query** Should be any deserializable object whose variables are json-primitives. It will - however not be parsed from json, but from HTTP GET parameters like in `search?id=1`. The - type needs to implement [`QueryStringExtractor`](gotham::extractor::QueryStringExtractor). - -Additionally, all handlers may take a reference to gotham's [`State`]. Please note that for async -handlers, it needs to be a mutable reference until rustc's lifetime checks across await bounds -improve. - -# Uploads and Downloads - -By default, every request body is parsed from json, and every respone is converted to json using -[serde_json]. However, you may also use raw bodies. This is an example where the request body -is simply returned as the response again, no json parsing involved: - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# use gotham::router::builder::*; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -#[derive(Resource)] -#[resource(create)] -struct ImageResource; - -#[derive(FromBody, RequestBody)] -#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage { - content: Vec, - content_type: Mime -} - -#[create] -fn create(body : RawImage) -> Raw> { - Raw::new(body.content, body.content_type) -} -# fn main() { -# gotham::start("127.0.0.1:8080", build_simple_router(|route| { -# route.resource::("image"); -# })); -# } -``` - -# Custom HTTP Headers - -You can read request headers from the state as you would in any other gotham handler, and specify -custom response headers using [Response::header]. - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# use gotham::hyper::header::{ACCEPT, HeaderMap, VARY}; -# use gotham::{router::builder::*, state::State}; -# use gotham_restful::*; -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -async fn read_all(state: &mut State) -> NoContent { - let headers: &HeaderMap = state.borrow(); - let accept = &headers[ACCEPT]; -# drop(accept); - - let mut res = NoContent::default(); - res.header(VARY, "accept".parse().unwrap()); - res -} -# fn main() { -# gotham::start("127.0.0.1:8080", build_simple_router(|route| { -# route.resource::("foo"); -# })); -# } -``` - -# Features - -To make life easier for common use-cases, this create offers a few features that might be helpful -when you implement your web server. The complete feature list is - - [`auth`](#authentication-feature) Advanced JWT middleware - - `chrono` openapi support for chrono types - - `full` enables all features except `without-openapi` - - [`cors`](#cors-feature) CORS handling for all endpoint handlers - - [`database`](#database-feature) diesel middleware support - - `errorlog` log errors returned from endpoint handlers - - [`openapi`](#openapi-feature) router additions to generate an openapi spec - - `uuid` openapi support for uuid - - `without-openapi` (**default**) disables `openapi` support. - -## Authentication Feature - -In order to enable authentication support, enable the `auth` feature gate. This allows you to -register a middleware that can automatically check for the existence of an JWT authentication -token. Besides being supported by the endpoint macros, it supports to lookup the required JWT secret -with the JWT data, hence you can use several JWT secrets and decide on the fly which secret to use. -None of this is currently supported by gotham's own JWT middleware. - -A simple example that uses only a single secret looks like this: - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# #[cfg(feature = "auth")] -# mod auth_feature_enabled { -# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -#[derive(Resource)] -#[resource(read)] -struct SecretResource; - -#[derive(Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] -struct Secret { - id: u64, - intended_for: String -} - -#[derive(Deserialize, Clone)] -struct AuthData { - sub: String, - exp: u64 -} - -#[read] -fn read(auth: AuthStatus, 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, looks like this: - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# #[cfg(feature = "cors")] -# mod cors_feature_enabled { -# use gotham::{hyper::header::*, router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; -# use gotham_restful::{*, cors::*}; -# use serde::{Deserialize, Serialize}; -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -fn read_all() { - // your handler -} - -fn main() { - let cors = CorsConfig { - origin: Origin::Copy, - headers: Headers::List(vec![CONTENT_TYPE]), - max_age: 0, - credentials: true - }; - let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build()); - gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| { - route.resource::("foo"); - })); -} -# } -``` - -The cors feature can also be used for non-resource handlers. Take a look at [`CorsRoute`] -for an example. - -## Database Feature - -The database feature allows an easy integration of [diesel] into your handler functions. Please -note however that due to the way gotham's diesel middleware implementation, it is not possible -to run async code while holding a database connection. If you need to combine async and database, -you'll need to borrow the connection from the [`State`] yourself and return a boxed future. - -A simple non-async example looks like this: - -```rust,no_run -# #[macro_use] extern crate diesel; -# #[macro_use] extern crate gotham_restful_derive; -# #[cfg(feature = "database")] -# mod database_feature_enabled { -# use diesel::{table, PgConnection, QueryResult, RunQueryDsl}; -# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State}; -# use gotham_middleware_diesel::DieselMiddleware; -# use gotham_restful::*; -# use serde::{Deserialize, Serialize}; -# use std::env; -# table! { -# foo (id) { -# id -> Int8, -# value -> Text, -# } -# } -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[derive(Queryable, Serialize)] -# #[cfg_attr(feature = "openapi", derive(openapi_type::OpenapiType))] -struct Foo { - id: i64, - value: String -} - -#[read_all] -fn read_all(conn: &PgConnection) -> QueryResult> { - 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"); - })); -} -# } -``` - -## 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. - - - [diesel]: https://diesel.rs/ - [example]: https://gitlab.com/msrd0/gotham-restful/tree/master/example - [gotham]: https://gotham.rs/ - [serde_json]: https://github.com/serde-rs/json#serde-json---- - [`State`]: gotham::state::State -*/ - -#[cfg(all(feature = "openapi", feature = "without-openapi"))] -compile_error!("The 'openapi' and 'without-openapi' features cannot be combined"); - -#[cfg(all(not(feature = "openapi"), not(feature = "without-openapi")))] -compile_error!("Either the 'openapi' or 'without-openapi' feature needs to be enabled"); - -// weird proc macro issue -extern crate self as gotham_restful; - -#[macro_use] -extern crate gotham_derive; -#[macro_use] -extern crate gotham_restful_derive; -#[macro_use] -extern crate log; -#[macro_use] -extern crate serde; - -#[doc(no_inline)] -pub use gotham; -#[doc(no_inline)] -pub use mime::Mime; - -pub use gotham_restful_derive::*; - -/// Not public API -#[doc(hidden)] -pub mod private { - pub use crate::routing::PathExtractor as IdPlaceholder; - - pub use futures_util::future::{BoxFuture, FutureExt}; - - pub use serde_json; - - #[cfg(feature = "database")] - pub use gotham_middleware_diesel::Repo; - - #[cfg(feature = "openapi")] - pub use indexmap::IndexMap; - #[cfg(feature = "openapi")] - pub use openapi_type::{OpenapiSchema, OpenapiType}; - #[cfg(feature = "openapi")] - pub use openapiv3 as openapi; -} - -#[cfg(feature = "auth")] -mod auth; -#[cfg(feature = "auth")] -pub use auth::{AuthHandler, AuthMiddleware, AuthSource, AuthStatus, AuthValidation, StaticAuthHandler}; - -#[cfg(feature = "cors")] -pub mod cors; -#[cfg(feature = "cors")] -pub use cors::{handle_cors, CorsConfig, CorsRoute}; - -#[cfg(feature = "openapi")] -mod openapi; -#[cfg(feature = "openapi")] -pub use openapi::{builder::OpenapiInfo, router::GetOpenapi}; - -mod endpoint; -#[cfg(feature = "openapi")] -pub use endpoint::EndpointWithSchema; -pub use endpoint::{Endpoint, NoopExtractor}; - -mod response; -pub use response::{ - AuthError, AuthError::Forbidden, AuthErrorOrOther, AuthResult, AuthSuccess, IntoResponse, IntoResponseError, NoContent, - Raw, Redirect, Response, Success -}; -#[cfg(feature = "openapi")] -pub use response::{IntoResponseWithSchema, ResponseSchema}; - -mod routing; -pub use routing::{DrawResourceRoutes, DrawResources}; -#[cfg(feature = "openapi")] -pub use routing::{DrawResourceRoutesWithSchema, DrawResourcesWithSchema, WithOpenapi}; - -mod types; -pub use types::*; - -/// This trait must be implemented for every resource. It allows you to register the different -/// endpoints that can be handled by this resource to be registered with the underlying router. -/// -/// It is not recommended to implement this yourself, just use `#[derive(Resource)]`. -#[_private_openapi_trait(ResourceWithSchema)] -pub trait Resource { - /// Register all methods handled by this resource with the underlying router. - #[openapi_bound("D: crate::DrawResourceRoutesWithSchema")] - #[non_openapi_bound("D: crate::DrawResourceRoutes")] - fn setup(route: D); -} diff --git a/src/openapi/builder.rs b/src/openapi/builder.rs deleted file mode 100644 index 4fa6a0d..0000000 --- a/src/openapi/builder.rs +++ /dev/null @@ -1,157 +0,0 @@ -use indexmap::IndexMap; -use openapi_type::OpenapiSchema; -use openapiv3::{ - Components, OpenAPI, PathItem, ReferenceOr, - ReferenceOr::{Item, Reference}, - Schema, Server -}; -use std::sync::{Arc, RwLock}; - -#[derive(Clone, Debug)] -pub struct OpenapiInfo { - pub title: String, - pub version: String, - pub urls: Vec -} - -#[derive(Clone, Debug)] -pub struct OpenapiBuilder { - pub openapi: Arc> -} - -impl OpenapiBuilder { - pub fn new(info: OpenapiInfo) -> Self { - Self { - openapi: Arc::new(RwLock::new(OpenAPI { - openapi: "3.0.2".to_string(), - info: openapiv3::Info { - title: info.title, - version: info.version, - ..Default::default() - }, - servers: info - .urls - .into_iter() - .map(|url| Server { - url, - ..Default::default() - }) - .collect(), - ..Default::default() - })) - } - } - - /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to - /// modify the path and add it back after the modification - pub fn remove_path(&mut self, path: &str) -> PathItem { - let mut openapi = self.openapi.write().unwrap(); - match openapi.paths.swap_remove(path) { - Some(Item(item)) => item, - _ => PathItem::default() - } - } - - pub fn add_path(&mut self, path: Path, item: PathItem) { - let mut openapi = self.openapi.write().unwrap(); - openapi.paths.insert(path.to_string(), Item(item)); - } - - fn add_schema_impl(&mut self, name: String, mut schema: OpenapiSchema) { - self.add_schema_dependencies(&mut schema.dependencies); - - let mut openapi = self.openapi.write().unwrap(); - match &mut openapi.components { - Some(comp) => { - comp.schemas.insert(name, Item(schema.into_schema())); - }, - None => { - let mut comp = Components::default(); - comp.schemas.insert(name, Item(schema.into_schema())); - openapi.components = Some(comp); - } - }; - } - - fn add_schema_dependencies(&mut self, dependencies: &mut IndexMap) { - 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 { - self.add_schema_impl(dep, dep_schema); - } - } - } - - pub fn add_schema(&mut self, mut schema: OpenapiSchema) -> ReferenceOr { - match schema.name.clone() { - Some(name) => { - let reference = Reference { - reference: format!("#/components/schemas/{}", name) - }; - self.add_schema_impl(name, schema); - reference - }, - None => { - self.add_schema_dependencies(&mut schema.dependencies); - Item(schema.into_schema()) - } - } - } -} - -#[cfg(test)] -#[allow(dead_code)] -mod test { - use super::*; - use openapi_type::OpenapiType; - - #[derive(OpenapiType)] - struct Message { - msg: String - } - - #[derive(OpenapiType)] - struct Messages { - msgs: Vec - } - - fn info() -> OpenapiInfo { - OpenapiInfo { - title: "TEST CASE".to_owned(), - version: "1.2.3".to_owned(), - urls: vec!["http://localhost:1234".to_owned(), "https://example.org".to_owned()] - } - } - - fn openapi(builder: OpenapiBuilder) -> OpenAPI { - Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap() - } - - #[test] - fn new_builder() { - let info = info(); - let builder = OpenapiBuilder::new(info.clone()); - let openapi = openapi(builder); - - assert_eq!(info.title, openapi.info.title); - assert_eq!(info.version, openapi.info.version); - assert_eq!(info.urls.len(), openapi.servers.len()); - } - - #[test] - fn add_schema() { - let mut builder = OpenapiBuilder::new(info()); - builder.add_schema(>::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()) - ); - } -} diff --git a/src/openapi/handler.rs b/src/openapi/handler.rs deleted file mode 100644 index 9762ca6..0000000 --- a/src/openapi/handler.rs +++ /dev/null @@ -1,261 +0,0 @@ -#![cfg_attr(not(feature = "auth"), allow(unused_imports))] -use super::SECURITY_NAME; -use futures_util::{future, future::FutureExt}; -use gotham::{ - anyhow, - handler::{Handler, HandlerFuture, NewHandler}, - helpers::http::response::{create_empty_response, create_response}, - hyper::{ - header::{ - HeaderMap, HeaderValue, CACHE_CONTROL, CONTENT_SECURITY_POLICY, ETAG, IF_NONE_MATCH, REFERRER_POLICY, - X_CONTENT_TYPE_OPTIONS - }, - Body, Response, StatusCode, Uri - }, - state::State -}; -use indexmap::IndexMap; -use mime::{APPLICATION_JSON, TEXT_HTML, TEXT_PLAIN}; -use once_cell::sync::Lazy; -use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme}; -use sha2::{Digest, Sha256}; -use std::{ - pin::Pin, - sync::{Arc, RwLock} -}; - -#[cfg(feature = "auth")] -fn get_security(state: &mut State) -> IndexMap> { - use crate::AuthSource; - use gotham::state::FromState; - - let source = match AuthSource::try_borrow_from(state) { - Some(source) => source, - None => return Default::default() - }; - - let security_scheme = match source { - AuthSource::Cookie(name) => SecurityScheme::APIKey { - location: APIKeyLocation::Cookie, - name: name.to_string() - }, - AuthSource::Header(name) => SecurityScheme::APIKey { - location: APIKeyLocation::Header, - name: name.to_string() - }, - AuthSource::AuthorizationHeader => SecurityScheme::HTTP { - scheme: "bearer".to_owned(), - bearer_format: Some("JWT".to_owned()) - } - }; - - let mut security_schemes: IndexMap> = 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> { - 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); - future::ok((state, res)).boxed() - }, - _ => { - { - let headers: &HeaderMap = state.borrow(); - if headers - .get(IF_NONE_MATCH) - .map_or(false, |etag| etag.as_bytes() == SWAGGER_UI_HTML_ETAG.as_bytes()) - { - let res = create_empty_response(&state, StatusCode::NOT_MODIFIED); - return future::ok((state, res)).boxed(); - } - } - - let mut res = create_response(&state, StatusCode::OK, TEXT_HTML, SWAGGER_UI_HTML.as_bytes()); - let headers = res.headers_mut(); - headers.insert(CACHE_CONTROL, HeaderValue::from_static("public,max-age=2592000")); - headers.insert(CONTENT_SECURITY_POLICY, format!("default-src 'none'; script-src 'unsafe-inline' 'sha256-{}' 'strict-dynamic'; style-src 'unsafe-inline' https://cdnjs.cloudflare.com; connect-src 'self'; img-src data:;", SWAGGER_UI_SCRIPT_HASH.as_str()).parse().unwrap()); - headers.insert(ETAG, SWAGGER_UI_HTML_ETAG.parse().unwrap()); - headers.insert(REFERRER_POLICY, HeaderValue::from_static("strict-origin-when-cross-origin")); - headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")); - future::ok((state, res)).boxed() - } - } - } -} - -// inspired by https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html -const SWAGGER_UI_HTML: Lazy<&'static String> = Lazy::new(|| { - let template = indoc::indoc! { - r#" - - - - - - - - -

- - - - "# - }; - 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 deleted file mode 100644 index 5eefc1f..0000000 --- a/src/openapi/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -const SECURITY_NAME: &str = "authToken"; - -pub mod builder; -pub mod handler; -pub mod operation; -pub mod router; diff --git a/src/openapi/operation.rs b/src/openapi/operation.rs deleted file mode 100644 index 1823b3c..0000000 --- a/src/openapi/operation.rs +++ /dev/null @@ -1,207 +0,0 @@ -use super::SECURITY_NAME; -use crate::{response::OrAllTypes, EndpointWithSchema, IntoResponse, RequestBody, ResponseSchema}; -use indexmap::IndexMap; -use mime::Mime; -use openapi_type::OpenapiSchema; -use openapiv3::{ - MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, ReferenceOr::Item, - RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, StatusCode, Type -}; - -#[derive(Default)] -struct OperationParams { - path_params: Option, - 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); - params.push(Item(Parameter::Path { - parameter_data: ParameterData { - name, - description: None, - required, - deprecated: None, - format: ParameterSchemaOrContent::Schema(schema.unbox()), - example: None, - examples: IndexMap::new() - }, - style: Default::default() - })) - } - } - - fn add_query_params(query_params: Option, params: &mut Vec>) { - let query_params = match query_params { - Some(qp) => qp.schema, - None => return - }; - let query_params = match query_params { - SchemaKind::Type(Type::Object(ty)) => ty, - _ => panic!("Query Parameters needs to be a plain struct") - }; - for (name, schema) in query_params.properties { - let required = query_params.required.contains(&name); - params.push(Item(Parameter::Query { - parameter_data: ParameterData { - name, - description: None, - required, - deprecated: None, - format: ParameterSchemaOrContent::Schema(schema.unbox()), - example: None, - examples: IndexMap::new() - }, - allow_reserved: false, - style: Default::default(), - allow_empty_value: None - })) - } - } - - fn into_params(self) -> Vec> { - let mut params: Vec> = Vec::new(); - Self::add_path_params(self.path_params, &mut params); - Self::add_query_params(self.query_params, &mut params); - params - } -} - -pub struct OperationDescription { - operation_id: Option, - default_status: gotham::hyper::StatusCode, - accepted_types: Option>, - schema: ReferenceOr, - params: OperationParams, - body_schema: Option>, - supported_types: Option>, - requires_auth: bool -} - -impl OperationDescription { - pub fn new(schema: ReferenceOr) -> Self { - Self { - operation_id: E::operation_id(), - default_status: E::Output::default_status(), - accepted_types: E::Output::accepted_types(), - schema, - params: Default::default(), - body_schema: None, - supported_types: None, - requires_auth: E::wants_auth() - } - } - - pub fn set_path_params(&mut self, params: OpenapiSchema) { - self.params.path_params = Some(params); - } - - pub fn set_query_params(&mut self, params: OpenapiSchema) { - self.params.query_params = Some(params); - } - - pub fn set_body(&mut self, schema: ReferenceOr) { - self.body_schema = Some(schema); - self.supported_types = Body::supported_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() - }); - } - content - } - - pub fn into_operation(self) -> Operation { - // this is unfortunately neccessary to prevent rust from complaining about partially moving self - let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = ( - self.operation_id, - self.default_status, - self.accepted_types, - self.schema, - self.params, - self.body_schema, - self.supported_types, - self.requires_auth - ); - - let content = Self::schema_to_content(accepted_types.or_all_types(), schema); - - let mut responses: IndexMap> = IndexMap::new(); - responses.insert( - StatusCode::Code(default_status.as_u16()), - Item(Response { - description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(), - content, - ..Default::default() - }) - ); - - let request_body = body_schema.map(|schema| { - Item(OARequestBody { - description: None, - content: Self::schema_to_content(supported_types.or_all_types(), schema), - required: true - }) - }); - - let mut security = Vec::new(); - if requires_auth { - let mut sec = IndexMap::new(); - sec.insert(SECURITY_NAME.to_owned(), Vec::new()); - security.push(sec); - } - - Operation { - tags: Vec::new(), - operation_id, - parameters: params.into_params(), - request_body, - responses: Responses { - default: None, - responses - }, - deprecated: false, - security, - ..Default::default() - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{NoContent, Raw, ResponseSchema}; - - #[test] - fn no_content_schema_to_content() { - let types = NoContent::accepted_types(); - let schema = ::schema(); - let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); - assert!(content.is_empty()); - } - - #[test] - fn raw_schema_to_content() { - let types = Raw::<&str>::accepted_types(); - let schema = as ResponseSchema>::schema(); - let content = OperationDescription::schema_to_content(types.or_all_types(), Item(schema.into_schema())); - assert_eq!(content.len(), 1); - let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap(); - assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#); - } -} diff --git a/src/openapi/router.rs b/src/openapi/router.rs deleted file mode 100644 index e6b3187..0000000 --- a/src/openapi/router.rs +++ /dev/null @@ -1,134 +0,0 @@ -use super::{ - builder::OpenapiBuilder, - handler::{OpenapiHandler, SwaggerUiHandler}, - operation::OperationDescription -}; -use crate::{routing::*, EndpointWithSchema, ResourceWithSchema, ResponseSchema}; -use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*}; -use once_cell::sync::Lazy; -use openapi_type::OpenapiType; -use regex::{Captures, Regex}; -use std::panic::RefUnwindSafe; - -/// This trait adds the `get_openapi` and `swagger_ui` method to an OpenAPI-aware router. -pub trait GetOpenapi { - fn get_openapi(&mut self, path: &str); - fn swagger_ui(&mut self, path: &str); -} - -#[derive(Debug)] -pub struct OpenapiRouter<'a, D> { - pub(crate) router: &'a mut D, - pub(crate) scope: Option<&'a str>, - pub(crate) openapi_builder: &'a mut OpenapiBuilder -} - -macro_rules! implOpenapiRouter { - ($implType:ident) => { - impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>> - where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static - { - pub fn scope(&mut self, path: &str, callback: F) - where - F: FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>) - { - let mut openapi_builder = self.openapi_builder.clone(); - let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/")); - self.router.scope(path, |router| { - let mut router = OpenapiRouter { - router, - scope: Some(new_scope.as_ref().map(String::as_ref).unwrap_or(path)), - openapi_builder: &mut openapi_builder - }; - callback(&mut router); - }); - } - } - - impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>> - where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static - { - fn get_openapi(&mut self, path: &str) { - self.router - .get(path) - .to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone())); - } - - fn swagger_ui(&mut self, path: &str) { - self.router - .get(path) - .to_new_handler(SwaggerUiHandler::new(self.openapi_builder.openapi.clone())); - } - } - - impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>> - where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static - { - 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) - where - 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(); - let mut item = (self.0).openapi_builder.remove_path(&path); - match E::http_method() { - Method::GET => item.get = Some(op), - Method::PUT => item.put = Some(op), - Method::POST => item.post = Some(op), - Method::DELETE => item.delete = Some(op), - Method::OPTIONS => item.options = Some(op), - Method::HEAD => item.head = Some(op), - Method::PATCH => item.patch = Some(op), - Method::TRACE => item.trace = Some(op), - method => warn!("Ignoring unsupported method '{}' in OpenAPI Specification", method) - }; - (self.0).openapi_builder.add_path(path, item); - - (&mut *(self.0).router, self.1).endpoint::() - } - } - }; -} - -implOpenapiRouter!(RouterBuilder); -implOpenapiRouter!(ScopeBuilder); diff --git a/src/response/auth_result.rs b/src/response/auth_result.rs deleted file mode 100644 index d8b3300..0000000 --- a/src/response/auth_result.rs +++ /dev/null @@ -1,122 +0,0 @@ -use gotham_restful_derive::ResourceError; - -/** -This is an error type that always yields a _403 Forbidden_ response. This type is best used in -combination with [AuthSuccess] or [AuthResult]. -*/ -#[derive(Debug, Clone, Copy, ResourceError)] -pub enum AuthError { - #[status(FORBIDDEN)] - #[display("Forbidden")] - Forbidden -} - -/** -This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse) -that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ -response will be issued. - -Use can look something like this (assuming the `auth` feature is enabled): - -```rust -# #[macro_use] extern crate gotham_restful_derive; -# #[cfg(feature = "auth")] -# mod auth_feature_enabled { -# use gotham::state::State; -# use gotham_restful::*; -# use serde::Deserialize; -# -# #[derive(Resource)] -# #[resource(read_all)] -# struct MyResource; -# -# #[derive(Clone, Deserialize)] -# struct MyAuthData { exp : u64 } -# -#[read_all] -fn read_all(auth : AuthStatus) -> AuthSuccess { - let auth_data = match auth { - AuthStatus::Authenticated(data) => data, - _ => return Err(Forbidden) - }; - // do something - Ok(NoContent::default()) -} -# } -``` -*/ -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]. -*/ -#[derive(Debug, ResourceError)] -pub enum AuthErrorOrOther { - #[status(FORBIDDEN)] - #[display("Forbidden")] - Forbidden, - #[status(INTERNAL_SERVER_ERROR)] - #[display("{0}")] - Other(E) -} - -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 -{ - fn from(err: F) -> Self { - Self::Other(err.into()) - } -} - -/** -This return type can be used to wrap any type implementing [IntoResponse](crate::IntoResponse) -that can only be returned if the client is authenticated. Otherwise, an empty _403 Forbidden_ -response will be issued. - -Use can look something like this (assuming the `auth` feature is enabled): - -``` -# #[macro_use] extern crate gotham_restful_derive; -# #[cfg(feature = "auth")] -# mod auth_feature_enabled { -# use gotham::state::State; -# use gotham_restful::*; -# use serde::Deserialize; -# use std::io; -# -# #[derive(Resource)] -# #[resource(read_all)] -# struct MyResource; -# -# #[derive(Clone, Deserialize)] -# struct MyAuthData { exp : u64 } -# -#[read_all] -fn read_all(auth : AuthStatus) -> AuthResult { - let auth_data = match auth { - AuthStatus::Authenticated(data) => data, - _ => Err(Forbidden)? - }; - // do something - Ok(NoContent::default().into()) -} -# } -*/ -pub type AuthResult = Result>; 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/raw.rs b/src/response/raw.rs deleted file mode 100644 index 3722146..0000000 --- a/src/response/raw.rs +++ /dev/null @@ -1,166 +0,0 @@ -use super::{handle_error, IntoResponse, IntoResponseError}; -use crate::{FromBody, RequestBody, ResourceType, Response}; -#[cfg(feature = "openapi")] -use crate::{IntoResponseWithSchema, ResponseSchema}; -#[cfg(feature = "openapi")] -use openapi_type::{OpenapiSchema, OpenapiType}; - -use futures_core::future::Future; -use futures_util::{future, future::FutureExt}; -use gotham::hyper::{ - body::{Body, Bytes}, - StatusCode -}; -use mime::Mime; -#[cfg(feature = "openapi")] -use openapiv3::{SchemaKind, StringFormat, StringType, Type, VariantOrUnknownOrEmpty}; -use serde_json::error::Error as SerdeJsonError; -use std::{convert::Infallible, fmt::Display, pin::Pin}; - -/** -This type can be used both as a raw request body, as well as as a raw response. However, all types -of request bodies are accepted by this type. It is therefore recommended to derive your own type -from [RequestBody] and only use this when you need to return a raw response. This is a usage -example that simply returns its body: - -```rust,no_run -# #[macro_use] extern crate gotham_restful_derive; -# use gotham::router::builder::*; -# use gotham_restful::*; -#[derive(Resource)] -#[resource(create)] -struct ImageResource; - -#[create] -fn create(body : Raw>) -> Raw> { - body -} -# fn main() { -# gotham::start("127.0.0.1:8080", build_simple_router(|route| { -# route.resource::("img"); -# })); -# } -``` -*/ -#[derive(Debug)] -pub struct Raw { - pub raw: T, - pub mime: Mime -} - -impl Raw { - pub fn new(raw: T, mime: Mime) -> Self { - Self { raw, mime } - } -} - -impl AsMut for Raw -where - T: AsMut -{ - fn as_mut(&mut self) -> &mut U { - self.raw.as_mut() - } -} - -impl AsRef for Raw -where - T: AsRef -{ - fn as_ref(&self) -> &U { - self.raw.as_ref() - } -} - -impl Clone for Raw { - fn clone(&self) -> Self { - Self { - raw: self.raw.clone(), - mime: self.mime.clone() - } - } -} - -impl From<&'a [u8]>> FromBody for Raw { - type Err = Infallible; - - fn from_body(body: Bytes, mime: Mime) -> Result { - Ok(Self::new(body.as_ref().into(), mime)) - } -} - -impl RequestBody for Raw where Raw: FromBody + ResourceType {} - -#[cfg(feature = "openapi")] -impl OpenapiType for Raw { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { - format: VariantOrUnknownOrEmpty::Item(StringFormat::Binary), - ..Default::default() - }))) - } -} - -impl> IntoResponse 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))).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> -{ - type Err = E::Err; - - 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(test)] -mod test { - use super::*; - use futures_executor::block_on; - use mime::TEXT_PLAIN; - - #[test] - fn raw_response() { - let msg = "Test"; - let raw = Raw::new(msg, TEXT_PLAIN); - let res = block_on(raw.into_response()).expect("didn't expect error response"); - assert_eq!(res.status, StatusCode::OK); - assert_eq!(res.mime, Some(TEXT_PLAIN)); - assert_eq!(res.full_body().unwrap(), msg.as_bytes()); - } -} 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/routing.rs b/src/routing.rs deleted file mode 100644 index c7cd5a6..0000000 --- a/src/routing.rs +++ /dev/null @@ -1,256 +0,0 @@ -#[cfg(feature = "openapi")] -use crate::openapi::{ - builder::{OpenapiBuilder, OpenapiInfo}, - router::OpenapiRouter -}; -use crate::{response::ResourceError, Endpoint, FromBody, IntoResponse, Resource, Response}; -#[cfg(feature = "cors")] -use gotham::router::route::matcher::AccessControlRequestMethodMatcher; -use gotham::{ - handler::HandlerError, - helpers::http::response::{create_empty_response, create_response}, - hyper::{body::to_bytes, header::CONTENT_TYPE, Body, HeaderMap, Method, StatusCode}, - pipeline::chain::PipelineHandleChain, - router::{ - builder::{DefineSingleRoute, DrawRoutes, RouterBuilder, ScopeBuilder}, - non_match::RouteNonMatch, - route::matcher::{AcceptHeaderRouteMatcher, ContentTypeHeaderRouteMatcher, RouteMatcher} - }, - state::{FromState, State} -}; -use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; -use std::{any::TypeId, panic::RefUnwindSafe}; - -/// Allow us to extract an id from a path. -#[derive(Clone, Copy, Debug, Deserialize, StateData, StaticResponseExtender)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] -pub struct PathExtractor { - pub id: ID -} - -/// This trait adds the `with_openapi` method to gotham's routing. It turns the default -/// router into one that will only allow RESTful resources, but record them and generate -/// an OpenAPI specification on request. -#[cfg(feature = "openapi")] -pub trait WithOpenapi { - fn with_openapi(&mut self, info: OpenapiInfo, block: F) - where - F: FnOnce(OpenapiRouter<'_, D>); -} - -/// This trait adds the `resource` method to gotham's routing. It allows you to register -/// any RESTful [Resource] with a path. -#[_private_openapi_trait(DrawResourcesWithSchema)] -pub trait DrawResources { - #[openapi_bound("R: crate::ResourceWithSchema")] - #[non_openapi_bound("R: crate::Resource")] - fn resource(&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); -} - -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()); - } - let mut last_name = None; - for (name, value) in res.headers { - if name.is_some() { - last_name = name; - } - // this unwrap is safe: the first item will always be Some - let name = last_name.clone().unwrap(); - headers.insert(name, value); - } - - let method = Method::borrow_from(state); - if method != Method::HEAD { - *r.body_mut() = res.body; - } - - #[cfg(feature = "cors")] - crate::cors::handle_cors(state, &mut r); - - r -} - -async fn endpoint_handler(state: &mut State) -> Result, HandlerError> -where - E: Endpoint, - ::Err: Into -{ - trace!("entering endpoint_handler"); - let placeholders = E::Placeholders::take_from(state); - // workaround for E::Placeholders and E::Param being the same type - // when fixed remove `Clone` requirement on endpoint - if TypeId::of::() == 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); - } - } - }, - false => None - }; - - let out = E::handle(state, placeholders, params, body).await; - let res = out.into_response().await.map_err(Into::into)?; - debug!("Returning response {:?}", res); - Ok(response_from(res, state)) -} - -#[derive(Clone)] -struct MaybeMatchAcceptHeader { - matcher: Option -} - -impl RouteMatcher for MaybeMatchAcceptHeader { - fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { - match &self.matcher { - Some(matcher) => matcher.is_match(state), - None => Ok(()) - } - } -} - -impl MaybeMatchAcceptHeader { - fn new(types: Option>) -> Self { - let types = match types { - Some(types) if types.is_empty() => None, - types => types - }; - Self { - matcher: types.map(AcceptHeaderRouteMatcher::new) - } - } -} - -impl From>> for MaybeMatchAcceptHeader { - fn from(types: Option>) -> Self { - Self::new(types) - } -} - -#[derive(Clone)] -struct MaybeMatchContentTypeHeader { - matcher: Option -} - -impl RouteMatcher for MaybeMatchContentTypeHeader { - fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> { - match &self.matcher { - Some(matcher) => matcher.is_match(state), - None => Ok(()) - } - } -} - -impl MaybeMatchContentTypeHeader { - fn new(types: Option>) -> 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 - { - fn with_openapi(&mut self, info: OpenapiInfo, block: F) - where - F: FnOnce(OpenapiRouter<'_, $implType<'a, C, P>>) - { - let router = OpenapiRouter { - router: self, - scope: None, - openapi_builder: &mut OpenapiBuilder::new(info) - }; - block(router); - } - } - - impl<'a, C, P> DrawResources for $implType<'a, C, P> - where - C: PipelineHandleChain

+ Copy + Send + Sync + 'static, - P: RefUnwindSafe + Send + Sync + 'static - { - fn resource(&mut self, path: &str) { - R::setup((self, path)); - } - } - - impl<'a, C, P> DrawResourceRoutes for (&mut $implType<'a, C, P>, &str) - where - 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::); - - #[cfg(feature = "cors")] - if E::http_method() != Method::GET { - assoc - .options() - .add_route_matcher(AccessControlRequestMethodMatcher::new(E::http_method())) - .to(crate::cors::cors_preflight_handler); - } - }); - } - } - }; -} - -implDrawResourceRoutes!(RouterBuilder); -implDrawResourceRoutes!(ScopeBuilder); diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 20be58d..0000000 --- a/src/types.rs +++ /dev/null @@ -1,99 +0,0 @@ -use gotham::hyper::body::Bytes; -use mime::{Mime, APPLICATION_JSON}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; -use serde::{de::DeserializeOwned, Serialize}; -use std::error::Error; - -#[cfg(not(feature = "openapi"))] -pub trait ResourceType {} - -#[cfg(not(feature = "openapi"))] -impl ResourceType for T {} - -#[cfg(feature = "openapi")] -pub trait ResourceType: OpenapiType {} - -#[cfg(feature = "openapi")] -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 {} - -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: - -```rust -# #[macro_use] extern crate gotham_restful; -# use gotham_restful::*; -#[derive(FromBody, RequestBody)] -#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage { - content: Vec, - content_type: Mime -} -``` -*/ -pub trait FromBody: Sized { - /// The error type returned by the conversion if it was unsuccessfull. When using the derive - /// macro, there is no way to trigger an error, so [std::convert::Infallible] is used here. - /// However, this might change in the future. - type Err: Error; - - /// Perform the conversion. - fn from_body(body: Bytes, content_type: Mime) -> Result; -} - -impl FromBody for T { - type Err = serde_json::Error; - - 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]. - -If you want a non-deserializable type to be used as a request body, e.g. because you'd like to -get the raw data, you can derive it for your own type. All you need is to have a type implementing -[FromBody] and optionally a list of supported media types: - -```rust -# #[macro_use] extern crate gotham_restful; -# use gotham_restful::*; -#[derive(FromBody, RequestBody)] -#[supported_types(mime::IMAGE_GIF, mime::IMAGE_JPEG, mime::IMAGE_PNG)] -struct RawImage { - content: Vec, - content_type: Mime -} -``` - - [OpenapiType]: trait.OpenapiType.html -*/ -pub trait RequestBody: ResourceType + FromBody { - /// Return all types that are supported as content types. Use `None` if all types are supported. - fn supported_types() -> Option> { - None - } -} - -impl RequestBody for T { - fn supported_types() -> Option> { - Some(vec![APPLICATION_JSON]) - } -} diff --git a/tests/async_methods.rs b/tests/async_methods.rs deleted file mode 100644 index 9a1669b..0000000 --- a/tests/async_methods.rs +++ /dev/null @@ -1,133 +0,0 @@ -#[macro_use] -extern crate gotham_derive; - -use gotham::{ - hyper::{HeaderMap, Method}, - router::builder::*, - state::State, - test::TestServer -}; -use gotham_restful::*; -use mime::{APPLICATION_JSON, TEXT_PLAIN}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; -use serde::Deserialize; -use tokio::time::{sleep, Duration}; - -mod util { - include!("util/mod.rs"); -} -use util::{test_delete_response, test_get_response, test_post_response, test_put_response}; - -#[derive(Resource)] -#[resource(read_all, read, search, create, change_all, change, remove_all, remove, state_test)] -struct FooResource; - -#[derive(Deserialize)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] -#[allow(dead_code)] -struct FooBody { - data: String -} - -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] -#[allow(dead_code)] -struct FooSearch { - query: String -} - -const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; -#[read_all] -async fn read_all() -> Raw<&'static [u8]> { - Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) -} - -const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; -#[read] -async fn read(_id: u64) -> Raw<&'static [u8]> { - Raw::new(READ_RESPONSE, TEXT_PLAIN) -} - -const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; -#[search] -async fn search(_body: FooSearch) -> Raw<&'static [u8]> { - Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) -} - -const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; -#[create] -async fn create(_body: FooBody) -> Raw<&'static [u8]> { - Raw::new(CREATE_RESPONSE, TEXT_PLAIN) -} - -const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; -#[change_all] -async fn change_all(_body: FooBody) -> Raw<&'static [u8]> { - Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) -} - -const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; -#[change] -async fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> { - Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) -} - -const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; -#[remove_all] -async fn remove_all() -> Raw<&'static [u8]> { - Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) -} - -const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; -#[remove] -async fn remove(_id: u64) -> Raw<&'static [u8]> { - Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) -} - -const STATE_TEST_RESPONSE: &[u8] = b"xxJbxOuwioqR5DfzPuVqvaqRSfpdNQGluIvHU4n1LM"; -#[endpoint(method = "Method::GET", uri = "state_test")] -async fn state_test(state: &mut State) -> Raw<&'static [u8]> { - sleep(Duration::from_nanos(1)).await; - state.borrow::(); - sleep(Duration::from_nanos(1)).await; - Raw::new(STATE_TEST_RESPONSE, TEXT_PLAIN) -} - -#[test] -fn async_methods() { - let _ = pretty_env_logger::try_init_timed(); - - let server = TestServer::new(build_simple_router(|router| { - router.resource::("foo"); - })) - .unwrap(); - - test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE); - test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE); - test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE); - test_post_response( - &server, - "http://localhost/foo", - r#"{"data":"hello world"}"#, - APPLICATION_JSON, - CREATE_RESPONSE - ); - test_put_response( - &server, - "http://localhost/foo", - r#"{"data":"hello world"}"#, - APPLICATION_JSON, - CHANGE_ALL_RESPONSE - ); - test_put_response( - &server, - "http://localhost/foo/1", - r#"{"data":"hello world"}"#, - APPLICATION_JSON, - CHANGE_RESPONSE - ); - test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE); - test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE); - test_get_response(&server, "http://localhost/foo/state_test", STATE_TEST_RESPONSE); -} diff --git a/tests/cors_handling.rs b/tests/cors_handling.rs deleted file mode 100644 index b74e7b3..0000000 --- a/tests/cors_handling.rs +++ /dev/null @@ -1,323 +0,0 @@ -#![cfg(feature = "cors")] -use gotham::{ - hyper::{body::Body, client::connect::Connect, header::*, StatusCode}, - pipeline::{new_pipeline, single::single_pipeline}, - router::builder::*, - test::{Server, TestRequest, TestServer} -}; -use gotham_restful::{ - change_all, - cors::{Headers, Origin}, - read_all, CorsConfig, DrawResources, Raw, Resource -}; -use mime::TEXT_PLAIN; - -#[derive(Resource)] -#[resource(read_all, change_all)] -struct FooResource; - -#[read_all] -fn read_all() {} - -#[change_all] -fn change_all(_body: Raw>) {} - -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 - ); - assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), vary); - assert_eq!( - headers - .get(ACCESS_CONTROL_ALLOW_CREDENTIALS) - .and_then(|value| value.to_str().ok()) - .map(|value| value == "true") - .unwrap_or(false), - credentials - ); - assert!(headers.get(ACCESS_CONTROL_MAX_AGE).is_none()); -} - -fn test_preflight(server: &TestServer, method: &str, origin: Option<&str>, vary: &str, credentials: bool, max_age: u64) { - let res = server - .client() - .options("http://example.org/foo") - .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) - .with_header(ORIGIN, "http://example.org".parse().unwrap()) - .perform() - .unwrap(); - assert_eq!(res.status(), StatusCode::NO_CONTENT); - let headers = res.headers(); - println!("{}", headers.keys().map(|name| name.as_str()).collect::>().join(",")); - assert_eq!( - headers - .get(ACCESS_CONTROL_ALLOW_METHODS) - .and_then(|value| value.to_str().ok()) - .as_deref(), - Some(method) - ); - assert_eq!( - headers - .get(ACCESS_CONTROL_ALLOW_ORIGIN) - .and_then(|value| value.to_str().ok()) - .as_deref(), - origin - ); - assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary)); - assert_eq!( - headers - .get(ACCESS_CONTROL_ALLOW_CREDENTIALS) - .and_then(|value| value.to_str().ok()) - .map(|value| value == "true") - .unwrap_or(false), - credentials - ); - assert_eq!( - headers - .get(ACCESS_CONTROL_MAX_AGE) - .and_then(|value| value.to_str().ok()) - .and_then(|value| value.parse().ok()), - Some(max_age) - ); -} - -fn test_preflight_headers( - server: &TestServer, - method: &str, - request_headers: Option<&str>, - allowed_headers: Option<&str>, - vary: &str -) { - let client = server.client(); - let mut res = client - .options("http://example.org/foo") - .with_header(ACCESS_CONTROL_REQUEST_METHOD, method.parse().unwrap()) - .with_header(ORIGIN, "http://example.org".parse().unwrap()); - if let Some(hdr) = request_headers { - res = res.with_header(ACCESS_CONTROL_REQUEST_HEADERS, hdr.parse().unwrap()); - } - let res = res.perform().unwrap(); - assert_eq!(res.status(), StatusCode::NO_CONTENT); - let headers = res.headers(); - println!("{}", headers.keys().map(|name| name.as_str()).collect::>().join(",")); - if let Some(hdr) = allowed_headers { - assert_eq!( - headers - .get(ACCESS_CONTROL_ALLOW_HEADERS) - .and_then(|value| value.to_str().ok()) - .as_deref(), - Some(hdr) - ) - } else { - assert!(!headers.contains_key(ACCESS_CONTROL_ALLOW_HEADERS)); - } - assert_eq!(headers.get(VARY).and_then(|value| value.to_str().ok()).as_deref(), Some(vary)); -} - -#[test] -fn cors_origin_none() { - let cfg = Default::default(); - let server = test_server(cfg); - - test_preflight(&server, "PUT", None, "access-control-request-method", false, 0); - - test_response(server.client().get("http://example.org/foo"), None, None, false); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - None, - None, - false - ); -} - -#[test] -fn cors_origin_star() { - let cfg = CorsConfig { - origin: Origin::Star, - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight(&server, "PUT", Some("*"), "access-control-request-method", false, 0); - - test_response(server.client().get("http://example.org/foo"), Some("*"), None, false); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - Some("*"), - None, - false - ); -} - -#[test] -fn cors_origin_single() { - let cfg = CorsConfig { - origin: Origin::Single("https://foo.com".to_owned()), - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight( - &server, - "PUT", - Some("https://foo.com"), - "access-control-request-method", - false, - 0 - ); - - test_response( - server.client().get("http://example.org/foo"), - Some("https://foo.com"), - None, - false - ); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - Some("https://foo.com"), - None, - false - ); -} - -#[test] -fn cors_origin_copy() { - let cfg = CorsConfig { - origin: Origin::Copy, - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight( - &server, - "PUT", - Some("http://example.org"), - "access-control-request-method,origin", - false, - 0 - ); - - test_response( - server.client().get("http://example.org/foo"), - Some("http://example.org"), - Some("origin"), - false - ); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - Some("http://example.org"), - Some("origin"), - false - ); -} - -#[test] -fn cors_headers_none() { - let cfg = Default::default(); - let server = test_server(cfg); - - test_preflight_headers(&server, "PUT", None, None, "access-control-request-method"); - test_preflight_headers(&server, "PUT", Some("Content-Type"), None, "access-control-request-method"); -} - -#[test] -fn cors_headers_list() { - let cfg = CorsConfig { - headers: Headers::List(vec![CONTENT_TYPE]), - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight_headers(&server, "PUT", None, Some("content-type"), "access-control-request-method"); - test_preflight_headers( - &server, - "PUT", - Some("content-type"), - Some("content-type"), - "access-control-request-method" - ); -} - -#[test] -fn cors_headers_copy() { - let cfg = CorsConfig { - headers: Headers::Copy, - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight_headers( - &server, - "PUT", - None, - None, - "access-control-request-method,access-control-request-headers" - ); - test_preflight_headers( - &server, - "PUT", - Some("content-type"), - Some("content-type"), - "access-control-request-method,access-control-request-headers" - ); -} - -#[test] -fn cors_credentials() { - let cfg = CorsConfig { - origin: Origin::None, - credentials: true, - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight(&server, "PUT", None, "access-control-request-method", true, 0); - - test_response(server.client().get("http://example.org/foo"), None, None, true); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - None, - None, - true - ); -} - -#[test] -fn cors_max_age() { - let cfg = CorsConfig { - origin: Origin::None, - max_age: 31536000, - ..Default::default() - }; - let server = test_server(cfg); - - test_preflight(&server, "PUT", None, "access-control-request-method", false, 31536000); - - test_response(server.client().get("http://example.org/foo"), None, None, false); - test_response( - server.client().put("http://example.org/foo", Body::empty(), TEXT_PLAIN), - None, - None, - false - ); -} diff --git a/tests/custom_request_body.rs b/tests/custom_request_body.rs deleted file mode 100644 index 66156b6..0000000 --- a/tests/custom_request_body.rs +++ /dev/null @@ -1,39 +0,0 @@ -use gotham::{hyper::header::CONTENT_TYPE, router::builder::*, test::TestServer}; -use gotham_restful::*; -use mime::TEXT_PLAIN; - -const RESPONSE: &[u8] = b"This is the only valid response."; - -#[derive(Resource)] -#[resource(create)] -struct FooResource; - -#[derive(FromBody, RequestBody)] -#[supported_types(TEXT_PLAIN)] -struct Foo { - content: Vec, - content_type: Mime -} - -#[create] -fn create(body: Foo) -> Raw> { - Raw::new(body.content, body.content_type) -} - -#[test] -fn custom_request_body() { - let server = TestServer::new(build_simple_router(|router| { - router.resource::("foo"); - })) - .unwrap(); - - let res = server - .client() - .post("http://localhost/foo", RESPONSE, TEXT_PLAIN) - .perform() - .unwrap(); - assert_eq!(res.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), "text/plain"); - let res = res.read_body().unwrap(); - let body: &[u8] = res.as_ref(); - assert_eq!(body, RESPONSE); -} diff --git a/tests/openapi_specification.json b/tests/openapi_specification.json deleted file mode 100644 index c3297cd..0000000 --- a/tests/openapi_specification.json +++ /dev/null @@ -1,252 +0,0 @@ -{ - "components": { - "schemas": { - "Secret": { - "properties": { - "code": { - "format": "float", - "type": "number" - } - }, - "required": [ - "code" - ], - "title": "Secret", - "type": "object" - }, - "Secrets": { - "properties": { - "secrets": { - "items": { - "$ref": "#/components/schemas/Secret" - }, - "type": "array" - } - }, - "required": [ - "secrets" - ], - "title": "Secrets", - "type": "object" - } - }, - "securitySchemes": { - "authToken": { - "bearerFormat": "JWT", - "scheme": "bearer", - "type": "http" - } - } - }, - "info": { - "title": "This is just a test", - "version": "1.2.3" - }, - "openapi": "3.0.2", - "paths": { - "/custom": { - "patch": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/custom/read/{from}/with/{id}": { - "get": { - "parameters": [ - { - "in": "path", - "name": "from", - "required": true, - "schema": { - "type": "string" - }, - "style": "simple" - }, - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "int64", - "minimum": 0, - "type": "integer" - }, - "style": "simple" - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/img/{id}": { - "get": { - "operationId": "getImage", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "int64", - "minimum": 0, - "type": "integer" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "content": { - "*/*": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "OK" - } - } - }, - "put": { - "operationId": "setImage", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "int64", - "minimum": 0, - "type": "integer" - }, - "style": "simple" - } - ], - "requestBody": { - "content": { - "image/png": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/secret/search": { - "get": { - "parameters": [ - { - "in": "query", - "name": "date", - "required": true, - "schema": { - "format": "date", - "type": "string" - }, - "style": "form" - }, - { - "in": "query", - "name": "hour", - "schema": { - "format": "int16", - "minimum": 0, - "type": "integer" - }, - "style": "form" - }, - { - "in": "query", - "name": "minute", - "schema": { - "format": "int16", - "minimum": 0, - "type": "integer" - }, - "style": "form" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Secrets" - } - } - }, - "description": "OK" - } - }, - "security": [ - { - "authToken": [] - } - ] - } - }, - "/secret/{id}": { - "get": { - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "format": "date-time", - "type": "string" - }, - "style": "simple" - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Secret" - } - } - }, - "description": "OK" - } - }, - "security": [ - { - "authToken": [] - } - ] - } - } - }, - "servers": [ - { - "url": "http://localhost:12345/api/v1" - } - ] -} \ No newline at end of file diff --git a/tests/openapi_specification.rs b/tests/openapi_specification.rs deleted file mode 100644 index a2d33d4..0000000 --- a/tests/openapi_specification.rs +++ /dev/null @@ -1,125 +0,0 @@ -#![cfg(all(feature = "auth", feature = "chrono", feature = "openapi"))] - -#[macro_use] -extern crate gotham_derive; - -use chrono::{NaiveDate, NaiveDateTime}; -use gotham::{ - hyper::Method, - pipeline::{new_pipeline, single::single_pipeline}, - router::builder::*, - test::TestServer -}; -use gotham_restful::*; -use mime::IMAGE_PNG; -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -mod util { - include!("util/mod.rs"); -} -use util::{test_get_response, test_openapi_response}; - -const IMAGE_RESPONSE : &[u8] = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUA/wA0XsCoAAAAAXRSTlN/gFy0ywAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII="; - -#[derive(Resource)] -#[resource(get_image, set_image)] -struct ImageResource; - -#[derive(FromBody, RequestBody)] -#[supported_types(IMAGE_PNG)] -struct Image(Vec); - -#[read(operation_id = "getImage")] -fn get_image(_id: u64) -> Raw<&'static [u8]> { - Raw::new(IMAGE_RESPONSE, "image/png;base64".parse().unwrap()) -} - -#[change(operation_id = "setImage")] -fn set_image(_id: u64, _image: Image) {} - -#[derive(Resource)] -#[resource(read_secret, search_secret)] -struct SecretResource; - -#[derive(Deserialize, Clone)] -struct AuthData { - sub: String, - iat: u64, - exp: u64 -} - -type AuthStatus = gotham_restful::AuthStatus; - -#[derive(OpenapiType, Serialize)] -struct Secret { - code: f32 -} - -#[derive(OpenapiType, Serialize)] -struct Secrets { - secrets: Vec -} - -#[derive(Deserialize, OpenapiType, StateData, StaticResponseExtender)] -struct SecretQuery { - date: NaiveDate, - hour: Option, - minute: Option -} - -#[read] -fn read_secret(auth: AuthStatus, _id: NaiveDateTime) -> AuthSuccess { - auth.ok()?; - Ok(Secret { code: 4.2 }) -} - -#[search] -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() { - let info = OpenapiInfo { - title: "This is just a test".to_owned(), - version: "1.2.3".to_owned(), - urls: vec!["http://localhost:12345/api/v1".to_owned()] - }; - let auth: AuthMiddleware = AuthMiddleware::new( - AuthSource::AuthorizationHeader, - AuthValidation::default(), - StaticAuthHandler::from_array(b"zlBsA2QXnkmpe0QTh8uCvtAEa4j33YAc") - ); - let (chain, pipelines) = single_pipeline(new_pipeline().add(auth).build()); - let server = TestServer::new(build_router(chain, pipelines, |router| { - router.with_openapi(info, |mut router| { - router.resource::("img"); - router.resource::("secret"); - router.resource::("custom"); - router.get_openapi("openapi"); - }); - })) - .unwrap(); - - test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_specification.json"); -} diff --git a/tests/openapi_supports_scope.json b/tests/openapi_supports_scope.json deleted file mode 100644 index bdef1fd..0000000 --- a/tests/openapi_supports_scope.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "components": {}, - "info": { - "title": "Test", - "version": "1.2.3" - }, - "openapi": "3.0.2", - "paths": { - "/bar/baz/foo3": { - "get": { - "responses": { - "200": { - "content": { - "*/*": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "OK" - } - } - } - }, - "/bar/foo2": { - "get": { - "responses": { - "200": { - "content": { - "*/*": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "OK" - } - } - } - }, - "/foo1": { - "get": { - "responses": { - "200": { - "content": { - "*/*": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "OK" - } - } - } - }, - "/foo4": { - "get": { - "responses": { - "200": { - "content": { - "*/*": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "OK" - } - } - } - } - } -} \ No newline at end of file diff --git a/tests/openapi_supports_scope.rs b/tests/openapi_supports_scope.rs deleted file mode 100644 index 33f240a..0000000 --- a/tests/openapi_supports_scope.rs +++ /dev/null @@ -1,50 +0,0 @@ -#![cfg(feature = "openapi")] -use gotham::{router::builder::*, test::TestServer}; -use gotham_restful::*; -use mime::TEXT_PLAIN; - -#[allow(dead_code)] -mod util { - include!("util/mod.rs"); -} -use util::{test_get_response, test_openapi_response}; - -const RESPONSE: &[u8] = b"This is the only valid response."; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all] -fn read_all() -> Raw<&'static [u8]> { - Raw::new(RESPONSE, TEXT_PLAIN) -} - -#[test] -fn openapi_supports_scope() { - let info = OpenapiInfo { - title: "Test".to_owned(), - version: "1.2.3".to_owned(), - urls: Vec::new() - }; - let server = TestServer::new(build_simple_router(|router| { - router.with_openapi(info, |mut router| { - router.get_openapi("openapi"); - router.resource::("foo1"); - router.scope("/bar", |router| { - router.resource::("foo2"); - router.scope("/baz", |router| { - router.resource::("foo3"); - }) - }); - router.resource::("foo4"); - }); - })) - .unwrap(); - - test_get_response(&server, "http://localhost/foo1", RESPONSE); - test_get_response(&server, "http://localhost/bar/foo2", RESPONSE); - test_get_response(&server, "http://localhost/bar/baz/foo3", RESPONSE); - test_get_response(&server, "http://localhost/foo4", RESPONSE); - test_openapi_response(&server, "http://localhost/openapi", "tests/openapi_supports_scope.json"); -} 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 deleted file mode 100644 index 2b440fa..0000000 --- a/tests/sync_methods.rs +++ /dev/null @@ -1,117 +0,0 @@ -#[macro_use] -extern crate gotham_derive; - -use gotham::{router::builder::*, test::TestServer}; -use gotham_restful::*; -use mime::{APPLICATION_JSON, TEXT_PLAIN}; -#[cfg(feature = "openapi")] -use openapi_type::OpenapiType; -use serde::Deserialize; - -mod util { - include!("util/mod.rs"); -} -use util::{test_delete_response, test_get_response, test_post_response, test_put_response}; - -#[derive(Resource)] -#[resource(read_all, read, search, create, change_all, change, remove_all, remove)] -struct FooResource; - -#[derive(Deserialize)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] -#[allow(dead_code)] -struct FooBody { - data: String -} - -#[derive(Clone, Deserialize, StateData, StaticResponseExtender)] -#[cfg_attr(feature = "openapi", derive(OpenapiType))] -#[allow(dead_code)] -struct FooSearch { - query: String -} - -const READ_ALL_RESPONSE: &[u8] = b"1ARwwSPVyOKpJKrYwqGgECPVWDl1BqajAAj7g7WJ3e"; -#[read_all] -fn read_all() -> Raw<&'static [u8]> { - Raw::new(READ_ALL_RESPONSE, TEXT_PLAIN) -} - -const READ_RESPONSE: &[u8] = b"FEReHoeBKU17X2bBpVAd1iUvktFL43CDu0cFYHdaP9"; -#[read] -fn read(_id: u64) -> Raw<&'static [u8]> { - Raw::new(READ_RESPONSE, TEXT_PLAIN) -} - -const SEARCH_RESPONSE: &[u8] = b"AWqcQUdBRHXKh3at4u79mdupOAfEbnTcx71ogCVF0E"; -#[search] -fn search(_body: FooSearch) -> Raw<&'static [u8]> { - Raw::new(SEARCH_RESPONSE, TEXT_PLAIN) -} - -const CREATE_RESPONSE: &[u8] = b"y6POY7wOMAB0jBRBw0FJT7DOpUNbhmT8KdpQPLkI83"; -#[create] -fn create(_body: FooBody) -> Raw<&'static [u8]> { - Raw::new(CREATE_RESPONSE, TEXT_PLAIN) -} - -const CHANGE_ALL_RESPONSE: &[u8] = b"QlbYg8gHE9OQvvk3yKjXJLTSXlIrg9mcqhfMXJmQkv"; -#[change_all] -fn change_all(_body: FooBody) -> Raw<&'static [u8]> { - Raw::new(CHANGE_ALL_RESPONSE, TEXT_PLAIN) -} - -const CHANGE_RESPONSE: &[u8] = b"qGod55RUXkT1lgPO8h0uVM6l368O2S0GrwENZFFuRu"; -#[change] -fn change(_id: u64, _body: FooBody) -> Raw<&'static [u8]> { - Raw::new(CHANGE_RESPONSE, TEXT_PLAIN) -} - -const REMOVE_ALL_RESPONSE: &[u8] = b"Y36kZ749MRk2Nem4BedJABOZiZWPLOtiwLfJlGTwm5"; -#[remove_all] -fn remove_all() -> Raw<&'static [u8]> { - Raw::new(REMOVE_ALL_RESPONSE, TEXT_PLAIN) -} - -const REMOVE_RESPONSE: &[u8] = b"CwRzBrKErsVZ1N7yeNfjZuUn1MacvgBqk4uPOFfDDq"; -#[remove] -fn remove(_id: u64) -> Raw<&'static [u8]> { - Raw::new(REMOVE_RESPONSE, TEXT_PLAIN) -} - -#[test] -fn sync_methods() { - let _ = pretty_env_logger::try_init_timed(); - - let server = TestServer::new(build_simple_router(|router| { - router.resource::("foo"); - })) - .unwrap(); - - test_get_response(&server, "http://localhost/foo", READ_ALL_RESPONSE); - test_get_response(&server, "http://localhost/foo/1", READ_RESPONSE); - test_get_response(&server, "http://localhost/foo/search?query=hello+world", SEARCH_RESPONSE); - test_post_response( - &server, - "http://localhost/foo", - r#"{"data":"hello world"}"#, - APPLICATION_JSON, - CREATE_RESPONSE - ); - test_put_response( - &server, - "http://localhost/foo", - r#"{"data":"hello world"}"#, - APPLICATION_JSON, - CHANGE_ALL_RESPONSE - ); - test_put_response( - &server, - "http://localhost/foo/1", - r#"{"data":"hello world"}"#, - APPLICATION_JSON, - CHANGE_RESPONSE - ); - test_delete_response(&server, "http://localhost/foo", REMOVE_ALL_RESPONSE); - test_delete_response(&server, "http://localhost/foo/1", REMOVE_RESPONSE); -} diff --git a/tests/trybuild_ui.rs b/tests/trybuild_ui.rs deleted file mode 100644 index 406ae6a..0000000 --- a/tests/trybuild_ui.rs +++ /dev/null @@ -1,10 +0,0 @@ -use trybuild::TestCases; - -#[test] -#[ignore] -fn trybuild_ui() { - let t = TestCases::new(); - t.compile_fail("tests/ui/endpoint/*.rs"); - t.compile_fail("tests/ui/from_body/*.rs"); - t.compile_fail("tests/ui/resource/*.rs"); -} 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.rs b/tests/ui/endpoint/invalid_attribute.rs deleted file mode 100644 index 3c321a5..0000000 --- a/tests/ui/endpoint/invalid_attribute.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_use] -extern crate gotham_restful; - -#[derive(Resource)] -#[resource(read_all)] -struct FooResource; - -#[read_all(FooResource)] -fn read_all() {} - -fn main() {} 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.rs b/tests/ui/endpoint/self.rs deleted file mode 100644 index 17591d0..0000000 --- a/tests/ui/endpoint/self.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(self) {} - -fn main() {} 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.stderr b/tests/ui/from_body/enum.stderr deleted file mode 100644 index f10c2c8..0000000 --- a/tests/ui/from_body/enum.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: #[derive(FromBody)] only works for structs - --> $DIR/enum.rs:5:1 - | -5 | enum FromBodyEnum { - | ^^^^ 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/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 deleted file mode 100644 index 40cae26..0000000 --- a/tests/util/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -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); -} - -pub fn test_post_response(server : &TestServer, path : &str, body : B, mime : Mime, expected : &[u8]) -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); -} - -pub fn test_put_response(server : &TestServer, path : &str, body : B, mime : Mime, expected : &[u8]) -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); -} - -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); -} - -#[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(_) => { - let mut file = File::create(output_file).unwrap(); - file.write_all(body.as_bytes()).unwrap(); - } - }; -}