├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── bors.toml ├── examples ├── body.rs ├── dyn_reply.rs ├── futures.rs ├── headers.rs ├── multiple_routes_call.rs ├── openapi.rs ├── rejections.rs ├── routing.rs ├── sse.rs ├── sse_chat.rs ├── todo_hello.rs ├── todo_websockets.rs ├── todo_websockets_chat.rs └── todos.rs ├── macros ├── Cargo.toml └── src │ ├── lib.rs │ ├── openapi │ ├── case.rs │ ├── derive.rs │ └── mod.rs │ ├── parse.rs │ ├── path.rs │ ├── route │ ├── fn_attr.rs │ ├── mod.rs │ └── param.rs │ ├── router.rs │ └── util.rs ├── src ├── docs.rs ├── factory.rs ├── lib.rs ├── openapi │ ├── builder.rs │ ├── entity.rs │ └── mod.rs ├── routes.rs └── rt.rs └── tests ├── async.rs ├── body.rs ├── factory.rs ├── filter.rs ├── header.rs ├── item_attrs.rs ├── mixed.rs ├── openapi.rs ├── openapi_arr_length.rs ├── openapi_clike_enum.rs ├── openapi_components_recursive.rs ├── openapi_description_example.rs ├── openapi_enum.rs ├── openapi_enumset.rs ├── openapi_generic_multi_struct.rs ├── openapi_generic_struct.rs ├── openapi_multi_struct.rs ├── openapi_rename.rs ├── openapi_request.rs ├── openapi_required.rs ├── openapi_response.rs ├── openapi_result_extagged.rs ├── openapi_schema.rs ├── openapi_serde_skip.rs ├── openapi_skip.rs ├── path_arg.rs ├── query_arg.rs ├── router.rs └── state.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: build 19 | args: --all-features 20 | - uses: actions-rs/cargo@v1 21 | with: 22 | command: test 23 | args: --all-features 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.bk 11 | 12 | 13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 14 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 15 | 16 | # User-specific stuff 17 | .idea/**/workspace.xml 18 | .idea/**/tasks.xml 19 | .idea/**/usage.statistics.xml 20 | .idea/**/dictionaries 21 | .idea/**/shelf 22 | .idea/ 23 | 24 | # Generated files 25 | .idea/**/contentModel.xml 26 | 27 | # Sensitive or high-churn files 28 | .idea/**/dataSources/ 29 | .idea/**/dataSources.ids 30 | .idea/**/dataSources.local.xml 31 | .idea/**/sqlDataSources.xml 32 | .idea/**/dynamic.xml 33 | .idea/**/uiDesigner.xml 34 | .idea/**/dbnavigator.xml 35 | 36 | # Gradle 37 | .idea/**/gradle.xml 38 | .idea/**/libraries 39 | 40 | # Gradle and Maven with auto-import 41 | # When using Gradle or Maven with auto-import, you should exclude module files, 42 | # since they will be recreated, and may cause churn. Uncomment if using 43 | # auto-import. 44 | # .idea/artifacts 45 | # .idea/compiler.xml 46 | # .idea/jarRepositories.xml 47 | # .idea/modules.xml 48 | # .idea/*.iml 49 | # .idea/modules 50 | # *.iml 51 | # *.ipr 52 | 53 | # CMake 54 | cmake-build-*/ 55 | 56 | # Mongo Explorer plugin 57 | .idea/**/mongoSettings.xml 58 | 59 | # File-based project format 60 | *.iws 61 | 62 | # IntelliJ 63 | out/ 64 | 65 | # mpeltonen/sbt-idea plugin 66 | .idea_modules/ 67 | 68 | # JIRA plugin 69 | atlassian-ide-plugin.xml 70 | 71 | # Cursive Clojure plugin 72 | .idea/replstate.xml 73 | 74 | # Crashlytics plugin (for Android Studio and IntelliJ) 75 | com_crashlytics_export_strings.xml 76 | crashlytics.properties 77 | crashlytics-build.properties 78 | fabric.properties 79 | 80 | # Editor-based Rest Client 81 | .idea/httpRequests 82 | 83 | # Android studio 3.1+ serialized cache file 84 | .idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_strings = true 2 | merge_imports = true 3 | use_field_init_shorthand = true 4 | wrap_comments = true 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | addons: 5 | apt: 6 | packages: 7 | - libssl-dev 8 | 9 | language: rust 10 | rust: 11 | - stable 12 | 13 | # Need to cache the whole `.cargo` directory to keep .crates.toml for 14 | # cargo-update to work 15 | cache: 16 | directories: 17 | - /home/travis/.cargo 18 | # But don't cache the cargo registry 19 | before_cache: 20 | - rm -rf /home/travis/.cargo/registry 21 | 22 | #git: 23 | # submodules: false 24 | #before_install: 25 | # - sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules 26 | # - git submodule update --init --recursive 27 | 28 | install: 29 | - cargo test --no-run --color always --all --all-features 30 | 31 | script: 32 | - RUST_BACKTRACE=full cargo test --color always --all 33 | - RUST_BACKTRACE=full cargo test --color always --all --all-features 34 | 35 | #before_deploy: 36 | # - CARGO_TARGET_DIR=$HOME/cargo-target cargo doc --color always 37 | 38 | after_success: 39 | # Temporarily disabled because cargo tarpaulin does not set CARGO_MANIFEST_DIR. 40 | # 41 | # - bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh) 42 | # - | 43 | # cargo tarpaulin --all --ignore-tests --out Xml && 44 | # bash <(curl -s https://codecov.io/bash) 45 | # - '[ $TRAVIS_PULL_REQUEST = false ] && 46 | # [ "$TRAVIS_BRANCH" == "master" ] && 47 | # ./.travis/docs.sh' 48 | #deploy: 49 | # local_dir: $HOME/cargo-target/doc 50 | # repo: kdy1/rustdoc 51 | # target_branch: gh-pages 52 | # provider: pages 53 | # skip_cleanup: true 54 | # github_token: $GH_TOKEN 55 | # email: kdy1997.dev@gmail.com 56 | # name: "강동윤" 57 | # on: 58 | # branch: master 59 | 60 | notifications: 61 | email: never 62 | # slack: 63 | # secure: rJ4xuH2auOcENKwxAM+0K08IufQ5HY5nFxTHAnSW82bQQfnP9D2mwo1782b2Jo05rt72FzbRBBEqqhC2vU5Mzs8btdtcl4CsEZLEZ5JGcV8G/Xq4Wkug6xk65LvzrfW6v9ZNdsdXc41KCbbalCDouJR3KkQ3RDQBQviG1nQzI0GsyuraMqTH7aKwZh4S4U/PRAalriW3eMoLw3al4mn3X4S60mAmmLs9bO4glUwMXsc68630ItEt+u2lPGXFj3LaWFkmD9nMSWCbAfAibZWThtqZogSxOEEsE+nW//HTXzICsic5s50JsIvwCXPqpAWDALGJhSTt+gSsrGFtCVhRJ1VOCcG/Y1ttGtsii3eeJ9yGgGt5F4ywbofQH9Decc5MWnKLiWDKPkTLDUV573fexvc4kgHYk0JtSz3q/5jVe6FwCwg1YNKcKW9A28sIoBxvgt5FOWwrwDhl7Ha0HX9gV0ylE88uaR/5OPzl5kXrjlaR3eua5EqaJ1lkezZvyRffJJJA07BxoF0eI5cQnR8jrw5PmybJpJWHXN48gd2CmGj3YB+JwiLpRAlWtkyKhM62UnQihN2h7mHcvwygGG8AGTk9mdWBkUYXxahZ+PPQHb4Mip/QXWdfK0DTAd52CLsYjW+Wc0xxW6jwEff3GaDaRDxU6IjWBmEo74XCRPqUiPk= 64 | 65 | env: 66 | global: 67 | - CARGO_INCREMENTAL=0 68 | 69 | branches: 70 | only: 71 | # This is where pull requests from "bors r+" are built. 72 | - staging 73 | # This is where pull requests from "bors try" are built. 74 | - trying 75 | # This is required to update docs. 76 | - master -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[rust]": { 4 | "editor.formatOnSave": true 5 | }, 6 | "rust-analyzer.cargo.allFeatures": true 7 | } 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | authors = ["강동윤 "] 5 | autoexamples = true 6 | autotests = true 7 | description = "Yet another web server framework for rust" 8 | edition = "2018" 9 | keywords = ["rweb", "server", "http", "hyper"] 10 | license = "Apache-2.0" 11 | name = "rweb" 12 | repository = "https://github.com/kdy1/rweb.git" 13 | version = "0.15.0" 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | 18 | [features] 19 | boxed = ["rweb-macros/boxed"] 20 | default = ["multipart", "websocket"] 21 | multipart = ["warp/multipart"] 22 | openapi = ["rweb-macros/openapi", "rweb-openapi"] 23 | tls = ["warp/tls"] 24 | websocket = ["warp/websocket"] 25 | 26 | [dependencies] 27 | chrono = {version = "0.4.19", features = ["serde"], optional = true} 28 | enumset = {version = "1.0", features = ["serde"], optional = true} 29 | futures = "0.3" 30 | http = "0.2" 31 | indexmap = "1" 32 | rweb-macros = {version = "0.14.0", path = "./macros"} 33 | rweb-openapi = {version = "0.7.0", optional = true} 34 | scoped-tls = "1" 35 | serde = {version = "1", features = ["derive"]} 36 | serde_json = "1" 37 | tokio = {version = "1.2", features = ["macros", "rt-multi-thread"]} 38 | tokio-stream = "0.1" 39 | uuid = {version = "0.8", features = ["serde"], optional = true} 40 | warp = {version = "0.3.0", default-features = false} 41 | 42 | [dev-dependencies] 43 | bytes = "1.0" 44 | http = "0.2" 45 | hyper = "0.14" 46 | log = "0.4" 47 | pretty_env_logger = "0.4" 48 | serde_yaml = "0.8" 49 | 50 | [[example]] 51 | name = "openapi" 52 | required-features = ["openapi"] 53 | 54 | [[example]] 55 | name = "todo_websockets" 56 | required-features = ["websocket"] 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | # "Cargo", 3 | "continuous-integration/travis-ci/push" 4 | ] 5 | use_squash_merge = true -------------------------------------------------------------------------------- /examples/body.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use rweb::{post, Reply}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Deserialize, Serialize)] 7 | struct Employee { 8 | name: String, 9 | rate: u32, 10 | } 11 | 12 | // TODO: Limit body size 13 | #[post("/employees/{rate}")] 14 | fn rate(rate: u32, #[json] mut employee: Employee) -> impl Reply { 15 | employee.rate = rate; 16 | rweb::reply::json(&employee) 17 | } 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | pretty_env_logger::init(); 22 | 23 | rweb::serve(rate()).run(([127, 0, 0, 1], 3030)).await 24 | } 25 | -------------------------------------------------------------------------------- /examples/dyn_reply.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use http::StatusCode; 4 | use rweb::*; 5 | 6 | #[get("/{word}")] 7 | async fn dyn_reply(word: String) -> Result, warp::Rejection> { 8 | if &word == "hello" { 9 | // a cast is needed for now, see https://github.com/rust-lang/rust/issues/60424 10 | Ok(Box::new("world") as Box) 11 | } else { 12 | Ok(Box::new(StatusCode::BAD_REQUEST) as Box) 13 | } 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | warp::serve(dyn_reply()).run(([127, 0, 0, 1], 3030)).await; 19 | } 20 | -------------------------------------------------------------------------------- /examples/futures.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use rweb::*; 4 | use std::{convert::Infallible, str::FromStr, time::Duration}; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // Match `/:Seconds`... 9 | let routes = sleepy(); 10 | 11 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 12 | } 13 | 14 | #[get("/{seconds}")] 15 | async fn sleepy(seconds: Seconds) -> Result { 16 | tokio::time::sleep(Duration::from_secs(seconds.0)).await; 17 | Ok(format!("I waited {} seconds!", seconds.0)) 18 | } 19 | 20 | /// A newtype to enforce our maximum allowed seconds. 21 | #[derive(Schema)] 22 | struct Seconds(u64); 23 | 24 | impl FromStr for Seconds { 25 | type Err = (); 26 | fn from_str(src: &str) -> Result { 27 | src.parse::().map_err(|_| ()).and_then(|num| { 28 | if num <= 5 { 29 | Ok(Seconds(num)) 30 | } else { 31 | Err(()) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/headers.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use rweb::*; 4 | use std::net::SocketAddr; 5 | 6 | /// For this example, we assume no DNS was used, 7 | /// so the Host header should be an address. 8 | /// 9 | /// Match when we get `accept: */*` exactly. 10 | #[get("/")] 11 | #[header("accept", "*/*")] 12 | fn routes(#[header = "host"] host: SocketAddr) -> String { 13 | format!("accepting stars on {}", host) 14 | } 15 | 16 | /// Create a server that requires header conditions: 17 | /// 18 | /// - `Host` is a `SocketAddr` 19 | /// - `Accept` is exactly `*/*` 20 | /// 21 | /// If these conditions don't match, a 404 is returned. 22 | #[tokio::main] 23 | async fn main() { 24 | pretty_env_logger::init(); 25 | 26 | rweb::serve(routes()).run(([127, 0, 0, 1], 3030)).await; 27 | } 28 | -------------------------------------------------------------------------------- /examples/multiple_routes_call.rs: -------------------------------------------------------------------------------- 1 | use rweb::*; 2 | use serde::Serialize; 3 | #[derive(Clone, Serialize)] 4 | struct User { 5 | id: u32, 6 | name: String, 7 | } 8 | #[get("/hi")] 9 | fn hi() -> &'static str { 10 | "Hello, World!" 11 | } 12 | 13 | #[get("/ping/{word}")] 14 | async fn ping(word: String) -> Result, warp::Rejection> { 15 | // a cast is needed for now, see https://github.com/rust-lang/rust/issues/60424 16 | Ok(Box::new(format!("pong {}", word)) as Box) 17 | } 18 | #[get("/user")] 19 | fn display_user(#[data] user: User) -> impl Reply { 20 | rweb::reply::json(&user) 21 | } 22 | 23 | #[tokio::main] 24 | async fn main() { 25 | let user = User { 26 | id: 1, 27 | name: "Christoffer".to_string(), 28 | }; 29 | // Sending user so it can be used in display user. Usually you would send a 30 | // db_connection or something. 31 | let routes = routes![hi, ping].or(routes![user; display_user]); 32 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 33 | } 34 | -------------------------------------------------------------------------------- /examples/openapi.rs: -------------------------------------------------------------------------------- 1 | use rweb::*; 2 | use serde::Deserialize; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | let (spec, filter) = openapi::spec().build(move || { 7 | // Build filters 8 | 9 | math::math() 10 | .or(products::products()) 11 | .or(generic::body()) 12 | .or(generic::optional()) 13 | .or(generic::search()) 14 | .or(response::response()) 15 | }); 16 | 17 | println!("go to http://localhost:3030/docs to interact with your openapi!"); 18 | serve(filter.or(openapi_docs(spec))) 19 | .run(([127, 0, 0, 1], 3030)) 20 | .await; 21 | } 22 | 23 | mod response { 24 | use rweb::*; 25 | use serde::Serialize; 26 | 27 | #[router("/response", services(json))] 28 | pub fn response() {} 29 | 30 | #[derive(Debug, Serialize, Schema)] 31 | pub struct Data { 32 | msg: String, 33 | } 34 | 35 | #[get("/json")] 36 | pub fn json() -> Json { 37 | Json::from(Data { 38 | msg: "Hello".into(), 39 | }) 40 | } 41 | } 42 | 43 | mod math { 44 | use rweb::*; 45 | 46 | #[router("/math", services(sum))] 47 | #[openapi(tags("math"))] 48 | pub fn math() {} 49 | 50 | /// Adds a and b 51 | /// and 52 | /// return it 53 | #[get("/sum/{a}/{b}")] 54 | fn sum(a: usize, b: usize) -> String { 55 | (a + b).to_string() 56 | } 57 | } 58 | 59 | mod products { 60 | use super::SearchReq; 61 | use rweb::*; 62 | use serde::{Deserialize, Serialize}; 63 | 64 | #[derive(Debug, Default, Serialize, Deserialize, Schema)] 65 | pub struct Product { 66 | pub id: String, 67 | pub title: String, 68 | } 69 | 70 | #[router("/products", services(list, product))] 71 | #[openapi(tags("products"))] 72 | pub fn products() {} 73 | 74 | #[get("/")] 75 | #[openapi(id = "products.list")] 76 | #[openapi(summary = "List products")] 77 | fn list(_query: Query) -> Json> { 78 | // Mix 79 | vec![].into() 80 | } 81 | 82 | #[get("/{id}")] 83 | #[openapi(id = "products.get")] 84 | #[openapi(summary = "Get a product")] 85 | #[openapi(response(code = "404", description = "Product not found"))] 86 | fn product(id: String) -> Json { 87 | Product { 88 | id, 89 | title: Default::default(), 90 | } 91 | .into() 92 | } 93 | } 94 | 95 | #[derive(Debug, Deserialize, Schema)] 96 | struct SearchReq { 97 | query: String, 98 | } 99 | 100 | mod generic { 101 | use super::SearchReq; 102 | use rweb::*; 103 | use serde::Deserialize; 104 | 105 | #[derive(Debug, Deserialize, Schema)] 106 | struct LoginForm { 107 | id: String, 108 | } 109 | 110 | #[post("/login")] 111 | #[openapi(tags("auth"))] 112 | pub fn body(_: Json) -> String { 113 | String::new() 114 | } 115 | 116 | #[post("/optional")] 117 | pub fn optional(_: Option>) -> String { 118 | String::new() 119 | } 120 | 121 | #[post("/search")] 122 | pub fn search(_: Option>) -> String { 123 | String::new() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /examples/rejections.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use rweb::{get, http::StatusCode, reject, Filter, Rejection, Reply}; 4 | use serde::Serialize; 5 | use std::{convert::Infallible, num::NonZeroU16}; 6 | 7 | #[get("/math/{num}")] 8 | fn math(num: u16, #[filter = "div_by"] denom: NonZeroU16) -> impl Reply { 9 | rweb::reply::json(&Math { 10 | op: format!("{} / {}", num, denom), 11 | output: num / denom.get(), 12 | }) 13 | } 14 | 15 | /// Rejections represent cases where a filter should not continue processing 16 | /// the request, but a different filter *could* process it. 17 | #[tokio::main] 18 | async fn main() { 19 | let routes = rweb::get().and(math()).recover(handle_rejection); 20 | 21 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 22 | } 23 | 24 | /// Extract a denominator from a "div-by" header, or reject with DivideByZero. 25 | fn div_by() -> impl Filter + Copy { 26 | rweb::header::("div-by").and_then(|n: u16| async move { 27 | if let Some(denom) = NonZeroU16::new(n) { 28 | Ok(denom) 29 | } else { 30 | Err(reject::custom(DivideByZero)) 31 | } 32 | }) 33 | } 34 | 35 | #[derive(Debug)] 36 | struct DivideByZero; 37 | 38 | impl reject::Reject for DivideByZero {} 39 | 40 | // JSON replies 41 | 42 | /// A successful math operation. 43 | #[derive(Serialize)] 44 | struct Math { 45 | op: String, 46 | output: u16, 47 | } 48 | 49 | /// An API error serializable to JSON. 50 | #[derive(Serialize)] 51 | struct ErrorMessage { 52 | code: u16, 53 | message: String, 54 | } 55 | 56 | // This function receives a `Rejection` and tries to return a custom 57 | // value, otherwise simply passes the rejection along. 58 | async fn handle_rejection(err: Rejection) -> Result { 59 | let code; 60 | let message; 61 | 62 | if err.is_not_found() { 63 | code = StatusCode::NOT_FOUND; 64 | message = "NOT_FOUND"; 65 | } else if let Some(DivideByZero) = err.find() { 66 | code = StatusCode::BAD_REQUEST; 67 | message = "DIVIDE_BY_ZERO"; 68 | } else if err.find::().is_some() { 69 | // We can handle a specific error, here METHOD_NOT_ALLOWED, 70 | // and render it however we want 71 | code = StatusCode::METHOD_NOT_ALLOWED; 72 | message = "METHOD_NOT_ALLOWED"; 73 | } else { 74 | // We should have expected this... Just log and say its a 500 75 | eprintln!("unhandled rejection: {:?}", err); 76 | code = StatusCode::INTERNAL_SERVER_ERROR; 77 | message = "UNHANDLED_REJECTION"; 78 | } 79 | 80 | let json = rweb::reply::json(&ErrorMessage { 81 | code: code.as_u16(), 82 | message: message.into(), 83 | }); 84 | 85 | Ok(rweb::reply::with_status(json, code)) 86 | } 87 | -------------------------------------------------------------------------------- /examples/routing.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use rweb::*; 4 | 5 | #[get("/hi")] 6 | fn hi() -> &'static str { 7 | "Hello, World!" 8 | } 9 | 10 | /// How about multiple segments? First, we could use the `path!` macro: 11 | #[get("/hello/from/warp")] 12 | fn hello_from_warp() -> &'static str { 13 | "Hello from warp!" 14 | } 15 | 16 | /// Fine, but how do I handle parameters in paths? 17 | #[get("/sum/{a}/{b}")] 18 | fn sum(a: u32, b: u32) -> String { 19 | format!("{} + {} = {}", a, b, a + b) 20 | } 21 | 22 | /// Any type that implements FromStr can be used, and in any order: 23 | #[get("/{a}/times/{b}")] 24 | fn times(a: u16, b: u16) -> String { 25 | format!("{} times {} = {}", a, b, a * b) 26 | } 27 | 28 | /// What! And? What's that do? 29 | /// 30 | /// It combines the filters in a sort of "this and then that" order. In 31 | /// fact, it's exactly what the `path!` macro has been doing internally. 32 | #[get("/byt/{name}")] 33 | fn bye(name: String) -> String { 34 | format!("Good bye, {}!", name) 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() { 39 | pretty_env_logger::init(); 40 | 41 | // We'll start simple, and gradually show how you combine these powers 42 | // into super powers! 43 | 44 | // Oh shoot, those math routes should be mounted at a different path, 45 | // is that possible? Yep. 46 | // 47 | // GET /math/sum/:u32/:u32 48 | // GET /math/:u16/times/:u16 49 | let math = rweb::path("math"); 50 | let _sum = math.and(sum()); 51 | let _times = math.and(times()); 52 | 53 | // Ah, can filters do things besides `and`? 54 | // 55 | // Why, yes they can! They can also `or`! As you might expect, `or` creates 56 | // a "this or else that" chain of filters. If the first doesn't succeed, 57 | // then it tries the other. 58 | // 59 | // So, those `math` routes could have been mounted all as one, with `or`. 60 | // 61 | // GET /math/sum/:u32/:u32 62 | // GET /math/:u16/times/:u16 63 | let math = rweb::path("math").and(sum().or(times())); 64 | 65 | // We can use the end() filter to match a shorter path 66 | let help = rweb::path("math") 67 | // Careful! Omitting the following line would make this filter match 68 | // requests to /math/sum/:u32/:u32 and /math/:u16/times/:u16 69 | .and(rweb::path::end()) 70 | .map(|| "This is the Math API. Try calling /math/sum/:u32/:u32 or /math/:u16/times/:u16"); 71 | let math = help.or(math); 72 | 73 | // Let's let people know that the `sum` and `times` routes are under `math`. 74 | let sum = 75 | sum().map(|output| format!("(This route has moved to /math/sum/:u16/:u16) {}", output)); 76 | let times = 77 | times().map(|output| format!("(This route has moved to /math/:u16/times/:u16) {}", output)); 78 | 79 | // It turns out, using `or` is how you combine everything together into 80 | // a single API. (We also actually haven't been enforcing the that the 81 | // method is GET, so we'll do that too!) 82 | // 83 | // GET /hi 84 | // GET /hello/from/warp 85 | // GET /bye/:string 86 | // GET /math/sum/:u32/:u32 87 | // GET /math/:u16/times/:u16 88 | 89 | let routes = rweb::get().and( 90 | hi().or(hello_from_warp()) 91 | .or(bye()) 92 | .or(math) 93 | .or(sum) 94 | .or(times), 95 | ); 96 | 97 | // Note that composing filters for many routes may increase compile times 98 | // (because it uses a lot of generics). If you wish to use dynamic dispatch 99 | // instead and speed up compile times while making it slightly slower at 100 | // runtime, you can use Filter::boxed(). 101 | 102 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 103 | } 104 | -------------------------------------------------------------------------------- /examples/sse.rs: -------------------------------------------------------------------------------- 1 | use rweb::{filters::sse::Event, get, Reply}; 2 | use std::{convert::Infallible, time::Duration}; 3 | use tokio::time::interval; 4 | use tokio_stream::{wrappers::IntervalStream, StreamExt}; 5 | 6 | // create server-sent event 7 | fn sse_counter(counter: u64) -> Result { 8 | Ok(Event::default().data(counter.to_string())) 9 | } 10 | 11 | #[get("/ticks")] 12 | fn ticks() -> impl Reply { 13 | let mut counter: u64 = 0; 14 | // create server event source 15 | let event_stream = IntervalStream::new(interval(Duration::from_secs(1))).map(move |_| { 16 | counter += 1; 17 | sse_counter(counter) 18 | }); 19 | // reply using server-sent events 20 | rweb::sse::reply(event_stream) 21 | } 22 | 23 | #[tokio::main] 24 | async fn main() { 25 | pretty_env_logger::init(); 26 | 27 | rweb::serve(ticks()).run(([127, 0, 0, 1], 3030)).await; 28 | } 29 | -------------------------------------------------------------------------------- /examples/sse_chat.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use futures::Stream; 3 | use rweb::{get, post, sse::Event, Filter, Rejection, Reply}; 4 | use std::{ 5 | collections::HashMap, 6 | sync::{ 7 | atomic::{AtomicUsize, Ordering}, 8 | Arc, Mutex, 9 | }, 10 | }; 11 | use tokio::sync::{mpsc, oneshot}; 12 | use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 13 | 14 | /// Our global unique user id counter. 15 | static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); 16 | 17 | /// Message variants. 18 | enum Message { 19 | UserId(usize), 20 | Reply(String), 21 | } 22 | 23 | #[derive(Debug)] 24 | struct NotUtf8; 25 | impl rweb::reject::Reject for NotUtf8 {} 26 | 27 | /// Our state of currently connected users. 28 | /// 29 | /// - Key is their id 30 | /// - Value is a sender of `Message` 31 | /// 32 | /// TODO(kdy1): .and(rweb::body::content_length_limit(500)) 33 | type Users = Arc>>>; 34 | 35 | #[post("/chat/{my_id}")] 36 | async fn send_chat( 37 | my_id: usize, 38 | #[body] msg: Bytes, 39 | #[data] users: Users, 40 | ) -> Result { 41 | let msg = std::str::from_utf8(&msg) 42 | .map(String::from) 43 | .map_err(|_e| rweb::reject::custom(NotUtf8))?; 44 | 45 | user_message(my_id, msg, &users); 46 | Ok(rweb::reply()) 47 | } 48 | 49 | #[get("/chat")] 50 | fn recv_chat(#[data] users: Users) -> impl Reply { 51 | // reply using server-sent events 52 | let stream = user_connected(users); 53 | rweb::sse::reply(rweb::sse::keep_alive().stream(stream)) 54 | } 55 | 56 | #[get("/")] 57 | fn index() -> impl Reply { 58 | rweb::http::Response::builder() 59 | .header("content-type", "text/html; charset=utf-8") 60 | .body(INDEX_HTML) 61 | } 62 | 63 | #[tokio::main] 64 | async fn main() { 65 | pretty_env_logger::init(); 66 | 67 | // Keep track of all connected users, key is usize, value 68 | // is an event stream sender. 69 | let users = Arc::new(Mutex::new(HashMap::new())); 70 | 71 | // POST /chat -> send message 72 | let chat_send = send_chat(users.clone()); 73 | 74 | // GET /chat -> messages stream 75 | let chat_recv = recv_chat(users.clone()); 76 | 77 | let routes = index().or(chat_recv).or(chat_send); 78 | 79 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 80 | } 81 | 82 | fn user_connected(users: Users) -> impl Stream> + Send + 'static { 83 | // Use a counter to assign a new unique ID for this user. 84 | let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); 85 | 86 | eprintln!("new chat user: {}", my_id); 87 | 88 | // Use an unbounded channel to handle buffering and flushing of messages 89 | // to the event source... 90 | let (tx, rx) = mpsc::unbounded_channel(); 91 | 92 | match tx.send(Message::UserId(my_id)) { 93 | Ok(()) => (), 94 | Err(_disconnected) => { 95 | // The tx is disconnected, our `user_disconnected` code 96 | // should be happening in another task, nothing more to 97 | // do here. 98 | } 99 | } 100 | 101 | // Make an extra clone of users list to give to our disconnection handler... 102 | let users2 = users.clone(); 103 | 104 | // Save the sender in our list of connected users. 105 | users.lock().unwrap().insert(my_id, tx); 106 | 107 | // Create channel to track disconnecting the receiver side of events. 108 | // This is little bit tricky. 109 | let (mut dtx, mut drx) = oneshot::channel::<()>(); 110 | 111 | // When `drx` will dropped then `dtx` will be canceled. 112 | // We can track it to make sure when the user leaves chat. 113 | tokio::task::spawn(async move { 114 | dtx.closed().await; 115 | drx.close(); 116 | user_disconnected(my_id, &users2); 117 | }); 118 | 119 | // Convert messages into Server-Sent Events and return resulting stream. 120 | UnboundedReceiverStream::new(rx).map(|msg| match msg { 121 | Message::UserId(my_id) => Ok(rweb::sse::Event::default() 122 | .event("user") 123 | .data(my_id.to_string())), 124 | Message::Reply(reply) => Ok(rweb::sse::Event::default().data(reply)), 125 | }) 126 | } 127 | 128 | fn user_message(my_id: usize, msg: String, users: &Users) { 129 | let new_msg = format!(": {}", my_id, msg); 130 | 131 | // New message from this user, send it to everyone else (except same uid)... 132 | // 133 | // We use `retain` instead of a for loop so that we can reap any user that 134 | // appears to have disconnected. 135 | for (&uid, tx) in users.lock().unwrap().iter_mut() { 136 | if my_id != uid { 137 | match tx.send(Message::Reply(new_msg.clone())) { 138 | Ok(()) => (), 139 | Err(_disconnected) => { 140 | // The tx is disconnected, our `user_disconnected` code 141 | // should be happening in another task, nothing more to 142 | // do here. 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | fn user_disconnected(my_id: usize, users: &Users) { 150 | eprintln!("good bye user: {}", my_id); 151 | 152 | // Stream closed up, so remove from the user list 153 | users.lock().unwrap().remove(&my_id); 154 | } 155 | 156 | static INDEX_HTML: &str = r#" 157 | 158 | 159 | 160 | Warp Chat 161 | 162 | 163 |

warp chat

164 |
165 |

Connecting...

166 |
167 | 168 | 169 | 196 | 197 | 198 | "#; 199 | -------------------------------------------------------------------------------- /examples/todo_hello.rs: -------------------------------------------------------------------------------- 1 | // Temporarily ignored as rweb does not support ** route yet. 2 | // 3 | //#![deny(warnings)] 4 | //use rweb::Filter; 5 | // 6 | #[tokio::main] 7 | async fn main() { 8 | // // Match any request and return hello world! 9 | // let routes = rweb::any().map(|| "Hello, World!"); 10 | // 11 | // rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 12 | } 13 | -------------------------------------------------------------------------------- /examples/todo_websockets.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use futures::{FutureExt, StreamExt}; 4 | use rweb::Filter; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | pretty_env_logger::init(); 9 | 10 | let routes = rweb::path("echo") 11 | // The `ws()` filter will prepare the Websocket handshake. 12 | .and(rweb::ws()) 13 | .map(|ws: rweb::ws::Ws| { 14 | // And then our closure will be called when it completes... 15 | ws.on_upgrade(|websocket| { 16 | // Just echo all messages back... 17 | let (tx, rx) = websocket.split(); 18 | rx.forward(tx).map(|result| { 19 | if let Err(e) = result { 20 | eprintln!("websocket error: {:?}", e); 21 | } 22 | }) 23 | }) 24 | }); 25 | 26 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 27 | } 28 | -------------------------------------------------------------------------------- /examples/todo_websockets_chat.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use std::{ 3 | collections::HashMap, 4 | sync::{ 5 | atomic::{AtomicUsize, Ordering}, 6 | Arc, Mutex, 7 | }, 8 | }; 9 | 10 | use futures::{future, Future, FutureExt, StreamExt}; 11 | use rweb::{ 12 | ws::{Message, WebSocket}, 13 | Filter, 14 | }; 15 | use tokio::sync::mpsc; 16 | use tokio_stream::wrappers::UnboundedReceiverStream; 17 | 18 | /// Our global unique user id counter. 19 | static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); 20 | 21 | /// Our state of currently connected users. 22 | /// 23 | /// - Key is their id 24 | /// - Value is a sender of `rweb::ws::Message` 25 | type Users = Arc>>>>; 26 | 27 | #[tokio::main] 28 | async fn main() { 29 | pretty_env_logger::init(); 30 | 31 | // Keep track of all connected users, key is usize, value 32 | // is a websocket sender. 33 | let users = Arc::new(Mutex::new(HashMap::new())); 34 | // Turn our "state" into a new Filter... 35 | let users = rweb::any().map(move || users.clone()); 36 | 37 | // GET /chat -> websocket upgrade 38 | let chat = rweb::path("chat") 39 | // The `ws()` filter will prepare Websocket handshake... 40 | .and(rweb::ws()) 41 | .and(users) 42 | .map(|ws: rweb::ws::Ws, users| { 43 | // This will call our function if the handshake succeeds. 44 | ws.on_upgrade(move |socket| user_connected(socket, users).map(|result| result.unwrap())) 45 | }); 46 | 47 | // GET / -> index html 48 | let index = rweb::path::end().map(|| rweb::reply::html(INDEX_HTML)); 49 | 50 | let routes = index.or(chat); 51 | 52 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 53 | } 54 | 55 | fn user_connected(ws: WebSocket, users: Users) -> impl Future> { 56 | // Use a counter to assign a new unique ID for this user. 57 | let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); 58 | 59 | eprintln!("new chat user: {}", my_id); 60 | 61 | // Split the socket into a sender and receive of messages. 62 | let (user_ws_tx, user_ws_rx) = ws.split(); 63 | 64 | // Use an unbounded channel to handle buffering and flushing of messages 65 | // to the websocket... 66 | let (tx, rx) = mpsc::unbounded_channel(); 67 | tokio::task::spawn( 68 | UnboundedReceiverStream::new(rx) 69 | .forward(user_ws_tx) 70 | .map(|result| { 71 | if let Err(e) = result { 72 | eprintln!("websocket send error: {}", e); 73 | } 74 | }), 75 | ); 76 | 77 | // Save the sender in our list of connected users. 78 | users.lock().unwrap().insert(my_id, tx); 79 | 80 | // Return a `Future` that is basically a state machine managing 81 | // this specific user's connection. 82 | 83 | // Make an extra clone to give to our disconnection handler... 84 | let users2 = users.clone(); 85 | 86 | user_ws_rx 87 | // Every time the user sends a message, broadcast it to 88 | // all other users... 89 | .for_each(move |msg| { 90 | user_message(my_id, msg.unwrap(), &users); 91 | future::ready(()) 92 | }) 93 | // for_each will keep processing as long as the user stays 94 | // connected. Once they disconnect, then... 95 | .then(move |result| { 96 | user_disconnected(my_id, &users2); 97 | future::ok(result) 98 | }) 99 | // If at any time, there was a websocket error, log here... 100 | // .map_err(move |e| { 101 | // eprintln!("websocket error(uid={}): {}", my_id, e); 102 | // }) 103 | } 104 | 105 | fn user_message(my_id: usize, msg: Message, users: &Users) { 106 | // Skip any non-Text messages... 107 | let msg = if let Ok(s) = msg.to_str() { 108 | s 109 | } else { 110 | return; 111 | }; 112 | 113 | let new_msg = format!(": {}", my_id, msg); 114 | 115 | // New message from this user, send it to everyone else (except same uid)... 116 | // 117 | // We use `retain` instead of a for loop so that we can reap any user that 118 | // appears to have disconnected. 119 | for (&uid, tx) in users.lock().unwrap().iter_mut() { 120 | if my_id != uid { 121 | match tx.send(Ok(Message::text(new_msg.clone()))) { 122 | Ok(()) => (), 123 | Err(_disconnected) => { 124 | // The tx is disconnected, our `user_disconnected` code 125 | // should be happening in another task, nothing more to 126 | // do here. 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | fn user_disconnected(my_id: usize, users: &Users) { 134 | eprintln!("good bye user: {}", my_id); 135 | 136 | // Stream closed up, so remove from the user list 137 | users.lock().unwrap().remove(&my_id); 138 | } 139 | 140 | static INDEX_HTML: &str = r#" 141 | 142 | 143 | 144 | Warp Chat 145 | 146 | 147 |

warp chat

148 |
149 |

Connecting...

150 |
151 | 152 | 153 | 179 | 180 | 181 | "#; 182 | -------------------------------------------------------------------------------- /examples/todos.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use rweb::Filter; 4 | use std::env; 5 | 6 | /// Provides a RESTful web server managing some Todos. 7 | /// 8 | /// API will be: 9 | /// 10 | /// - `GET /todos`: return a JSON list of Todos. 11 | /// - `POST /todos`: create a new Todo. 12 | /// - `PUT /todos/:id`: update a specific Todo. 13 | /// - `DELETE /todos/:id`: delete a specific Todo. 14 | #[tokio::main] 15 | async fn main() { 16 | if env::var_os("RUST_LOG").is_none() { 17 | // Set `RUST_LOG=todos=debug` to see debug logs, 18 | // this only shows access logs. 19 | env::set_var("RUST_LOG", "todos=info"); 20 | } 21 | pretty_env_logger::init(); 22 | 23 | let db = models::blank_db(); 24 | 25 | let api = filters::todos(db); 26 | 27 | // View access logs by setting `RUST_LOG=todos`. 28 | let routes = api.with(rweb::log("todos")); 29 | // Start up the server... 30 | rweb::serve(routes).run(([127, 0, 0, 1], 3030)).await; 31 | } 32 | 33 | mod filters { 34 | use super::{ 35 | handlers, 36 | models::{Db, ListOptions, Todo}, 37 | }; 38 | use rweb::Filter; 39 | 40 | /// The 4 TODOs filters combined. 41 | pub fn todos( 42 | db: Db, 43 | ) -> impl Filter + Clone { 44 | todos_list(db.clone()) 45 | .or(todos_create(db.clone())) 46 | .or(todos_update(db.clone())) 47 | .or(todos_delete(db)) 48 | } 49 | 50 | /// GET /todos?offset=3&limit=5 51 | pub fn todos_list( 52 | db: Db, 53 | ) -> impl Filter + Clone { 54 | rweb::path!("todos") 55 | .and(rweb::get()) 56 | .and(rweb::query::()) 57 | .and(with_db(db)) 58 | .and_then(handlers::list_todos) 59 | } 60 | 61 | /// POST /todos with JSON body 62 | pub fn todos_create( 63 | db: Db, 64 | ) -> impl Filter + Clone { 65 | rweb::path!("todos") 66 | .and(rweb::post()) 67 | .and(json_body()) 68 | .and(with_db(db)) 69 | .and_then(handlers::create_todo) 70 | } 71 | 72 | /// PUT /todos/:id with JSON body 73 | pub fn todos_update( 74 | db: Db, 75 | ) -> impl Filter + Clone { 76 | rweb::path!("todos" / u64) 77 | .and(rweb::put()) 78 | .and(json_body()) 79 | .and(with_db(db)) 80 | .and_then(handlers::update_todo) 81 | } 82 | 83 | /// DELETE /todos/:id 84 | pub fn todos_delete( 85 | db: Db, 86 | ) -> impl Filter + Clone { 87 | // We'll make one of our endpoints admin-only to show how authentication filters 88 | // are used 89 | let admin_only = rweb::header::exact("authorization", "Bearer admin"); 90 | 91 | rweb::path!("todos" / u64) 92 | // It is important to put the auth check _after_ the path filters. 93 | // If we put the auth check before, the request `PUT /todos/invalid-string` 94 | // would try this filter and reject because the authorization header doesn't match, 95 | // rather because the param is wrong for that other path. 96 | .and(admin_only) 97 | .and(rweb::delete()) 98 | .and(with_db(db)) 99 | .and_then(handlers::delete_todo) 100 | } 101 | 102 | fn with_db(db: Db) -> impl Filter + Clone { 103 | rweb::any().map(move || db.clone()) 104 | } 105 | 106 | fn json_body() -> impl Filter + Clone { 107 | // When accepting a body, we want a JSON body 108 | // (and to reject huge payloads)... 109 | rweb::body::content_length_limit(1024 * 16).and(rweb::body::json()) 110 | } 111 | } 112 | 113 | /// These are our API handlers, the ends of each filter chain. 114 | /// Notice how thanks to using `Filter::and`, we can define a function 115 | /// with the exact arguments we'd expect from each filter in the chain. 116 | /// No tuples are needed, it's auto flattened for the functions. 117 | mod handlers { 118 | use super::models::{Db, ListOptions, Todo}; 119 | use rweb::http::StatusCode; 120 | use std::convert::Infallible; 121 | 122 | pub async fn list_todos(opts: ListOptions, db: Db) -> Result { 123 | // Just return a JSON array of todos, applying the limit and offset. 124 | let todos = db.lock().await; 125 | let todos: Vec = todos 126 | .clone() 127 | .into_iter() 128 | .skip(opts.offset.unwrap_or(0)) 129 | .take(opts.limit.unwrap_or(std::usize::MAX)) 130 | .collect(); 131 | Ok(rweb::reply::json(&todos)) 132 | } 133 | 134 | pub async fn create_todo(create: Todo, db: Db) -> Result { 135 | log::debug!("create_todo: {:?}", create); 136 | 137 | let mut vec = db.lock().await; 138 | 139 | for todo in vec.iter() { 140 | if todo.id == create.id { 141 | log::debug!(" -> id already exists: {}", create.id); 142 | // Todo with id already exists, return `400 BadRequest`. 143 | return Ok(StatusCode::BAD_REQUEST); 144 | } 145 | } 146 | 147 | // No existing Todo with id, so insert and return `201 Created`. 148 | vec.push(create); 149 | 150 | Ok(StatusCode::CREATED) 151 | } 152 | 153 | pub async fn update_todo( 154 | id: u64, 155 | update: Todo, 156 | db: Db, 157 | ) -> Result { 158 | log::debug!("update_todo: id={}, todo={:?}", id, update); 159 | let mut vec = db.lock().await; 160 | 161 | // Look for the specified Todo... 162 | for todo in vec.iter_mut() { 163 | if todo.id == id { 164 | *todo = update; 165 | return Ok(StatusCode::OK); 166 | } 167 | } 168 | 169 | log::debug!(" -> todo id not found!"); 170 | 171 | // If the for loop didn't return OK, then the ID doesn't exist... 172 | Ok(StatusCode::NOT_FOUND) 173 | } 174 | 175 | pub async fn delete_todo(id: u64, db: Db) -> Result { 176 | log::debug!("delete_todo: id={}", id); 177 | 178 | let mut vec = db.lock().await; 179 | 180 | let len = vec.len(); 181 | vec.retain(|todo| { 182 | // Retain all Todos that aren't this id... 183 | // In other words, remove all that *are* this id... 184 | todo.id != id 185 | }); 186 | 187 | // If the vec is smaller, we found and deleted a Todo! 188 | let deleted = vec.len() != len; 189 | 190 | if deleted { 191 | // respond with a `204 No Content`, which means successful, 192 | // yet no body expected... 193 | Ok(StatusCode::NO_CONTENT) 194 | } else { 195 | log::debug!(" -> todo id not found!"); 196 | Ok(StatusCode::NOT_FOUND) 197 | } 198 | } 199 | } 200 | 201 | mod models { 202 | use serde::{Deserialize, Serialize}; 203 | use std::sync::Arc; 204 | use tokio::sync::Mutex; 205 | 206 | /// So we don't have to tackle how different database work, we'll just use 207 | /// a simple in-memory DB, a vector synchronized by a mutex. 208 | pub type Db = Arc>>; 209 | 210 | pub fn blank_db() -> Db { 211 | Arc::new(Mutex::new(Vec::new())) 212 | } 213 | 214 | #[derive(Debug, Deserialize, Serialize, Clone)] 215 | pub struct Todo { 216 | pub id: u64, 217 | pub text: String, 218 | pub completed: bool, 219 | } 220 | 221 | // The query parameters for list_todos. 222 | #[derive(Debug, Deserialize)] 223 | pub struct ListOptions { 224 | pub offset: Option, 225 | pub limit: Option, 226 | } 227 | } 228 | 229 | #[cfg(test)] 230 | mod tests { 231 | use rweb::{http::StatusCode, test::request}; 232 | 233 | use super::{ 234 | filters, 235 | models::{self, Todo}, 236 | }; 237 | 238 | #[tokio::test] 239 | async fn test_post() { 240 | let db = models::blank_db(); 241 | let api = filters::todos(db); 242 | 243 | let resp = request() 244 | .method("POST") 245 | .path("/todos") 246 | .json(&Todo { 247 | id: 1, 248 | text: "test 1".into(), 249 | completed: false, 250 | }) 251 | .reply(&api) 252 | .await; 253 | 254 | assert_eq!(resp.status(), StatusCode::CREATED); 255 | } 256 | 257 | #[tokio::test] 258 | async fn test_post_conflict() { 259 | let db = models::blank_db(); 260 | db.lock().await.push(todo1()); 261 | let api = filters::todos(db); 262 | 263 | let resp = request() 264 | .method("POST") 265 | .path("/todos") 266 | .json(&todo1()) 267 | .reply(&api) 268 | .await; 269 | 270 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 271 | } 272 | 273 | #[tokio::test] 274 | async fn test_put_unknown() { 275 | let _ = pretty_env_logger::try_init(); 276 | let db = models::blank_db(); 277 | let api = filters::todos(db); 278 | 279 | let resp = request() 280 | .method("PUT") 281 | .path("/todos/1") 282 | .header("authorization", "Bearer admin") 283 | .json(&todo1()) 284 | .reply(&api) 285 | .await; 286 | 287 | assert_eq!(resp.status(), StatusCode::NOT_FOUND); 288 | } 289 | 290 | fn todo1() -> Todo { 291 | Todo { 292 | id: 1, 293 | text: "test 1".into(), 294 | completed: false, 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["강동윤 "] 3 | description = "Yet another web server framework for rust" 4 | edition = "2018" 5 | license = "Apache-2.0" 6 | name = "rweb-macros" 7 | repository = "https://github.com/kdy1/rweb.git" 8 | version = "0.14.0" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [features] 14 | boxed = [] 15 | openapi = [] 16 | 17 | [dependencies] 18 | pmutil = "0.5.3" 19 | proc-macro2 = "1" 20 | quote = "1" 21 | rweb-openapi = "0.6.0" 22 | syn = {version = "1", features = ["full", "visit"]} 23 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use self::route::compile_route; 3 | use pmutil::{q, ToTokensExt}; 4 | 5 | mod openapi; 6 | mod parse; 7 | mod path; 8 | mod route; 9 | mod router; 10 | mod util; 11 | 12 | #[proc_macro_attribute] 13 | pub fn get( 14 | path: proc_macro::TokenStream, 15 | fn_item: proc_macro::TokenStream, 16 | ) -> proc_macro::TokenStream { 17 | compile_route(Some(q!({ get })), path.into(), fn_item.into()) 18 | } 19 | 20 | #[proc_macro_attribute] 21 | pub fn post( 22 | path: proc_macro::TokenStream, 23 | fn_item: proc_macro::TokenStream, 24 | ) -> proc_macro::TokenStream { 25 | compile_route(Some(q!({ post })), path.into(), fn_item.into()) 26 | } 27 | 28 | #[proc_macro_attribute] 29 | pub fn put( 30 | path: proc_macro::TokenStream, 31 | fn_item: proc_macro::TokenStream, 32 | ) -> proc_macro::TokenStream { 33 | compile_route(Some(q!({ put })), path.into(), fn_item.into()) 34 | } 35 | 36 | #[proc_macro_attribute] 37 | pub fn delete( 38 | path: proc_macro::TokenStream, 39 | fn_item: proc_macro::TokenStream, 40 | ) -> proc_macro::TokenStream { 41 | compile_route(Some(q!({ delete })), path.into(), fn_item.into()) 42 | } 43 | 44 | #[proc_macro_attribute] 45 | pub fn head( 46 | path: proc_macro::TokenStream, 47 | fn_item: proc_macro::TokenStream, 48 | ) -> proc_macro::TokenStream { 49 | compile_route(Some(q!({ head })), path.into(), fn_item.into()) 50 | } 51 | 52 | #[proc_macro_attribute] 53 | pub fn options( 54 | path: proc_macro::TokenStream, 55 | fn_item: proc_macro::TokenStream, 56 | ) -> proc_macro::TokenStream { 57 | compile_route(Some(q!({ options })), path.into(), fn_item.into()) 58 | } 59 | 60 | #[proc_macro_attribute] 61 | pub fn patch( 62 | path: proc_macro::TokenStream, 63 | fn_item: proc_macro::TokenStream, 64 | ) -> proc_macro::TokenStream { 65 | compile_route(Some(q!({ patch })), path.into(), fn_item.into()) 66 | } 67 | 68 | /// Creates a router. Useful for modularizing codes. 69 | /// 70 | /// 71 | /// # Note 72 | /// 73 | /// Currently router returns 404 error if there is a no matching rule. 74 | #[proc_macro_attribute] 75 | pub fn router( 76 | attr: proc_macro::TokenStream, 77 | item: proc_macro::TokenStream, 78 | ) -> proc_macro::TokenStream { 79 | router::router(attr.into(), item.into()).dump().into() 80 | } 81 | 82 | /// Implements Entity for the type. 83 | /// 84 | /// See documentation of Entity for details and examples. 85 | #[proc_macro_derive(Schema, attributes(schema))] 86 | pub fn derive_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 87 | if !cfg!(feature = "openapi") { 88 | return "".parse().unwrap(); 89 | } 90 | let input = syn::parse::(input).expect("failed to parse derive input"); 91 | openapi::derive_schema(input).into() 92 | } 93 | -------------------------------------------------------------------------------- /macros/src/openapi/case.rs: -------------------------------------------------------------------------------- 1 | //! Code to convert the Rust-styled field/variant (e.g. `my_field`, `MyType`) to 2 | //! the case of the source (e.g. `my-field`, `MY_FIELD`). 3 | //! 4 | //! Copied from https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/case.rs 5 | 6 | // See https://users.rust-lang.org/t/psa-dealing-with-warning-unused-import-std-ascii-asciiext-in-today-s-nightly/13726 7 | use self::RenameRule::*; 8 | #[allow(deprecated, unused_imports)] 9 | use std::ascii::AsciiExt; 10 | use std::str::FromStr; 11 | 12 | /// The different possible ways to change case of fields in a struct, or 13 | /// variants in an enum. 14 | #[derive(Copy, Clone, PartialEq)] 15 | pub enum RenameRule { 16 | /// Don't apply a default rename rule. 17 | None, 18 | /// Rename direct children to "lowercase" style. 19 | LowerCase, 20 | /// Rename direct children to "UPPERCASE" style. 21 | UPPERCASE, 22 | /// Rename direct children to "PascalCase" style, as typically used for 23 | /// enum variants. 24 | PascalCase, 25 | /// Rename direct children to "camelCase" style. 26 | CamelCase, 27 | /// Rename direct children to "snake_case" style, as commonly used for 28 | /// fields. 29 | SnakeCase, 30 | /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly 31 | /// used for constants. 32 | ScreamingSnakeCase, 33 | /// Rename direct children to "kebab-case" style. 34 | KebabCase, 35 | /// Rename direct children to "SCREAMING-KEBAB-CASE" style. 36 | ScreamingKebabCase, 37 | } 38 | 39 | impl RenameRule { 40 | /// Apply a renaming rule to an enum variant, returning the version expected 41 | /// in the source. 42 | #[allow(dead_code)] 43 | pub fn apply_to_variant(&self, variant: &str) -> String { 44 | match *self { 45 | None | PascalCase => variant.to_owned(), 46 | LowerCase => variant.to_ascii_lowercase(), 47 | UPPERCASE => variant.to_ascii_uppercase(), 48 | CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..], 49 | SnakeCase => { 50 | let mut snake = String::new(); 51 | for (i, ch) in variant.char_indices() { 52 | if i > 0 && ch.is_uppercase() { 53 | snake.push('_'); 54 | } 55 | snake.push(ch.to_ascii_lowercase()); 56 | } 57 | snake 58 | } 59 | ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(), 60 | KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"), 61 | ScreamingKebabCase => ScreamingSnakeCase 62 | .apply_to_variant(variant) 63 | .replace('_', "-"), 64 | } 65 | } 66 | 67 | /// Apply a renaming rule to a struct field, returning the version expected 68 | /// in the source. 69 | pub fn apply_to_field(&self, field: &str) -> String { 70 | match *self { 71 | None | LowerCase | SnakeCase => field.to_owned(), 72 | UPPERCASE => field.to_ascii_uppercase(), 73 | PascalCase => { 74 | let mut pascal = String::new(); 75 | let mut capitalize = true; 76 | for ch in field.chars() { 77 | if ch == '_' { 78 | capitalize = true; 79 | } else if capitalize { 80 | pascal.push(ch.to_ascii_uppercase()); 81 | capitalize = false; 82 | } else { 83 | pascal.push(ch); 84 | } 85 | } 86 | pascal 87 | } 88 | CamelCase => { 89 | let pascal = PascalCase.apply_to_field(field); 90 | pascal[..1].to_ascii_lowercase() + &pascal[1..] 91 | } 92 | ScreamingSnakeCase => field.to_ascii_uppercase(), 93 | KebabCase => field.replace('_', "-"), 94 | ScreamingKebabCase => ScreamingSnakeCase.apply_to_field(field).replace('_', "-"), 95 | } 96 | } 97 | } 98 | 99 | impl FromStr for RenameRule { 100 | type Err = (); 101 | 102 | fn from_str(rename_all_str: &str) -> Result { 103 | match rename_all_str { 104 | "lowercase" => Ok(LowerCase), 105 | "UPPERCASE" => Ok(UPPERCASE), 106 | "PascalCase" => Ok(PascalCase), 107 | "camelCase" => Ok(CamelCase), 108 | "snake_case" => Ok(SnakeCase), 109 | "SCREAMING_SNAKE_CASE" => Ok(ScreamingSnakeCase), 110 | "kebab-case" => Ok(KebabCase), 111 | "SCREAMING-KEBAB-CASE" => Ok(ScreamingKebabCase), 112 | _ => Err(()), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /macros/src/openapi/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! 3 | //! 4 | //! # Rules 5 | //! 6 | //! - We abuse `Parameter.ref_path` to store type name. 7 | 8 | pub use self::derive::derive_schema; 9 | use crate::{ 10 | parse::{Delimited, Paren}, 11 | path::find_ty, 12 | route::EqStr, 13 | }; 14 | use pmutil::{q, Quote, ToTokensExt}; 15 | use proc_macro2::TokenStream; 16 | use quote::ToTokens; 17 | use rweb_openapi::v3_0::{ 18 | Location, MediaType, ObjectOrReference, Operation, Parameter, ParameterRepresentation, 19 | Response, Schema, 20 | }; 21 | use std::borrow::Cow; 22 | use syn::{ 23 | parse2, 24 | punctuated::{Pair, Punctuated}, 25 | Attribute, Expr, Lit, Meta, NestedMeta, Signature, Token, 26 | }; 27 | 28 | mod case; 29 | mod derive; 30 | 31 | macro_rules! quote_str_indexmap { 32 | ($map:expr, $quot:ident) => { 33 | $map.iter() 34 | .map(|(nam, t)| { 35 | let tq = $quot(t); 36 | Pair::Punctuated( 37 | q!(Vars { nam, tq }, { rweb::rt::Cow::Borrowed(nam) => tq }), 38 | Default::default(), 39 | ) 40 | }) 41 | .collect(); 42 | }; 43 | } 44 | 45 | pub fn quote_op(op: Operation) -> Expr { 46 | let tags_v: Punctuated = op 47 | .tags 48 | .iter() 49 | .map(|tag| { 50 | Pair::Punctuated( 51 | q!(Vars { tag }, { rweb::rt::Cow::Borrowed(tag) }), 52 | Default::default(), 53 | ) 54 | }) 55 | .collect(); 56 | 57 | let params_v: Punctuated = op 58 | .parameters 59 | .iter() 60 | .map(|v| Pair::Punctuated(quote_parameter(v), Default::default())) 61 | .collect(); 62 | 63 | let responses_v: Punctuated = 64 | quote_str_indexmap!(op.responses, quote_response); 65 | 66 | q!( 67 | Vars { 68 | tags_v, 69 | id_v: op.operation_id, 70 | summary_v: op.summary, 71 | description_v: op.description, 72 | params_v, 73 | responses_v, 74 | }, 75 | { 76 | rweb::openapi::Operation { 77 | tags: vec![tags_v], 78 | summary: rweb::rt::Cow::Borrowed(summary_v), 79 | description: rweb::rt::Cow::Borrowed(description_v), 80 | operation_id: rweb::rt::Cow::Borrowed(id_v), 81 | parameters: vec![params_v], 82 | responses: rweb::rt::indexmap! {responses_v}, 83 | ..Default::default() 84 | } 85 | } 86 | ) 87 | .parse() 88 | } 89 | 90 | /// TODO: Move this to pmutil 91 | fn quote_option(o: Option) -> Quote 92 | where 93 | T: ToTokens, 94 | { 95 | match o { 96 | Some(v) => q!(Vars { v }, { Some(v) }), 97 | None => q!({ None }), 98 | } 99 | } 100 | 101 | fn quote_parameter(param: &ObjectOrReference) -> Expr { 102 | let param = match param { 103 | ObjectOrReference::Ref { .. } => unreachable!("quote_parameter(ObjectOrReference::Ref)"), 104 | ObjectOrReference::Object(param) => param, 105 | }; 106 | 107 | let required_v = quote_option(param.required); 108 | 109 | let ty = match ¶m.representation { 110 | Some(ParameterRepresentation::Simple { 111 | schema: ObjectOrReference::Ref { ref_path }, 112 | }) => ref_path.parse::(), 113 | Some(ParameterRepresentation::Simple { 114 | schema: ObjectOrReference::Object(Schema { ref_path, .. }), 115 | }) if !ref_path.is_empty() => ref_path.parse::(), 116 | Some(ParameterRepresentation::Simple { 117 | schema: ObjectOrReference::Object(_), 118 | }) => panic!("Inline parameter schemas are currently not supported"), 119 | Some(ParameterRepresentation::Content { .. }) => { 120 | panic!("Content-type specific parameters are currently not supported") 121 | } //TODO Implement 122 | None => panic!("Schema should contain a (rust) path to the type"), 123 | } 124 | .expect("failed to lex path to the type of parameter?"); 125 | q!( 126 | Vars { 127 | Type: ty, 128 | name_v: ¶m.name, 129 | location_v: quote_location(param.location), 130 | required_v, 131 | }, 132 | { 133 | rweb::openapi::ObjectOrReference::Object(rweb::openapi::Parameter { 134 | name: rweb::rt::Cow::Borrowed(name_v), 135 | location: location_v, 136 | required: required_v, 137 | representation: Some(rweb::openapi::ParameterRepresentation::Simple { 138 | schema: ::describe(__collector.components()), 139 | }), 140 | ..Default::default() 141 | }) 142 | } 143 | ) 144 | .parse() 145 | } 146 | 147 | fn quote_response(r: &Response) -> Expr { 148 | if let Some(irim) = r.content.get("rweb/intermediate") { 149 | if let Some(ObjectOrReference::Ref { ref_path }) = &irim.schema { 150 | let aschema_v: TokenStream = ref_path.parse().unwrap(); 151 | return q!( 152 | Vars { 153 | aschema_v, 154 | description_v: &r.description 155 | }, 156 | { 157 | (|| { 158 | let mut resp = 159 | ::describe_responses( 160 | __collector.components(), 161 | ) 162 | .into_iter() 163 | .next() 164 | .map(|(_, r)| r) 165 | .unwrap_or_else(|| Default::default()); 166 | resp.description = rweb::rt::Cow::Borrowed(description_v); 167 | resp 168 | })() 169 | } 170 | ) 171 | .parse(); 172 | } 173 | } 174 | //TODO headers, links 175 | let content_v: Punctuated = quote_str_indexmap!(r.content, quote_mediatype); 176 | q!( 177 | Vars { 178 | description_v: &r.description, 179 | content_v 180 | }, 181 | { 182 | rweb::openapi::Response { 183 | description: rweb::rt::Cow::Borrowed(description_v), 184 | content: rweb::rt::indexmap! {content_v}, 185 | ..Default::default() 186 | } 187 | } 188 | ) 189 | .parse() 190 | } 191 | 192 | fn quote_mediatype(m: &MediaType) -> Expr { 193 | //TODO examples, encoding 194 | let schema_v = quote_option(m.schema.as_ref().map(quote_schema_or_ref)); 195 | q!(Vars { schema_v }, { 196 | rweb::openapi::MediaType { 197 | schema: schema_v, 198 | ..Default::default() 199 | } 200 | }) 201 | .parse() 202 | } 203 | 204 | fn quote_schema_or_ref(ros: &ObjectOrReference) -> TokenStream { 205 | let r#ref = match ros { 206 | ObjectOrReference::Ref { ref_path } => ref_path, 207 | ObjectOrReference::Object(schema) => &schema.ref_path, 208 | }; 209 | 210 | r#ref 211 | .parse::() 212 | .expect("failed to lex path to type") 213 | } 214 | 215 | fn quote_location(l: Location) -> Quote { 216 | match l { 217 | Location::Query => q!({ rweb::openapi::Location::Query }), 218 | Location::Header => q!({ rweb::openapi::Location::Header }), 219 | Location::Path => q!({ rweb::openapi::Location::Path }), 220 | Location::FormData => q!({ rweb::openapi::Location::FormData }), 221 | } 222 | } 223 | 224 | pub fn parse(path: &str, sig: &Signature, attrs: &mut Vec) -> Operation { 225 | let mut op = Operation::default(); 226 | let mut has_description = false; 227 | 228 | for segment in path.split('/').filter(|s| !s.is_empty()) { 229 | if !segment.starts_with('{') { 230 | continue; 231 | } 232 | 233 | let var = &segment[1..segment.len() - 1]; 234 | if let Some(ty) = find_ty(sig, var) { 235 | op.parameters.push(ObjectOrReference::Object(Parameter { 236 | name: Cow::Owned(var.to_string()), 237 | location: Location::Path, 238 | required: Some(true), 239 | representation: Some(ParameterRepresentation::Simple { 240 | schema: ObjectOrReference::Object(Schema { 241 | ref_path: Cow::Owned(ty.dump().to_string()), 242 | ..Default::default() 243 | }), 244 | }), 245 | ..Default::default() 246 | })); 247 | } 248 | } 249 | 250 | attrs.retain(|attr| { 251 | if attr.path.is_ident("openapi") { 252 | // tags("foo", "bar", "baz) 253 | 254 | let configs = parse2::>>(attr.tokens.clone()) 255 | .expect("openapi config is invalid") 256 | .inner 257 | .inner; 258 | 259 | for config in configs { 260 | if config.path().is_ident("id") { 261 | assert!( 262 | op.operation_id.is_empty(), 263 | "#[openapi]: Duplicate operation id detected" 264 | ); 265 | match config { 266 | Meta::NameValue(v) => match v.lit { 267 | Lit::Str(s) => op.operation_id = Cow::Owned(s.value()), 268 | _ => panic!("#[openapi]: invalid operation id"), 269 | }, 270 | _ => panic!("Correct usage: #[openapi(id = \"foo\")]"), 271 | } 272 | } else if config.path().is_ident("description") { 273 | match config { 274 | Meta::NameValue(v) => match v.lit { 275 | Lit::Str(s) => { 276 | op.description = Cow::Owned(s.value()); 277 | has_description = true; 278 | } 279 | _ => panic!("#[openapi]: invalid operation summary"), 280 | }, 281 | _ => panic!("Correct usage: #[openapi(summary = \"foo\")]"), 282 | } 283 | } else if config.path().is_ident("summary") { 284 | match config { 285 | Meta::NameValue(v) => match v.lit { 286 | Lit::Str(s) => op.summary = Cow::Owned(s.value()), 287 | _ => panic!("#[openapi]: invalid operation summary"), 288 | }, 289 | _ => panic!("Correct usage: #[openapi(summary = \"foo\")]"), 290 | } 291 | } else if config.path().is_ident("tags") { 292 | match config { 293 | Meta::List(l) => { 294 | for tag in l.nested { 295 | match tag { 296 | NestedMeta::Lit(v) => match v { 297 | Lit::Str(s) => op.tags.push(Cow::Owned(s.value())), 298 | _ => panic!("#[openapi]: tag should be a string literal"), 299 | }, 300 | _ => panic!("Correct usage: #[openapi(tags(\"foo\" ,\"bar\")]"), 301 | } 302 | } 303 | } 304 | _ => panic!("Correct usage: #[openapi(tags(\"foo\" ,\"bar\")]"), 305 | } 306 | } else if config.path().is_ident("response") { 307 | macro_rules! invalid_usage { 308 | () => { 309 | panic!( 310 | "Correct usage: #[openapi(response(code = \"409\", description = \ 311 | \"foo already exists\")]" 312 | ) 313 | }; 314 | } 315 | let mut code: Option = None; 316 | let mut description: Option = None; 317 | let mut schema: Option = None; 318 | match config { 319 | Meta::List(l) => { 320 | for tag in l.nested { 321 | match tag { 322 | NestedMeta::Meta(Meta::NameValue(v)) => match v.lit { 323 | Lit::Str(s) => { 324 | if v.path.is_ident("code") { 325 | code = Some(s.value()) 326 | } else if v.path.is_ident("description") { 327 | description = Some(s.value()) 328 | } else if v.path.is_ident("schema") { 329 | schema = Some(s.value()) 330 | } else { 331 | invalid_usage!() 332 | } 333 | } 334 | Lit::Int(i) => { 335 | if i.base10_parse::().is_ok() { 336 | code = Some(i.to_string()) 337 | } else { 338 | invalid_usage!() 339 | } 340 | } 341 | _ => invalid_usage!(), 342 | }, 343 | _ => invalid_usage!(), 344 | } 345 | } 346 | match (code, description) { 347 | (Some(c), Some(d)) => { 348 | match op.responses.get_mut(&Cow::Owned(c.clone())) { 349 | Some(resp) => { 350 | resp.description = Cow::Owned(d); 351 | } 352 | None => { 353 | op.responses.insert( 354 | Cow::Owned(c.clone()), 355 | Response { 356 | description: Cow::Owned(d), 357 | ..Default::default() 358 | }, 359 | ); 360 | } 361 | }; 362 | if let Some(s) = schema { 363 | op.responses 364 | .get_mut(&Cow::Owned(c.clone())) 365 | .unwrap() 366 | .content 367 | .insert( 368 | Cow::Borrowed("rweb/intermediate"), 369 | MediaType { 370 | schema: Some(ObjectOrReference::Ref { 371 | ref_path: Cow::Owned(s), 372 | }), 373 | ..Default::default() 374 | }, 375 | ); 376 | } 377 | } 378 | _ => invalid_usage!(), 379 | } 380 | } 381 | _ => invalid_usage!(), 382 | } 383 | } else { 384 | panic!("Unknown openapi config `{}`", config.dump()) 385 | } 386 | } 387 | 388 | return false; 389 | } 390 | 391 | if attr.path.is_ident("doc") && !has_description { 392 | let s: EqStr = parse2(attr.tokens.clone()).expect("failed to parse comments"); 393 | if !op.description.is_empty() { 394 | op.description.to_mut().push(' '); 395 | } 396 | op.description 397 | .to_mut() 398 | .push_str(&s.value.value().trim_start()); 399 | // Preserve comments 400 | return true; 401 | } 402 | 403 | true 404 | }); 405 | 406 | op 407 | } 408 | -------------------------------------------------------------------------------- /macros/src/parse.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | parenthesized, 3 | parse::{Parse, ParseStream}, 4 | punctuated::Punctuated, 5 | LitStr, Token, 6 | }; 7 | 8 | pub(crate) struct KeyValue { 9 | pub key: K, 10 | _eq: Token![=], 11 | pub value: V, 12 | } 13 | 14 | impl Parse for KeyValue 15 | where 16 | K: Parse, 17 | V: Parse, 18 | { 19 | fn parse(input: ParseStream) -> syn::parse::Result { 20 | Ok(KeyValue { 21 | key: input.parse()?, 22 | _eq: input.parse()?, 23 | value: input.parse()?, 24 | }) 25 | } 26 | } 27 | 28 | /// A node wrapped with paren. 29 | pub(crate) struct Paren { 30 | pub inner: T, 31 | } 32 | 33 | impl Parse for Paren 34 | where 35 | T: Parse, 36 | { 37 | fn parse(input: ParseStream) -> syn::parse::Result { 38 | let content; 39 | parenthesized!(content in input); 40 | Ok(Paren { 41 | inner: content.parse()?, 42 | }) 43 | } 44 | } 45 | 46 | pub(crate) struct Delimited { 47 | pub inner: Punctuated, 48 | } 49 | 50 | impl Parse for Delimited 51 | where 52 | T: Parse, 53 | { 54 | fn parse(input: ParseStream) -> syn::parse::Result { 55 | Ok(Delimited { 56 | inner: Punctuated::parse_separated_nonempty(input)?, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /macros/src/path.rs: -------------------------------------------------------------------------------- 1 | use pmutil::q; 2 | use proc_macro2::TokenStream; 3 | use syn::{ 4 | parse_quote::parse, punctuated::Punctuated, Expr, FnArg, LitStr, Pat, Signature, Token, Type, 5 | }; 6 | 7 | pub fn find_ty<'a>(sig: &'a Signature, name: &str) -> Option<&'a Type> { 8 | sig.inputs 9 | .iter() 10 | .filter_map(|arg| match arg { 11 | FnArg::Typed(ty) => match *ty.pat { 12 | Pat::Ident(ref i) if i.ident == name => Some(&*ty.ty), 13 | _ => None, 14 | }, 15 | 16 | _ => None, 17 | }) 18 | .next() 19 | } 20 | 21 | /// 22 | /// - `sig`: Should be [Some] only if path parameters are allowed 23 | pub fn compile( 24 | base: Option, 25 | path: TokenStream, 26 | sig: Option<&Signature>, 27 | end: bool, 28 | ) -> (Expr, Vec<(String, usize)>) { 29 | let path: LitStr = parse(path); 30 | let path = path.value(); 31 | assert!(path.starts_with('/'), "Path should start with /"); 32 | assert!( 33 | !path.contains("//"), 34 | "A path containing `//` doesn't make sense" 35 | ); 36 | 37 | let mut exprs: Punctuated = Default::default(); 38 | // Set base values 39 | exprs.extend(base); 40 | let mut vars = vec![]; 41 | 42 | // Filter empty segments before iterating over them. 43 | // Mainly it will come from the required path in the beginning / but could also 44 | // come from the end / Example: #[get("/{word}")] or #[get("/{word}/")] with 45 | // the `/` before and after `{word}` 46 | let segments: Vec<&str> = path 47 | .split('/') 48 | .into_iter() 49 | .filter(|x| !x.is_empty()) 50 | .collect(); 51 | for segment in segments { 52 | let expr = if segment.starts_with('{') { 53 | // Example if {word} we only want to extract `word` here 54 | let name = &segment[1..segment.len() - 1]; 55 | if let Some(sig) = sig { 56 | let ty = sig 57 | .inputs 58 | .iter() 59 | .enumerate() 60 | .filter_map(|(idx, arg)| match arg { 61 | FnArg::Typed(ty) => match *ty.pat { 62 | // Here if we find a Pat::Ident we get i: &PatIdent and i.ident is the 63 | // parameter in the route fn. 64 | // I.e dyn_reply(word: String), this would be named: `word` and we 65 | // compare it to the segment name mentioned above. 66 | // If they match: 67 | // We uses it and adds to our variables. 68 | // else 69 | // We will panic later. 70 | Pat::Ident(ref i) if i.ident == name => { 71 | vars.push((name.to_string(), idx)); 72 | Some(&ty.ty) 73 | } 74 | _ => None, 75 | }, 76 | 77 | _ => None, 78 | }) 79 | .next() 80 | .unwrap_or_else(|| panic!("failed to find parameter named `{}`", name)); 81 | 82 | q!(Vars { ty }, { rweb::filters::path::param::() }) 83 | } else { 84 | panic!("path parameters are not allowed here (currently)") 85 | } 86 | } else { 87 | q!(Vars { segment }, { rweb::filters::path::path(segment) }) 88 | }; 89 | 90 | if exprs.is_empty() { 91 | exprs.push(q!(Vars { expr }, { expr }).parse()); 92 | } else { 93 | exprs.push(q!(Vars { expr }, { and(expr) }).parse()); 94 | } 95 | } 96 | 97 | if end { 98 | exprs.push(q!({ and(rweb::filters::path::end()) }).parse()); 99 | } 100 | 101 | (q!(Vars { exprs }, { exprs }).parse(), vars) 102 | } 103 | #[cfg(test)] 104 | mod tests { 105 | use super::*; 106 | use quote::quote; 107 | 108 | #[test] 109 | fn should_work() { 110 | let path = quote!("/ping"); 111 | compile(None, path, None, false); 112 | } 113 | #[test] 114 | #[should_panic(expected = "Path should start with /")] 115 | fn should_panic_if_path_doesnt_start_with_slash() { 116 | let path = quote! {"{word}"}; 117 | compile(None, path, None, false); 118 | } 119 | #[test] 120 | #[should_panic(expected = "A path containing `//` doesn't make sense")] 121 | fn should_panic_if_path_contains_slash_slash() { 122 | let path = quote! {"/{word}//"}; 123 | compile(None, path, None, false); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /macros/src/route/fn_attr.rs: -------------------------------------------------------------------------------- 1 | use super::ParenTwoValue; 2 | use crate::parse::{Delimited, Paren}; 3 | use pmutil::{q, ToTokensExt}; 4 | use syn::{parse2, Attribute, Expr, Meta, MetaNameValue}; 5 | 6 | /// Handle attributes on fn item like `#[header(ContentType = 7 | /// "application/json")]` 8 | pub fn compile_fn_attrs(mut base: Expr, attrs: &mut Vec, emitted_map: bool) -> Expr { 9 | attrs.retain(|attr| { 10 | if attr.path.is_ident("header") { 11 | let t: ParenTwoValue = parse2(attr.tokens.clone()).expect( 12 | "failed to parser header. Please provide it like #[header(\"ContentType\", \ 13 | \"application/json\")]", 14 | ); 15 | 16 | base = q!( 17 | Vars { 18 | base: &base, 19 | k: t.key, 20 | v: t.value 21 | }, 22 | { base.and(rweb::header::exact_ignore_case(k, v)) } 23 | ) 24 | .parse(); 25 | return false; 26 | } 27 | 28 | if attr.path.is_ident("body_size") { 29 | let meta = parse2::>(attr.tokens.clone()) 30 | .expect("Correct usage: #[body_size(max = \"8192\")]") 31 | .inner; 32 | 33 | if meta.path.is_ident("max") { 34 | let v = meta.lit.dump().to_string(); 35 | let mut value = &*v; 36 | if value.starts_with('"') { 37 | value = &value[1..value.len() - 1]; 38 | } 39 | let tts: proc_macro2::TokenStream = value.parse().unwrap_or_else(|err| { 40 | panic!( 41 | "#[body_size]: failed to parse value of max as number: {:?}", 42 | err 43 | ) 44 | }); 45 | 46 | base = q!( 47 | Vars { 48 | base: &base, 49 | v: tts 50 | }, 51 | { base.and(rweb::filters::body::content_length_limit(v)) } 52 | ) 53 | .parse(); 54 | return false; 55 | } 56 | 57 | panic!("Unknown configuration {} for #[body_size]", meta.dump()) 58 | } 59 | 60 | if attr.path.is_ident("cors") && emitted_map { 61 | let correct_usage = 62 | "Correct usage:\n#[cors(origins(\"example.com\", \"your.site.com\"), methods(get, \ 63 | post), headers(\"accept\"), max_age = 600)]\nNote: origins(\"*\") can be used to \ 64 | indicate cors is allowed for all origin. \nNote: you can omit methods to allow \ 65 | all methods.\nNote: you can omit headers to use the default behavior.\nNote: you \ 66 | can omit max_age to use the default value"; 67 | 68 | let configs = parse2::>>(attr.tokens.clone()) 69 | .expect(correct_usage) 70 | .inner 71 | .inner; 72 | 73 | let mut cors_expr: Expr = q!({ rweb::filters::cors::cors() }).parse(); 74 | 75 | for config in configs { 76 | match config { 77 | Meta::Path(p) => unimplemented!("Path meta: {}\n{}", p.dump(), correct_usage), 78 | Meta::List(l) => { 79 | if l.path.is_ident("origins") { 80 | for origin in l.nested { 81 | // TODO: More verification 82 | let is_wildcard = origin.dump().to_string() == "\"*\""; 83 | 84 | if is_wildcard { 85 | cors_expr = 86 | q!(Vars { cors_expr }, { cors_expr.allow_any_origin() }) 87 | .parse(); 88 | } else { 89 | cors_expr = q!(Vars { cors_expr, origin }, { 90 | cors_expr.allow_origin(origin) 91 | }) 92 | .parse(); 93 | } 94 | } 95 | } else if l.path.is_ident("methods") { 96 | // TODO: More verification (namely string literal) 97 | 98 | for method in l.nested { 99 | cors_expr = q!(Vars { cors_expr, method }, { 100 | cors_expr.allow_method(stringify!(method)) 101 | }) 102 | .parse(); 103 | } 104 | } else if l.path.is_ident("headers") { 105 | for header in l.nested { 106 | cors_expr = q!(Vars { cors_expr, header }, { 107 | cors_expr.allow_header(header) 108 | }) 109 | .parse(); 110 | } 111 | } else { 112 | panic!("Unknown config: `{}`\n{}", l.dump(), correct_usage) 113 | } 114 | } 115 | Meta::NameValue(n) => { 116 | if n.path.is_ident("max_age") { 117 | cors_expr = q!( 118 | Vars { 119 | cors_expr, 120 | v: n.lit 121 | }, 122 | { cors_expr.max_age(v) } 123 | ) 124 | .parse(); 125 | } else { 126 | panic!("Unknown config: `{}`\n{}", n.dump(), correct_usage) 127 | } 128 | } 129 | } 130 | } 131 | 132 | base = q!( 133 | Vars { 134 | base: &base, 135 | cors_expr 136 | }, 137 | { base.with(cors_expr.build()) } 138 | ) 139 | .parse(); 140 | 141 | return false; 142 | } 143 | 144 | true 145 | }); 146 | 147 | base 148 | } 149 | -------------------------------------------------------------------------------- /macros/src/route/mod.rs: -------------------------------------------------------------------------------- 1 | use pmutil::{q, Quote, ToTokensExt}; 2 | use proc_macro2::TokenStream; 3 | use syn::{ 4 | parenthesized, 5 | parse::{Parse, ParseStream}, 6 | parse_quote::parse, 7 | punctuated::Punctuated, 8 | visit::Visit, 9 | Block, Expr, ItemFn, LitStr, ReturnType, Signature, Token, Type, TypeImplTrait, Visibility, 10 | }; 11 | 12 | pub mod fn_attr; 13 | pub mod param; 14 | 15 | /// An eq token followed by literal string 16 | pub(crate) struct EqStr { 17 | _eq: Token![=], 18 | pub value: LitStr, 19 | } 20 | 21 | impl Parse for EqStr { 22 | fn parse(input: ParseStream) -> Result { 23 | Ok(EqStr { 24 | _eq: input.parse()?, 25 | value: input.parse()?, 26 | }) 27 | } 28 | } 29 | 30 | /// An eq token followed by literal string 31 | pub(crate) struct ParenTwoValue { 32 | key: LitStr, 33 | _eq: Token![,], 34 | value: LitStr, 35 | } 36 | 37 | impl Parse for ParenTwoValue { 38 | fn parse(input: ParseStream) -> Result { 39 | let content; 40 | parenthesized!(content in input); 41 | Ok(ParenTwoValue { 42 | key: content.parse()?, 43 | _eq: content.parse()?, 44 | value: content.parse()?, 45 | }) 46 | } 47 | } 48 | 49 | pub fn compile_route( 50 | method: Option, 51 | path: TokenStream, 52 | f: TokenStream, 53 | ) -> proc_macro::TokenStream { 54 | let mut f: ItemFn = parse(f); 55 | let sig = &f.sig; 56 | let mut data_inputs: Punctuated<_, Token![,]> = Default::default(); 57 | 58 | // Apply method filter 59 | let expr: Expr = if let Some(ref method) = method { 60 | q!( 61 | Vars { 62 | http_method: method, 63 | }, 64 | { rweb::filters::method::http_method() } 65 | ) 66 | .parse() 67 | } else { 68 | q!({ rweb::filters::any() }).parse() 69 | }; 70 | 71 | let (mut expr, vars) = crate::path::compile(Some(expr), path.clone(), Some(sig), true); 72 | let path: LitStr = parse(path); 73 | let path = path.value(); 74 | 75 | let (handler_fn, from_req_types) = { 76 | let (e, inputs, from_req_types) = 77 | param::compile(expr, &f.sig, &mut data_inputs, vars, true); 78 | expr = e; 79 | ( 80 | ItemFn { 81 | attrs: Default::default(), 82 | vis: Visibility::Inherited, 83 | 84 | sig: Signature { 85 | // asyncness: None, 86 | inputs, 87 | ..f.sig.clone() 88 | }, 89 | block: f.block, 90 | }, 91 | from_req_types, 92 | ) 93 | }; 94 | 95 | let should_use_impl_trait = 96 | sig.asyncness.is_some() || f.attrs.iter().any(|attr| attr.path.is_ident("cors")); 97 | 98 | let expr = fn_attr::compile_fn_attrs(expr, &mut f.attrs, false); 99 | 100 | let expr = if sig.asyncness.is_some() { 101 | q!( 102 | Vars { 103 | handler: &sig.ident, 104 | expr 105 | }, 106 | { expr.and_then(handler) } 107 | ) 108 | } else { 109 | q!( 110 | Vars { 111 | handler: &sig.ident, 112 | expr 113 | }, 114 | { expr.map(handler) } 115 | ) 116 | } 117 | .parse::(); 118 | 119 | let mut expr = fn_attr::compile_fn_attrs(expr, &mut f.attrs, true); 120 | 121 | let ret = if should_use_impl_trait { 122 | q!((impl rweb::Reply)).dump() 123 | } else { 124 | match sig.output { 125 | ReturnType::Default => panic!("http handler should return type"), 126 | ReturnType::Type(_, ref ty) => ty.dump(), 127 | } 128 | }; 129 | 130 | if cfg!(feature = "openapi") { 131 | let op = crate::openapi::parse(&path, sig, &mut f.attrs); 132 | let op = crate::openapi::quote_op(op); 133 | 134 | let mut op_body: Block = q!(Vars { op }, { 135 | { 136 | #[allow(unused_mut)] 137 | let mut v = op; 138 | } 139 | }) 140 | .parse(); 141 | 142 | for from_req in from_req_types { 143 | op_body.stmts.push( 144 | q!(Vars { Type: &from_req }, { 145 | rweb::openapi::Collector::add_request_type_to::(__collector, &mut v); 146 | }) 147 | .parse(), 148 | ); 149 | } 150 | 151 | match sig.output { 152 | ReturnType::Default => panic!("http handlers should have return type"), 153 | ReturnType::Type(_, ref ty) => { 154 | if !contains_impl_trait(&**ty) { 155 | op_body.stmts.push( 156 | q!(Vars { Type: ty }, { 157 | rweb::openapi::Collector::add_response_to::(__collector, &mut v); 158 | }) 159 | .parse(), 160 | ); 161 | } 162 | } 163 | } 164 | 165 | op_body.stmts.push( 166 | q!( 167 | Vars { 168 | path: &path, 169 | http_method: method, 170 | }, 171 | { 172 | __collector.add(path, rweb::openapi::http_methods::http_method(), v); 173 | } 174 | ) 175 | .parse(), 176 | ); 177 | 178 | expr = if cfg!(feature = "boxed") { 179 | q!(Vars { expr }, { expr.boxed() }).parse() 180 | } else { 181 | expr 182 | }; 183 | 184 | expr = q!(Vars { expr, op_body }, { 185 | rweb::openapi::with(|__collector: Option<&mut rweb::openapi::Collector>| { 186 | if let Some(__collector) = __collector { 187 | op_body 188 | } 189 | 190 | expr 191 | }) 192 | }) 193 | .parse(); 194 | } 195 | 196 | let mut outer = if cfg!(feature = "boxed") { 197 | q!( 198 | Vars { 199 | expr, 200 | handler: &sig.ident, 201 | Ret: ret, 202 | handler_fn, 203 | }, 204 | { 205 | fn handler( 206 | ) -> rweb::filters::BoxedFilter<(Ret,)> { 207 | use rweb::Filter; 208 | 209 | handler_fn 210 | 211 | expr 212 | } 213 | } 214 | ) 215 | .parse::() 216 | } else { 217 | q!( 218 | Vars { 219 | expr, 220 | handler: &sig.ident, 221 | Ret: ret, 222 | handler_fn, 223 | }, 224 | { 225 | fn handler( 226 | ) -> impl rweb::Filter 227 | + rweb::rt::Clone { 228 | use rweb::Filter; 229 | 230 | handler_fn 231 | 232 | expr 233 | } 234 | } 235 | ) 236 | .parse::() 237 | }; 238 | 239 | outer.vis = f.vis; 240 | outer.sig = Signature { 241 | inputs: data_inputs, 242 | ..outer.sig 243 | }; 244 | 245 | outer.dump().into() 246 | } 247 | 248 | fn contains_impl_trait(ty: &Type) -> bool { 249 | struct Visitor(bool); 250 | impl<'a> syn::visit::Visit<'a> for Visitor { 251 | fn visit_type_impl_trait(&mut self, _: &TypeImplTrait) { 252 | self.0 = true; 253 | } 254 | } 255 | 256 | let mut v = Visitor(false); 257 | 258 | v.visit_type(ty); 259 | 260 | v.0 261 | } 262 | -------------------------------------------------------------------------------- /macros/src/route/param.rs: -------------------------------------------------------------------------------- 1 | use crate::route::EqStr; 2 | use pmutil::{q, ToTokensExt}; 3 | use proc_macro2::TokenStream; 4 | use std::collections::HashSet; 5 | use syn::{ 6 | parse_quote::parse, punctuated::Punctuated, Attribute, Expr, FnArg, Pat, Path, Signature, 7 | Token, Type, 8 | }; 9 | 10 | /// Returns (expr, actual_inputs_of_handler, from_request_types) 11 | pub fn compile( 12 | mut expr: Expr, 13 | sig: &Signature, 14 | data_inputs: &mut Punctuated, 15 | path_vars: Vec<(String, usize)>, 16 | insert_data_provider: bool, 17 | ) -> (Expr, Punctuated, Vec) { 18 | let mut path_params = HashSet::new(); 19 | let mut inputs = sig.inputs.clone(); 20 | let mut from_request_types = vec![]; 21 | 22 | { 23 | // Handle path parameters 24 | 25 | for (orig_idx, (name, idx)) in path_vars.into_iter().enumerate() { 26 | if path_params.contains(&orig_idx) { 27 | continue; 28 | } 29 | 30 | if let FnArg::Typed(pat) = &sig.inputs[idx] { 31 | match *pat.pat { 32 | Pat::Ident(ref i) if i.ident == name => { 33 | inputs[orig_idx] = sig.inputs[idx].clone(); 34 | inputs[idx] = sig.inputs[orig_idx].clone(); 35 | path_params.insert(orig_idx); 36 | } 37 | _ => {} 38 | } 39 | } 40 | } 41 | } 42 | 43 | let inputs = { 44 | let mut actual_inputs = vec![]; 45 | 46 | // Handle annotated parameters. 47 | for (idx, mut i) in inputs.into_pairs().enumerate() { 48 | if path_params.contains(&idx) { 49 | actual_inputs.push(i); 50 | continue; 51 | } 52 | 53 | let cloned_i = i.clone(); 54 | 55 | match i.value_mut() { 56 | FnArg::Receiver(_) => continue, 57 | FnArg::Typed(ref mut pat) => { 58 | if pat.attrs.is_empty() { 59 | // If there's no attribute, it's type should implement FromRequest 60 | 61 | actual_inputs.push(cloned_i); 62 | from_request_types.push(*pat.ty.clone()); 63 | expr = q!(Vars { expr, T: &pat.ty }, { 64 | expr.and(::new()) 65 | }) 66 | .parse(); 67 | 68 | continue; 69 | } 70 | 71 | let is_rweb_attr = pat.attrs.iter().any(is_rweb_arg_attr); 72 | if !is_rweb_attr { 73 | // We don't care about this parameter. 74 | actual_inputs.push(i); 75 | continue; 76 | } 77 | 78 | if pat.attrs.len() != 1 { 79 | // TODO: Support cfg? 80 | panic!("rweb currently support only one attribute on a parameter") 81 | } 82 | 83 | let attr = pat.attrs.get(0).cloned().unwrap(); 84 | pat.attrs = vec![]; 85 | 86 | if attr.path.is_ident("form") { 87 | expr = q!(Vars { expr }, { expr.and(rweb::filters::body::form()) }).parse() 88 | } else if attr.path.is_ident("json") { 89 | expr = q!(Vars { expr }, { expr.and(rweb::filters::body::json()) }).parse() 90 | } else if attr.path.is_ident("body") { 91 | expr = q!(Vars { expr }, { expr.and(rweb::filters::body::bytes()) }).parse() 92 | } else if attr.path.is_ident("query") { 93 | expr = q!(Vars { expr }, { expr.and(rweb::filters::query::raw()) }).parse() 94 | } else if attr.path.is_ident("cookie") { 95 | if let Ok(cookie_name) = syn::parse2::(attr.tokens.clone()) { 96 | expr = q!( 97 | Vars { 98 | expr, 99 | cookie_name: cookie_name.value 100 | }, 101 | { expr.and(rweb::filters::cookie::cookie(cookie_name)) } 102 | ) 103 | .parse(); 104 | } else { 105 | panic!("#[cookie = \"foo\"] is used incorrectly") 106 | } 107 | } else if attr.path.is_ident("header") { 108 | if let Ok(header_name) = syn::parse2::(attr.tokens.clone()) { 109 | expr = q!( 110 | Vars { 111 | expr, 112 | header_name: header_name.value 113 | }, 114 | { expr.and(rweb::filters::header::header(header_name)) } 115 | ) 116 | .parse(); 117 | } else { 118 | panic!( 119 | "invalid usage of header: {}\nCorrect usage is#[header = \ 120 | \"accpet\"]", 121 | attr.tokens.dump() 122 | ) 123 | } 124 | } else if attr.path.is_ident("filter") { 125 | let filter_path: EqStr = parse(attr.tokens.clone()); 126 | let filter_path = filter_path.value.value(); 127 | let tts: TokenStream = filter_path.parse().expect("failed tokenize"); 128 | let filter_path: Path = parse(tts); 129 | 130 | expr = q!(Vars { expr, filter_path }, { expr.and(filter_path()) }).parse(); 131 | } else if attr.path.is_ident("data") { 132 | let ident = match &*pat.pat { 133 | Pat::Ident(i) => &i.ident, 134 | _ => unimplemented!("#[data] with complex pattern"), 135 | }; 136 | 137 | if insert_data_provider { 138 | expr = q!(Vars { expr, ident }, { 139 | expr.and(rweb::rt::provider(ident)) 140 | }) 141 | .parse(); 142 | } 143 | 144 | data_inputs.push(i.value().clone()); 145 | } 146 | 147 | // Don't add unit type to argument list 148 | if let FnArg::Typed(pat) = i.value() { 149 | match &*pat.ty { 150 | Type::Tuple(tuple) if tuple.elems.is_empty() => continue, 151 | _ => {} 152 | } 153 | } 154 | 155 | actual_inputs.push(i); 156 | } 157 | } 158 | } 159 | 160 | actual_inputs.into_iter().collect() 161 | }; 162 | 163 | (expr, inputs, from_request_types) 164 | } 165 | 166 | fn is_rweb_arg_attr(a: &Attribute) -> bool { 167 | a.path.is_ident("json") 168 | || a.path.is_ident("form") 169 | || a.path.is_ident("body") 170 | || a.path.is_ident("query") 171 | || a.path.is_ident("cookie") 172 | || a.path.is_ident("header") 173 | || a.path.is_ident("filter") 174 | || a.path.is_ident("data") 175 | } 176 | -------------------------------------------------------------------------------- /macros/src/router.rs: -------------------------------------------------------------------------------- 1 | use crate::route::fn_attr::compile_fn_attrs; 2 | use pmutil::{q, Quote, ToTokensExt}; 3 | use proc_macro2::{Ident, TokenStream}; 4 | use syn::{ 5 | parse::{Parse, ParseStream}, 6 | parse2, 7 | punctuated::{Pair, Punctuated}, 8 | Error, Expr, FnArg, ItemFn, LitStr, Meta, Pat, Token, 9 | }; 10 | 11 | struct Input { 12 | path: LitStr, 13 | _comma: Token![,], 14 | services: Meta, 15 | } 16 | 17 | impl Parse for Input { 18 | fn parse(input: ParseStream) -> Result { 19 | Ok(Input { 20 | path: input.parse()?, 21 | _comma: input.parse()?, 22 | services: input.parse()?, 23 | }) 24 | } 25 | } 26 | 27 | pub fn router(attr: TokenStream, item: TokenStream) -> ItemFn { 28 | let mut f: ItemFn = parse2(item).expect("failed to parse input as a function item"); 29 | assert!( 30 | f.block.stmts.is_empty(), 31 | "#[router] function cannot have body" 32 | ); 33 | 34 | let router_name = &f.sig.ident; 35 | let vis = &f.vis; 36 | let mut data_inputs: Punctuated<_, Token![,]> = Default::default(); 37 | 38 | let attr: Input = parse2(attr).expect("failed to parse input as Input { path , service }"); 39 | 40 | let (expr, path_vars) = crate::path::compile(None, attr.path.dump(), None, false); 41 | let (expr, inputs, _) = 42 | crate::route::param::compile(expr, &f.sig, &mut data_inputs, path_vars, false); 43 | 44 | let mut exprs: Punctuated = Punctuated::default(); 45 | 46 | let args: Punctuated = data_inputs 47 | .pairs() 48 | .map(|pair| { 49 | let p = pair.punct().cloned(); 50 | let t = pair.value(); 51 | 52 | let t = match t { 53 | FnArg::Typed(pat) => match &*pat.pat { 54 | Pat::Ident(p) => p.ident.clone(), 55 | _ => unimplemented!("proper error reporting for non-ident #[data] input"), 56 | }, 57 | _ => unimplemented!("proper error reporting for non-ident #[data] input"), 58 | }; 59 | // 60 | Pair::new(t, p) 61 | }) 62 | .collect(); 63 | 64 | let mut expr = compile_fn_attrs(expr, &mut f.attrs, false); 65 | 66 | match attr.services { 67 | Meta::List(ref list) => { 68 | if list.path.is_ident("services") { 69 | for name in list.nested.iter() { 70 | if exprs.is_empty() { 71 | exprs.push(q!(Vars { name, args: &args }, { name(args) }).parse()); 72 | } else { 73 | exprs.push(q!(Vars { name, args: &args }, { or(name(args)) }).parse()); 74 | } 75 | } 76 | 77 | expr = q!(Vars { exprs, expr }, { expr.and(exprs) }).parse(); 78 | } else { 79 | panic!("Unknown path {}", list.path.dump()) 80 | } 81 | } 82 | 83 | _ => panic!("#[router(\"/path\", services(a, b, c,))] is correct usage"), 84 | } 85 | 86 | let mut expr = compile_fn_attrs(expr, &mut f.attrs, true); 87 | 88 | if cfg!(feature = "openapi") { 89 | let op = crate::openapi::parse(&attr.path.value(), &f.sig, &mut f.attrs); 90 | let tags: Punctuated = op 91 | .tags 92 | .iter() 93 | .map(|tag| { 94 | Pair::Punctuated( 95 | q!(Vars { tag }, { rweb::rt::Cow::Borrowed(tag) }), 96 | Default::default(), 97 | ) 98 | }) 99 | .collect(); 100 | 101 | expr = q!( 102 | Vars { 103 | tags, 104 | path: &attr.path, 105 | expr 106 | }, 107 | { 108 | rweb::openapi::with(|__collector: Option<&mut rweb::openapi::Collector>| { 109 | if let Some(__collector) = __collector { 110 | __collector.with_appended_prefix(path, vec![tags], || expr) 111 | } else { 112 | expr 113 | } 114 | }) 115 | } 116 | ) 117 | .parse(); 118 | } 119 | 120 | // TODO: Default handler 121 | let mut ret = q!(Vars { expr, router_name }, { 122 | fn router_name( 123 | ) -> impl Clone + rweb::Filter 124 | { 125 | use rweb::{rt::StatusCode, Filter}; 126 | 127 | expr 128 | } 129 | }) 130 | .parse::(); 131 | 132 | ret.attrs = f.attrs; 133 | ret.sig.inputs = inputs; 134 | ret.vis = vis.clone(); 135 | 136 | ret 137 | } 138 | -------------------------------------------------------------------------------- /macros/src/util.rs: -------------------------------------------------------------------------------- 1 | use pmutil::{synom_ext::FromSpan, ToTokensExt}; 2 | use proc_macro2::Span; 3 | use quote::quote; 4 | use syn::{punctuated::Pair, *}; 5 | 6 | pub fn call_site() -> T { 7 | T::from_span(Span::call_site()) 8 | } 9 | 10 | /// Extension trait for `ItemImpl` (impl block). 11 | pub trait ItemImplExt { 12 | /// Instead of 13 | /// 14 | /// ```rust,ignore 15 | /// let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 16 | /// 17 | /// let item: Item = Quote::new(def_site::()) 18 | /// .quote_with(smart_quote!( 19 | /// Vars { 20 | /// Type: type_name, 21 | /// impl_generics, 22 | /// ty_generics, 23 | /// where_clause, 24 | /// }, 25 | /// { 26 | /// impl impl_generics ::swc_common::AstNode for Type ty_generics 27 | /// where_clause {} 28 | /// } 29 | /// )).parse(); 30 | /// ``` 31 | /// 32 | /// You can use this like 33 | /// 34 | /// ```rust,ignore 35 | // let item = Quote::new(def_site::()) 36 | /// .quote_with(smart_quote!(Vars { Type: type_name }, { 37 | /// impl ::swc_common::AstNode for Type {} 38 | /// })) 39 | /// .parse::() 40 | /// .with_generics(input.generics); 41 | /// ``` 42 | fn with_generics(self, generics: Generics) -> Self; 43 | } 44 | 45 | impl ItemImplExt for ItemImpl { 46 | fn with_generics(mut self, mut generics: Generics) -> Self { 47 | // TODO: Check conflicting name 48 | 49 | let need_new_punct = !generics.params.empty_or_trailing(); 50 | if need_new_punct { 51 | generics.params.push_punct(call_site()); 52 | } 53 | 54 | // Respan 55 | if let Some(t) = generics.lt_token { 56 | self.generics.lt_token = Some(t) 57 | } 58 | if let Some(t) = generics.gt_token { 59 | self.generics.gt_token = Some(t) 60 | } 61 | 62 | let ty = self.self_ty; 63 | 64 | // Handle generics defined on struct, enum, or union. 65 | let mut item: ItemImpl = { 66 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 67 | let item = if let Some((ref polarity, ref path, ref for_token)) = self.trait_ { 68 | quote! { 69 | impl #impl_generics #polarity #path #for_token #ty #ty_generics #where_clause {} 70 | } 71 | } else { 72 | quote! { 73 | impl #impl_generics #ty #ty_generics #where_clause {} 74 | 75 | } 76 | }; 77 | parse(item.dump().into()) 78 | .unwrap_or_else(|err| panic!("with_generics failed: {}\n{}", err, item.dump())) 79 | }; 80 | 81 | // Handle generics added by proc-macro. 82 | item.generics 83 | .params 84 | .extend(self.generics.params.into_pairs()); 85 | match self.generics.where_clause { 86 | Some(WhereClause { 87 | ref mut predicates, .. 88 | }) => predicates.extend( 89 | generics 90 | .where_clause 91 | .into_iter() 92 | .flat_map(|wc| wc.predicates.into_pairs()), 93 | ), 94 | ref mut opt @ None => *opt = generics.where_clause, 95 | } 96 | 97 | ItemImpl { 98 | attrs: self.attrs, 99 | defaultness: self.defaultness, 100 | unsafety: self.unsafety, 101 | impl_token: self.impl_token, 102 | brace_token: self.brace_token, 103 | items: self.items, 104 | ..item 105 | } 106 | } 107 | } 108 | 109 | pub trait PairExt: Sized + Into> { 110 | fn map_item(self, op: F) -> Pair 111 | where 112 | F: FnOnce(T) -> NewItem, 113 | { 114 | match self.into() { 115 | Pair::Punctuated(t, p) => Pair::Punctuated(op(t), p), 116 | Pair::End(t) => Pair::End(op(t)), 117 | } 118 | } 119 | } 120 | 121 | impl PairExt for Pair {} 122 | -------------------------------------------------------------------------------- /src/docs.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::Spec; 2 | use warp::{filters::BoxedFilter, Filter, Reply}; 3 | 4 | /// Helper filter that exposes an openapi spec on the `/docs` endpoint. 5 | /// 6 | /// # Example - single use with data injection 7 | /// 8 | /// ```ignore 9 | /// let (spec, filter) = openapi::spec().build(|| index()); 10 | /// serve(filter.or(openapi_docs(spec))) 11 | /// .run(([127, 0, 0, 1], 3030)) 12 | /// .await; 13 | /// ``` 14 | pub fn openapi_docs(spec: Spec) -> BoxedFilter<(impl Reply,)> { 15 | let docs_openapi = warp::path("openapi.json").map(move || warp::reply::json(&spec.to_owned())); 16 | let docs = warp::path("docs").map(|| { 17 | warp::reply::html( 18 | r#" 19 | 20 | 21 | 22 | rweb 23 | 24 | 25 | 26 |
27 | 28 | 45 | 46 | 47 | "#, 48 | ) 49 | }); 50 | docs.or(docs_openapi).boxed() 51 | } 52 | -------------------------------------------------------------------------------- /src/factory.rs: -------------------------------------------------------------------------------- 1 | use futures::future::ok; 2 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 3 | #[cfg(feature = "multipart")] 4 | use warp::filters::multipart; 5 | #[cfg(feature = "websocket")] 6 | use warp::filters::ws::Ws; 7 | use warp::{ 8 | filters::BoxedFilter, 9 | reply::{json, Response}, 10 | Filter, Rejection, Reply, 11 | }; 12 | 13 | pub trait FromRequest: Sized { 14 | /// Extract should be `(Self,),` 15 | type Filter: Filter; 16 | 17 | /// It's true iff the type represents whole request body. 18 | /// 19 | /// It returns true for `Json` and `Form`. 20 | fn is_body() -> bool { 21 | false 22 | } 23 | 24 | /// It's true if the type is optional. 25 | /// 26 | /// It returns true for `Option`. 27 | fn is_optional() -> bool { 28 | false 29 | } 30 | 31 | /// It's true iff the type represents whole request query. 32 | /// 33 | /// It returns true for `Query`. 34 | fn is_query() -> bool { 35 | false 36 | } 37 | 38 | fn content_type() -> &'static str { 39 | "*/*" 40 | } 41 | 42 | fn new() -> Self::Filter; 43 | } 44 | 45 | impl FromRequest for Option 46 | where 47 | T: 'static + FromRequest + Send + Send, 48 | T::Filter: Send + Sync + Filter, 49 | { 50 | type Filter = BoxedFilter<(Option,)>; 51 | 52 | fn is_body() -> bool { 53 | T::is_body() 54 | } 55 | 56 | fn is_optional() -> bool { 57 | true 58 | } 59 | 60 | fn is_query() -> bool { 61 | T::is_query() 62 | } 63 | 64 | fn new() -> Self::Filter { 65 | T::new() 66 | .map(Some) 67 | .or_else(|_| ok::<_, Rejection>((None,))) 68 | .boxed() 69 | } 70 | } 71 | 72 | /// Represents request body or response. 73 | /// 74 | /// 75 | /// If it is in a parameter, content-type should be `application/json` and 76 | /// request body will be deserialized. 77 | /// 78 | /// If it is in a return type, response will contain a content type header with 79 | /// value `application/json`, and value is serialized. 80 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 81 | #[serde(transparent)] 82 | pub struct Json(T); 83 | 84 | impl Json { 85 | pub fn into_inner(self) -> T { 86 | self.0 87 | } 88 | } 89 | 90 | impl From for Json { 91 | #[inline] 92 | fn from(v: T) -> Self { 93 | Json(v) 94 | } 95 | } 96 | 97 | impl FromRequest for Json 98 | where 99 | T: 'static + Send + DeserializeOwned, 100 | { 101 | type Filter = BoxedFilter<(Json,)>; 102 | 103 | fn is_body() -> bool { 104 | true 105 | } 106 | 107 | fn content_type() -> &'static str { 108 | "application/json" 109 | } 110 | 111 | fn new() -> Self::Filter { 112 | warp::body::json().boxed() 113 | } 114 | } 115 | 116 | impl Reply for Json 117 | where 118 | T: Serialize + Send, 119 | { 120 | fn into_response(self) -> Response { 121 | json(&self.0).into_response() 122 | } 123 | } 124 | 125 | /// Represents a request body with `www-url-form-encoded` content type. 126 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] 127 | #[serde(transparent)] 128 | pub struct Form(T); 129 | 130 | impl Form { 131 | pub fn into_inner(self) -> T { 132 | self.0 133 | } 134 | } 135 | 136 | impl FromRequest for Form 137 | where 138 | T: 'static + Send + DeserializeOwned, 139 | { 140 | type Filter = BoxedFilter<(Form,)>; 141 | 142 | fn is_body() -> bool { 143 | true 144 | } 145 | 146 | fn content_type() -> &'static str { 147 | "x-www-form-urlencoded" 148 | } 149 | 150 | fn new() -> Self::Filter { 151 | warp::body::form().boxed() 152 | } 153 | } 154 | 155 | /// Represents all query parameters. 156 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] 157 | #[serde(transparent)] 158 | pub struct Query(T); 159 | 160 | impl Query { 161 | pub fn into_inner(self) -> T { 162 | self.0 163 | } 164 | } 165 | 166 | impl FromRequest for Query 167 | where 168 | T: 'static + Send + DeserializeOwned, 169 | { 170 | type Filter = BoxedFilter<(Query,)>; 171 | 172 | fn is_query() -> bool { 173 | true 174 | } 175 | 176 | fn new() -> Self::Filter { 177 | warp::query().boxed() 178 | } 179 | } 180 | 181 | #[cfg(feature = "websocket")] 182 | impl FromRequest for Ws { 183 | type Filter = BoxedFilter<(Ws,)>; 184 | 185 | fn new() -> Self::Filter { 186 | warp::ws().boxed() 187 | } 188 | } 189 | 190 | #[cfg(feature = "multipart")] 191 | impl FromRequest for multipart::FormData { 192 | type Filter = BoxedFilter<(Self,)>; 193 | 194 | fn new() -> Self::Filter { 195 | warp::multipart::form().boxed() 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A macro to convert a function to rweb handler. 2 | //! 3 | //! All parameters should satisfy one of the following. 4 | //! 5 | //! - Has a path parameter with same name. 6 | //! - Annotated with the annotations documented below. 7 | //! - Has a type which implements [FromRequest]. 8 | //! 9 | //! 10 | //! # Path parmeters 11 | //! 12 | //! 13 | //! # Attributes on function item 14 | //! 15 | //! ## `#[header("content-type", "application/json")]` 16 | //! 17 | //! Make a route matches only if value of the header matches provided value. 18 | //! 19 | //! ```rust 20 | //! use rweb::*; 21 | //! 22 | //! #[get("/")] 23 | //! #[header("accept", "*/*")] 24 | //! fn routes() -> &'static str { 25 | //! "This route matches only if accept header is '*/*'" 26 | //! } 27 | //! 28 | //! fn main() { 29 | //! serve(routes()); 30 | //! } 31 | //! ``` 32 | //! 33 | //! ## `#[cors]` 34 | //! 35 | //! 36 | //! ```rust 37 | //! use rweb::*; 38 | //! 39 | //! #[get("/")] 40 | //! #[cors(origins("example.com"), max_age = 600)] 41 | //! fn cors_1() -> String { 42 | //! unreachable!() 43 | //! } 44 | //! 45 | //! #[get("/")] 46 | //! #[cors(origins("example.com"), methods(get), max_age = 600)] 47 | //! fn cors_2() -> String { 48 | //! unreachable!() 49 | //! } 50 | //! 51 | //! #[get("/")] 52 | //! #[cors(origins("*"), methods(get), max_age = 600)] 53 | //! fn cors_3() -> String { 54 | //! unreachable!() 55 | //! } 56 | //! 57 | //! #[get("/")] 58 | //! #[cors( 59 | //! origins("*"), 60 | //! methods(get, post, patch, delete), 61 | //! headers("accept"), 62 | //! max_age = 600 63 | //! )] 64 | //! fn cors_4() -> String { 65 | //! unreachable!() 66 | //! } 67 | //! ``` 68 | //! 69 | //! ## `#[body_size(max = "8192")]` 70 | //! ```rust 71 | //! use rweb::*; 72 | //! 73 | //! #[get("/")] 74 | //! #[body_size(max = "8192")] 75 | //! fn body_size() -> String { 76 | //! unreachable!() 77 | //! } 78 | //! ``` 79 | //! 80 | //! 81 | //! # Attributes on parameters 82 | //! 83 | //! ## `#[body]` 84 | //! 85 | //! Parses request body. Type is `bytes::Bytes`. 86 | //! ```rust 87 | //! use rweb::*; 88 | //! use http::Error; 89 | //! use bytes::Bytes; 90 | //! 91 | //! #[post("/body")] 92 | //! fn body(#[body] body: Bytes) -> Result { 93 | //! let _ = body; 94 | //! Ok(String::new()) 95 | //! } 96 | //! 97 | //! fn main() { 98 | //! serve(body()); 99 | //! } 100 | //! ``` 101 | //! 102 | //! ## `#[form]` 103 | //! Parses request body. `Content-Type` should be `x-www-form-urlencoded`. 104 | //! ```rust 105 | //! use rweb::*; 106 | //! use serde::Deserialize; 107 | //! 108 | //! #[derive(Deserialize)] 109 | //! struct LoginForm { 110 | //! id: String, 111 | //! password: String, 112 | //! } 113 | //! 114 | //! #[post("/form")] 115 | //! fn form(#[form] body: LoginForm) -> String { 116 | //! String::from("Ok") 117 | //! } 118 | //! 119 | //! fn main() { 120 | //! serve(form()); 121 | //! } 122 | //! ``` 123 | //! 124 | //! ## `#[json]` 125 | //! Parses request body. `Content-Type` should be `application/json`. 126 | //! ```rust 127 | //! use rweb::*; 128 | //! use serde::Deserialize; 129 | //! 130 | //! #[derive(Deserialize)] 131 | //! struct LoginForm { 132 | //! id: String, 133 | //! password: String, 134 | //! } 135 | //! 136 | //! #[post("/json")] 137 | //! fn json(#[json] body: LoginForm) -> String { 138 | //! String::from("Ok") 139 | //! } 140 | //! 141 | //! fn main() { 142 | //! serve(json()); 143 | //! } 144 | //! ``` 145 | //! 146 | //! Note that you can mix the order of parameters. 147 | //! ```rust 148 | //! use rweb::*; 149 | //! use serde::Deserialize; 150 | //! 151 | //! #[derive(Deserialize)] 152 | //! struct LoginForm { 153 | //! id: String, 154 | //! password: String, 155 | //! } 156 | //! 157 | //! #[get("/param/{a}/{b}")] 158 | //! fn body_between_path_params(a: u32, #[json] body: LoginForm, b: u32) -> 159 | //! String { assert_eq!(body.id, "TEST_ID"); 160 | //! assert_eq!(body.password, "TEST_PASSWORD"); 161 | //! (a + b).to_string() 162 | //! } 163 | //! 164 | //! fn main() { 165 | //! serve(body_between_path_params()); 166 | //! } 167 | //! ``` 168 | //! 169 | //! ## `#[query]` 170 | //! 171 | //! Parses query string. 172 | //! ```rust 173 | //! use rweb::*; 174 | //! 175 | //! #[get("/")] 176 | //! fn use_query(#[query] qs: String) -> String { 177 | //! qs 178 | //! } 179 | //! 180 | //! fn main() { 181 | //! serve(use_query()); 182 | //! } 183 | //! ``` 184 | //! 185 | //! ## `#[header = "header-name"]` 186 | //! Value of the header. 187 | //! ```rust 188 | //! use rweb::*; 189 | //! 190 | //! #[get("/")] 191 | //! fn ret_accept(#[header = "accept"] accept: String) -> String { 192 | //! accept 193 | //! } 194 | //! fn main() { 195 | //! serve(ret_accept()); 196 | //! } 197 | //! ``` 198 | //! 199 | //! ## `#[cookie = "cookie-name"]` 200 | //! Value of the header. 201 | //! ```rust 202 | //! use rweb::*; 203 | //! 204 | //! #[get("/")] 205 | //! fn cookie(#[header = "sess"] sess_id: String) -> String { 206 | //! sess_id 207 | //! } 208 | //! fn main() { 209 | //! serve(cookie()); 210 | //! } 211 | //! ``` 212 | //! 213 | //! ## `#[filter = "path_to_fn"]` 214 | //! Calls function. 215 | //! 216 | //! **Note**: If the callee returns `()`, you should use `()` as type. (Type 217 | //! alias is not allowed) 218 | //! ```rust 219 | //! use std::num::NonZeroU16; 220 | //! use rweb::*; 221 | //! use serde::Serialize; 222 | //! 223 | //! #[derive(Serialize)] 224 | //! struct Math { 225 | //! op: String, 226 | //! output: u16, 227 | //! } 228 | //! 229 | //! #[get("/math/{num}")] 230 | //! fn math(num: u16, #[filter = "div_by"] denom: NonZeroU16) -> impl Reply { 231 | //! rweb::reply::json(&Math { 232 | //! op: format!("{} / {}", num, denom), 233 | //! output: num / denom.get(), 234 | //! }) 235 | //! } 236 | //! 237 | //! fn div_by() -> impl Filter +Copy 238 | //! { rweb::header::("div-by").and_then(|n: u16| async move { 239 | //! if let Some(denom) = NonZeroU16::new(n) { 240 | //! Ok(denom) 241 | //! } else { 242 | //! Err(reject::custom(DivideByZero)) 243 | //! } 244 | //! }) 245 | //! } 246 | //! 247 | //! #[derive(Debug)] 248 | //! struct DivideByZero; 249 | //! 250 | //! impl reject::Reject for DivideByZero {} 251 | //! 252 | //! fn main() { 253 | //! serve(math()); 254 | //! } 255 | //! ``` 256 | //! 257 | //! ## `#[data]` 258 | //! ```rust 259 | //! use futures::lock::Mutex; 260 | //! use rweb::*; 261 | //! use std::sync::Arc; 262 | //! 263 | //! #[derive(Clone, Default)] 264 | //! struct Db { 265 | //! items: Arc>>, 266 | //! } 267 | //! 268 | //! #[get("/")] 269 | //! async fn index(#[data] db: Db) -> Result { 270 | //! let items = db.items.lock().await; 271 | //! 272 | //! Ok(items.len().to_string()) 273 | //! } 274 | //! 275 | //! fn main() { 276 | //! let db = Default::default(); 277 | //! serve(index(db)); 278 | //! } 279 | //! ``` 280 | //! 281 | //! # FromRequest 282 | //! ```rust 283 | //! use http::StatusCode; 284 | //! use rweb::{filters::BoxedFilter, *}; 285 | //! 286 | //! impl FromRequest for User { 287 | //! type Filter = BoxedFilter<(User,)>; 288 | //! 289 | //! fn new() -> Self::Filter { 290 | //! // In real world, you can use a header like Authorization 291 | //! header::("x-user-id").map(|id| User { id }).boxed() 292 | //! } 293 | //! } 294 | //! 295 | //! 296 | //! #[derive(Schema)] 297 | //! struct User { 298 | //! id: String, 299 | //! } 300 | //! 301 | //! #[get("/")] 302 | //! fn index(user: User) -> String { 303 | //! user.id 304 | //! } 305 | //! 306 | //! fn main() { 307 | //! serve(index()); 308 | //! } 309 | //! ``` 310 | //! 311 | //! 312 | //! # Guards 313 | //! ```rust 314 | //! use rweb::*; 315 | //! 316 | //! // This handler is invoked only if x-appengine-cron matches 1 (case insensitive). 317 | //! #[get("/")] 318 | //! #[header("X-AppEngine-Cron", "1")] 319 | //! fn gae_cron() -> String { 320 | //! String::new() 321 | //! } 322 | //! ``` 323 | //! 324 | //! # `#[router]` 325 | //! 326 | //! `#[router]` can be used to group routes. 327 | //! 328 | //! ## `#[data]` 329 | //! 330 | //! You can use `#[data]` with a router. 331 | //! ```rust 332 | //! use rweb::*; 333 | //! 334 | //! #[derive(Default, Clone)] 335 | //! struct Db {} 336 | //! 337 | //! #[get("/use")] 338 | //! fn use_db(#[data] _db: Db) -> String { 339 | //! String::new() 340 | //! } 341 | //! 342 | //! #[router("/data", services(use_db))] 343 | //! fn data_param(#[data] db: Db) {} 344 | //! ``` 345 | //! 346 | //! 347 | //! ## Guard 348 | //! ```rust 349 | //! use rweb::*; 350 | //! 351 | //! #[get("/")] 352 | //! fn admin_index() -> String { 353 | //! String::new() 354 | //! } 355 | //! 356 | //! #[get("/users")] 357 | //! fn admin_users() -> String { 358 | //! String::new() 359 | //! } 360 | //! 361 | //! #[router("/admin", services(admin_index, admin_users))] 362 | //! #[header("X-User-Admin", "1")] 363 | //! fn admin() {} 364 | //! ``` 365 | 366 | pub use self::factory::{Form, FromRequest, Json, Query}; 367 | pub use rweb_macros::{delete, get, head, options, patch, post, put, router, Schema}; 368 | pub use warp::{self, *}; 369 | 370 | #[cfg(feature = "openapi")] 371 | pub mod docs; 372 | #[cfg(feature = "openapi")] 373 | pub use self::docs::*; 374 | mod factory; 375 | #[cfg(feature = "openapi")] 376 | pub mod openapi; 377 | #[doc(hidden)] 378 | pub mod rt; 379 | 380 | pub mod routes; 381 | pub use self::routes::*; 382 | -------------------------------------------------------------------------------- /src/openapi/builder.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Builder for openapi v3 specification. 4 | #[derive(Debug, Clone, Default)] 5 | pub struct Builder { 6 | spec: Spec, 7 | path_prefix: String, 8 | } 9 | 10 | /// Crates a new specification builder 11 | #[inline] 12 | pub fn spec() -> Builder { 13 | Builder::default() 14 | } 15 | 16 | impl Builder { 17 | #[inline] 18 | pub fn info(mut self, info: Info) -> Self { 19 | self.spec.info = info; 20 | self 21 | } 22 | 23 | #[inline] 24 | pub fn server(mut self, server: Server) -> Self { 25 | self.spec.servers.push(server); 26 | self 27 | } 28 | 29 | /// **Overrides** path prefix with given string. 30 | #[inline] 31 | pub fn prefix(mut self, path: String) -> Self { 32 | assert!(path.starts_with('/')); 33 | self.path_prefix = path; 34 | 35 | self 36 | } 37 | 38 | /// Creates an openapi specification. You can serialize this as json or yaml 39 | /// to generate client codes. 40 | pub fn build(self, op: F) -> (Spec, Ret) 41 | where 42 | F: FnOnce() -> Ret, 43 | { 44 | let mut collector = new(); 45 | collector.path_prefix = self.path_prefix; 46 | collector.spec = self.spec; 47 | 48 | let cell = RefCell::new(collector); 49 | 50 | let ret = COLLECTOR.set(&cell, || op()); 51 | let mut spec = cell.into_inner().spec(); 52 | spec.openapi = "3.0.1".into(); 53 | (spec, ret) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/openapi/mod.rs: -------------------------------------------------------------------------------- 1 | //! Automatic openapi spec generator. 2 | //! 3 | //! 4 | //! # Usage 5 | //! 6 | //! Enable cargo feature by 7 | //! 8 | //! ```toml 9 | //! [dependencies] 10 | //! rweb = { version = "0.6", features = ["openapi"] } 11 | //! serde = "1" 12 | //! tokio = "1" 13 | //! ``` 14 | //! 15 | //! and wrap your handlers like 16 | //! 17 | //! ```rust 18 | //! use rweb::*; 19 | //! use serde::Serialize; 20 | //! 21 | //! #[get("/")] 22 | //! fn index() -> String { 23 | //! String::from("content type will be 'text/plain' as you return String") 24 | //! } 25 | //! 26 | //! #[derive(Debug, Serialize, Schema)] 27 | //! struct Product { 28 | //! id: String, 29 | //! price: usize, 30 | //! } 31 | //! 32 | //! #[get("/products")] 33 | //! fn products() -> Json> { 34 | //! unimplemented!("content type will be 'application/json', and type of openapi schema will be array") 35 | //! } 36 | //! 37 | //! #[get("/product/{id}")] 38 | //! fn product(id: String) -> Json { 39 | //! // See Component section below if you want to give a name to type. 40 | //! unimplemented!("content type will be 'application/json', and type of openapi schema will be object") 41 | //! } 42 | //! 43 | //! #[tokio::main] 44 | //! async fn main() { 45 | //! let (_spec, filter) = openapi::spec().build(||{ 46 | //! index().or(products()).or(product()) 47 | //! }); 48 | //! 49 | //! serve(filter); 50 | //! // Use the code below to run server. 51 | //! // 52 | //! // serve(filter).run(([127, 0, 0, 1], 3030)).await; 53 | //! } 54 | //! ``` 55 | //! 56 | //! **Note**: Currently using path filter from warp is **not** supported by 57 | //! rweb. If you use path filter from warp, generated document will point to 58 | //! different path. 59 | //! 60 | //! # Annotations 61 | //! 62 | //! This is applicable to `#[get]`, `#[post]`, ..etc 63 | //! 64 | //! 65 | //! ## `#[openapi(id = "foo")]` 66 | //! 67 | //! ```rust 68 | //! use rweb::*; 69 | //! 70 | //! #[get("/sum/{a}/{b}")] 71 | //! #[openapi(id = "math.sum")] 72 | //! fn sum(a: usize, b: usize) -> String { 73 | //! (a + b).to_string() 74 | //! } 75 | //! ``` 76 | //! 77 | //! 78 | //! ## `#[openapi(description = "foo")]` 79 | //! 80 | //! ```rust 81 | //! use rweb::*; 82 | //! 83 | //! /// By default, doc comments on the function will become description of the operation. 84 | //! #[get("/sum/{a}/{b}")] 85 | //! #[openapi(description = "But what if implementation details is written on it?")] 86 | //! fn sum(a: usize, b: usize) -> String { 87 | //! (a + b).to_string() 88 | //! } 89 | //! ``` 90 | //! 91 | //! 92 | //! ## `#[openapi(summary = "foo")]` 93 | //! 94 | //! ```rust 95 | //! use rweb::*; 96 | //! 97 | //! #[get("/sum/{a}/{b}")] 98 | //! #[openapi(summary = "summary of operation")] 99 | //! fn sum(a: usize, b: usize) -> String { 100 | //! (a + b).to_string() 101 | //! } 102 | //! ``` 103 | //! 104 | //! ## `#[openapi(tags("foo", "bar"))]` 105 | //! 106 | //! ```rust 107 | //! use rweb::*; 108 | //! 109 | //! #[get("/sum/{a}/{b}")] 110 | //! #[openapi(tags("sum"))] 111 | //! fn sum(a: usize, b: usize) -> String { 112 | //! (a + b).to_string() 113 | //! } 114 | //! 115 | //! #[get("/mul/{a}/{b}")] 116 | //! #[openapi(tags("mul"))] 117 | //! fn mul(a: usize, b: usize) -> String { 118 | //! (a * b).to_string() 119 | //! } 120 | //! 121 | //! // This is also applicable to #[router] 122 | //! #[router("/math", services(sum, mul))] 123 | //! #[openapi(tags("math"))] 124 | //! fn math() {} 125 | //! ``` 126 | //! 127 | //! 128 | //! # Parameters 129 | //! 130 | //! ```rust 131 | //! use rweb::*; 132 | //! use serde::Deserialize; 133 | //! 134 | //! #[derive(Debug, Deserialize, Schema)] 135 | //! struct Opt { 136 | //! query: String, 137 | //! limit: usize, 138 | //! page_token: String, 139 | //! } 140 | //! 141 | //! /// Look at the generated api document, and surprise :) 142 | //! /// 143 | //! /// Fields of [Opt] are documented as query parameters. 144 | //! #[get("/")] 145 | //! pub fn search(_q: Query) -> String { 146 | //! String::new() 147 | //! } 148 | //! 149 | //! /// Path parameter is documented. (as there's enough information to document it) 150 | //! #[get("/{id}")] 151 | //! pub fn get(id: String) -> String { 152 | //! String::new() 153 | //! } 154 | //! 155 | //! /// Fields of [Opt] are documented as request body parameters. 156 | //! pub fn store(_: Json) -> String{ 157 | //! String::new() 158 | //! } 159 | //! ``` 160 | //! 161 | //! # Response body 162 | //! 163 | //! ```rust 164 | //! use rweb::*; 165 | //! use serde::Serialize; 166 | //! 167 | //! #[derive(Debug, Default, Serialize, Schema)] 168 | //! struct Output { 169 | //! data: String, 170 | //! } 171 | //! 172 | //! /// Json implements rweb::openapi::ResponseEntity if T implements Entity. 173 | //! #[get("/")] 174 | //! pub fn get() -> Json { 175 | //! Output::default().into() 176 | //! } 177 | //! ``` 178 | //! 179 | //! # Entity 180 | //! 181 | //! See [Entity] for details and examples. 182 | //! 183 | //! # Custom error 184 | //! 185 | //! ```rust 186 | //! use rweb::*; 187 | //! use indexmap::IndexMap; 188 | //! use std::borrow::Cow; 189 | //! 190 | //! #[derive(Debug, Schema)] 191 | //! enum Error { 192 | //! NotFound, 193 | //! } 194 | //! 195 | //! impl openapi::ResponseEntity for Error { 196 | //! fn describe_responses(_: &mut openapi::ComponentDescriptor) -> openapi::Responses { 197 | //! let mut map = IndexMap::new(); 198 | //! 199 | //! map.insert( 200 | //! Cow::Borrowed("404"), 201 | //! openapi::Response { 202 | //! description: Cow::Borrowed("Item not found"), 203 | //! ..Default::default() 204 | //! }, 205 | //! ); 206 | //! 207 | //! map 208 | //! } 209 | //! } 210 | //! ``` 211 | 212 | pub use self::{ 213 | builder::{spec, Builder}, 214 | entity::{ComponentDescriptor, Entity, ResponseEntity, Responses}, 215 | }; 216 | use crate::FromRequest; 217 | use http::Method; 218 | use indexmap::IndexMap; 219 | pub use rweb_openapi::v3_0::*; 220 | use scoped_tls::scoped_thread_local; 221 | use std::{borrow::Cow, cell::RefCell, mem::replace}; 222 | 223 | mod builder; 224 | mod entity; 225 | 226 | scoped_thread_local!(static COLLECTOR: RefCell); 227 | 228 | #[derive(Debug)] 229 | pub struct Collector { 230 | spec: Spec, 231 | components: ComponentDescriptor, 232 | path_prefix: String, 233 | tags: Vec>, 234 | } 235 | 236 | impl Collector { 237 | /// Method used by `#[op]` 238 | #[doc(hidden)] 239 | pub fn components(&mut self) -> &mut ComponentDescriptor { 240 | &mut self.components 241 | } 242 | 243 | /// Method used by `#[router]`. 244 | #[doc(hidden)] 245 | pub fn with_appended_prefix( 246 | &mut self, 247 | prefix: &str, 248 | tags: Vec>, 249 | op: F, 250 | ) -> Ret 251 | where 252 | F: FnOnce() -> Ret, 253 | { 254 | let orig_len = self.path_prefix.len(); 255 | self.path_prefix.push_str(prefix); 256 | let orig_tag_len = self.tags.len(); 257 | self.tags.extend(tags); 258 | 259 | let new = replace(self, new()); 260 | let cell = RefCell::new(new); 261 | let ret = COLLECTOR.set(&cell, || op()); 262 | 263 | let new = cell.into_inner(); 264 | let _ = replace(self, new); 265 | 266 | self.tags.drain(orig_tag_len..); 267 | self.path_prefix.drain(orig_len..); 268 | ret 269 | } 270 | 271 | pub fn add_request_type_to(&mut self, op: &mut Operation) { 272 | if T::is_body() { 273 | if op.request_body.is_some() { 274 | panic!("Multiple body detected"); 275 | } 276 | 277 | let s = T::describe(&mut self.components); 278 | 279 | let mut content = IndexMap::new(); 280 | 281 | // TODO 282 | content.insert( 283 | Cow::Borrowed(T::content_type()), 284 | MediaType { 285 | schema: Some(s), 286 | examples: None, 287 | encoding: Default::default(), 288 | }, 289 | ); 290 | 291 | op.request_body = Some(ObjectOrReference::Object(RequestBody { 292 | content, 293 | required: Some(!T::is_optional()), 294 | ..Default::default() 295 | })); 296 | } 297 | 298 | if T::is_query() { 299 | self.add_query_type_to::(op); 300 | } 301 | } 302 | 303 | fn add_query_type_to(&mut self, op: &mut Operation) { 304 | debug_assert!(T::is_query()); 305 | 306 | let s = T::describe(&mut self.components); 307 | let s = self.components.get_unpack(&s); 308 | 309 | assert_eq!( 310 | Some(Type::Object), 311 | s.schema_type, 312 | "Query<[not object]> is invalid. Store [not object] as a field." 313 | ); 314 | 315 | for (name, ps) in &s.properties { 316 | op.parameters.push(ObjectOrReference::Object(Parameter { 317 | name: name.clone(), 318 | location: Location::Query, 319 | required: Some(s.required.contains(name)), 320 | representation: Some(ParameterRepresentation::Simple { schema: ps.clone() }), 321 | ..Default::default() 322 | })); 323 | } 324 | } 325 | 326 | pub fn add_response_to(&mut self, op: &mut Operation) { 327 | // T::describe(&mut self.components); 328 | let mut responses = T::describe_responses(&mut self.components); 329 | for (code, mut resp) in &mut responses { 330 | if let Some(ex_resp) = op.responses.remove(code) { 331 | if !ex_resp.description.is_empty() { 332 | resp.description = ex_resp.description 333 | } 334 | } 335 | } 336 | op.responses.extend(responses); 337 | } 338 | 339 | #[doc(hidden)] 340 | #[inline(never)] 341 | pub fn add(&mut self, path: &str, method: Method, operation: Operation) { 342 | let path = { 343 | let mut p = self.path_prefix.clone(); 344 | p.push_str(path); 345 | p 346 | }; 347 | 348 | let v = self 349 | .spec 350 | .paths 351 | .entry(Cow::Owned(path)) 352 | .or_insert_with(Default::default); 353 | 354 | let op = if method == Method::GET { 355 | &mut v.get 356 | } else if method == Method::POST { 357 | &mut v.post 358 | } else if method == Method::PUT { 359 | &mut v.put 360 | } else if method == Method::DELETE { 361 | &mut v.delete 362 | } else if method == Method::HEAD { 363 | &mut v.head 364 | } else if method == Method::OPTIONS { 365 | &mut v.options 366 | } else if method == Method::CONNECT { 367 | unimplemented!("openapi spec generation for http CONNECT") 368 | } else if method == Method::PATCH { 369 | &mut v.patch 370 | } else if method == Method::TRACE { 371 | &mut v.trace 372 | } else { 373 | unreachable!("Unknown http method: {:?}", method) 374 | }; 375 | 376 | match op { 377 | Some(op) => { 378 | assert_eq!(*op, operation); 379 | } 380 | None => { 381 | *op = Some(operation); 382 | } 383 | } 384 | 385 | let op = op.as_mut().unwrap(); 386 | op.tags.extend(self.tags.clone()); 387 | } 388 | 389 | pub fn add_scheme() {} 390 | 391 | fn spec(self) -> Spec { 392 | let mut spec = self.spec; 393 | spec.components 394 | .get_or_insert_with(Default::default) 395 | .schemas 396 | .extend(self.components.build()); 397 | spec 398 | } 399 | } 400 | 401 | fn new() -> Collector { 402 | Collector { 403 | spec: Default::default(), 404 | components: ComponentDescriptor::new(), 405 | path_prefix: Default::default(), 406 | tags: vec![], 407 | } 408 | } 409 | 410 | #[doc(hidden)] 411 | pub fn with(op: F) -> Ret 412 | where 413 | F: FnOnce(Option<&mut Collector>) -> Ret, 414 | { 415 | if COLLECTOR.is_set() { 416 | COLLECTOR.with(|c| { 417 | let mut r = c.borrow_mut(); 418 | op(Some(&mut r)) 419 | }) 420 | } else { 421 | op(None) 422 | } 423 | } 424 | 425 | /// I'm too lazy to use inflector. 426 | #[doc(hidden)] 427 | pub mod http_methods { 428 | use http::Method; 429 | 430 | pub const fn get() -> Method { 431 | Method::GET 432 | } 433 | 434 | pub const fn post() -> Method { 435 | Method::POST 436 | } 437 | 438 | pub const fn put() -> Method { 439 | Method::PUT 440 | } 441 | 442 | pub const fn delete() -> Method { 443 | Method::DELETE 444 | } 445 | 446 | pub const fn head() -> Method { 447 | Method::HEAD 448 | } 449 | 450 | pub const fn options() -> Method { 451 | Method::OPTIONS 452 | } 453 | 454 | pub const fn connect() -> Method { 455 | Method::CONNECT 456 | } 457 | 458 | pub const fn patch() -> Method { 459 | Method::PATCH 460 | } 461 | 462 | pub const fn trace() -> Method { 463 | Method::TRACE 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /src/routes.rs: -------------------------------------------------------------------------------- 1 | /// Helper macro to chain multiple routes with .or(route()) between them. 2 | /// 3 | /// # Example - single use with data injection 4 | /// 5 | /// ```ignore 6 | /// struct DbConnection; 7 | /// fn display_user(#[data] user: DbConnection) {} 8 | /// let db_connection = DbConnection; 9 | /// assert_eq!(routes![db_connection; display_user], display_user(db_connection)); 10 | /// ``` 11 | /// 12 | /// # Example - Multiple routes 13 | /// 14 | /// ```ignore 15 | /// fn info() {} 16 | /// fn ping() {} 17 | /// assert_eq!(routes![ping, info], ping().or(info())); 18 | /// ``` 19 | /// 20 | /// # Example - Multiple routes with data injection 21 | /// 22 | /// ```ignore 23 | /// struct DbConnection; 24 | /// fn display_user(#[data] db: DbConnection) {} 25 | /// fn display_users(#[data] db: DbConnection) {} 26 | /// let db_connection = DbConnection; 27 | /// assert_eq!(routes![db_connection; display_user, display_users], display_user(db_connection).or(display_users(db_connection))); 28 | /// ``` 29 | /// 30 | /// # Example - Multiple routes chaining with data injection 31 | /// 32 | /// ```ignore 33 | /// struct DbConnection; 34 | /// fn ping() {} 35 | /// fn info() {} 36 | /// fn display_user(#[data] db: DbConnection) {} 37 | /// let db_connection = DbConnection; 38 | /// assert_eq!(routes![ping, info].or(routes![db_connection; display_user]), ping().or(info()).or(display_user(db_connection))); 39 | /// ``` 40 | #[macro_export] 41 | macro_rules! routes { 42 | ( $s:expr ) => { 43 | /// This is used when you use routes! with a single route without any data; I.e routes!(ping) 44 | $s() 45 | }; 46 | ( $inject:expr; $s:expr ) => { 47 | /// This is used when you use routes! with a single route and want to pass some data to it; I.e routes!(db_connection; get_user) 48 | $s($inject) 49 | }; 50 | ( $s:expr, $( $x:expr ),* ) => { 51 | /// This is used when you use routes! with multiple routes without any data: I.e routes!(ping, get_users, get_users) 52 | $s() 53 | $( 54 | .or($x()) 55 | )* 56 | }; 57 | ( $inject:expr; $s:expr, $( $x:expr ),* ) => { 58 | /// This is used when you use routes! with multiple routes and want to pass some data to it: I.e routes!(db_connection; ping, get_users, get_users) 59 | $s(inject) 60 | $( 61 | .or($x($inject)) 62 | )* 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/rt.rs: -------------------------------------------------------------------------------- 1 | pub use http::StatusCode; 2 | pub use indexmap::{indexmap, IndexMap}; 3 | pub use serde_json; 4 | use std::convert::Infallible; 5 | pub use std::{borrow::Cow, clone::Clone, default::Default}; 6 | pub use tokio; 7 | use warp::{any, Filter}; 8 | 9 | pub fn provider( 10 | data: T, 11 | ) -> impl Filter + Clone { 12 | any().map(move || data.clone()) 13 | } 14 | -------------------------------------------------------------------------------- /tests/async.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "openapi"))] 2 | 3 | use rweb::{get, Filter, Rejection}; 4 | 5 | async fn task() -> Result { 6 | Ok(String::from("TEST")) 7 | } 8 | 9 | #[get("/")] 10 | async fn index() -> Result { 11 | task().await 12 | } 13 | 14 | #[tokio::test] 15 | async fn index_test() { 16 | let value = warp::test::request() 17 | .path("/") 18 | .reply(&index()) 19 | .await 20 | .into_body(); 21 | 22 | assert_eq!(value, b"TEST"[..]); 23 | } 24 | 25 | #[get("/foo")] 26 | async fn foo() -> Result { 27 | task().await 28 | } 29 | 30 | #[tokio::test] 31 | async fn foo_test() { 32 | let filter = assert_filter(foo()); 33 | 34 | let value = warp::test::request() 35 | .path("/foo") 36 | .reply(&filter) 37 | .await 38 | .into_body(); 39 | 40 | assert_eq!(value, b"TEST"[..]); 41 | } 42 | 43 | #[get("/param/{foo}")] 44 | async fn param(foo: String) -> Result { 45 | println!("{}", foo); // to use it 46 | task().await 47 | } 48 | 49 | #[tokio::test] 50 | async fn param_test() { 51 | let filter = assert_filter(param()); 52 | 53 | let value = warp::test::request() 54 | .path("/param/param") 55 | .reply(&filter) 56 | .await 57 | .into_body(); 58 | 59 | assert_eq!(value, b"TEST"[..]); 60 | } 61 | 62 | fn assert_filter( 63 | f: F, 64 | ) -> impl Filter 65 | where 66 | F: Filter, 67 | { 68 | f 69 | } 70 | -------------------------------------------------------------------------------- /tests/body.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "openapi"))] 2 | 3 | use bytes::Bytes; 4 | use http::Error; 5 | use rweb::{post, Filter}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | struct LoginForm { 10 | id: String, 11 | password: String, 12 | } 13 | 14 | #[post("/json")] 15 | fn json(#[json] body: LoginForm) -> Result { 16 | Ok(serde_json::to_string(&body).unwrap()) 17 | } 18 | 19 | #[post("/body")] 20 | fn body(#[body] body: Bytes) -> Result { 21 | let _ = body; 22 | Ok(String::new()) 23 | } 24 | 25 | #[post("/form")] 26 | fn form(#[form] body: LoginForm) -> Result { 27 | Ok(serde_json::to_string(&body).unwrap()) 28 | } 29 | 30 | //#[post("/")] 31 | //fn query(#[query] query: rweb::Json) -> Result { 32 | // Err(Error {}) 33 | //} 34 | 35 | #[tokio::test] 36 | async fn bind() { 37 | rweb::serve(json().or(body()).or(form())); 38 | } 39 | -------------------------------------------------------------------------------- /tests/factory.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "openapi"))] 2 | 3 | use http::StatusCode; 4 | use rweb::{filters::BoxedFilter, *}; 5 | use serde::Deserialize; 6 | 7 | impl FromRequest for User { 8 | type Filter = BoxedFilter<(User,)>; 9 | 10 | fn new() -> Self::Filter { 11 | let header = header::("x-user-id"); 12 | header.map(|id| User { id }).boxed() 13 | } 14 | } 15 | 16 | struct User { 17 | id: String, 18 | } 19 | 20 | #[get("/")] 21 | fn index(user: User) -> String { 22 | user.id 23 | } 24 | 25 | #[tokio::test] 26 | async fn index_test() { 27 | let value = warp::test::request() 28 | .header("x-user-id", "test-uid") 29 | .path("/") 30 | .reply(&index()) 31 | .await; 32 | 33 | assert_eq!(value.status(), StatusCode::OK); 34 | assert_eq!(value.into_body(), b"test-uid"[..]); 35 | } 36 | 37 | #[tokio::test] 38 | async fn index_test_fail() { 39 | let value = warp::test::request().path("/").reply(&index()).await; 40 | 41 | assert_eq!(value.status(), StatusCode::BAD_REQUEST); 42 | } 43 | 44 | #[derive(Deserialize)] 45 | struct LoginForm { 46 | id: String, 47 | #[allow(dead_code)] 48 | password: String, 49 | } 50 | 51 | #[get("/")] 52 | pub fn json(body: Json) -> String { 53 | body.into_inner().id 54 | } 55 | 56 | #[get("/")] 57 | pub fn form(body: Form) -> String { 58 | body.into_inner().id 59 | } 60 | 61 | #[derive(Deserialize)] 62 | struct Pagination { 63 | token: String, 64 | } 65 | 66 | #[get("/")] 67 | pub fn query(body: Query) -> String { 68 | body.into_inner().token 69 | } 70 | -------------------------------------------------------------------------------- /tests/filter.rs: -------------------------------------------------------------------------------- 1 | use rweb::*; 2 | use std::{net::SocketAddr, str::FromStr}; 3 | 4 | fn host_header( 5 | ) -> impl Clone + Filter { 6 | rweb::header::("host") 7 | } 8 | 9 | fn accept_all_header() -> impl Clone + Filter { 10 | rweb::header::exact("accept", "*/*") 11 | } 12 | 13 | #[get("/")] 14 | fn handler_guard(#[filter = "accept_all_header"] _header: ()) -> String { 15 | String::new() 16 | } 17 | 18 | #[tokio::test] 19 | async fn handler_guard_test() { 20 | let value = warp::test::request() 21 | .path("/") 22 | .header("accept", "*/*") 23 | .reply(&handler_guard()) 24 | .await 25 | .into_body(); 26 | 27 | assert_eq!(value, b""[..]); 28 | } 29 | 30 | #[get("/")] 31 | fn handler_value(#[filter = "host_header"] addr: SocketAddr) -> String { 32 | addr.to_string() 33 | } 34 | 35 | #[tokio::test] 36 | async fn handler_value_test() { 37 | let value = warp::test::request() 38 | .path("/") 39 | .header("host", "127.0.0.1:8080") 40 | .reply(&handler_value()) 41 | .await 42 | .into_body(); 43 | 44 | assert_eq!(value, b"127.0.0.1:8080"[..]); 45 | } 46 | 47 | #[get("/")] 48 | fn handler_mixed( 49 | #[filter = "accept_all_header"] _header: (), 50 | #[filter = "host_header"] addr: SocketAddr, 51 | ) -> String { 52 | addr.to_string() 53 | } 54 | 55 | #[tokio::test] 56 | async fn handler_mixed_test() { 57 | let value = warp::test::request() 58 | .path("/") 59 | .header("accept", "*/*") 60 | .header("host", "127.0.0.1:8080") 61 | .reply(&handler_mixed()) 62 | .await 63 | .into_body(); 64 | 65 | assert_eq!(value, b"127.0.0.1:8080"[..]); 66 | } 67 | -------------------------------------------------------------------------------- /tests/header.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use rweb::*; 3 | 4 | #[get("/")] 5 | fn ret_accept(#[header = "accept"] accept: String) -> String { 6 | accept 7 | } 8 | 9 | #[tokio::test] 10 | async fn ret_accept_test() { 11 | let value = warp::test::request() 12 | .path("/") 13 | .header("accept", "foo") 14 | .reply(&ret_accept()) 15 | .await 16 | .into_body(); 17 | assert_eq!(value, b"foo"[..]); 18 | } 19 | 20 | #[get("/")] 21 | #[header("X-AuthUser", "test-uid")] 22 | fn guard() -> String { 23 | unreachable!() 24 | } 25 | 26 | #[tokio::test] 27 | async fn guard_test() { 28 | let value = warp::test::request().path("/").reply(&guard()).await; 29 | 30 | assert_eq!(value.status(), StatusCode::BAD_REQUEST); 31 | } 32 | -------------------------------------------------------------------------------- /tests/item_attrs.rs: -------------------------------------------------------------------------------- 1 | use rweb::*; 2 | 3 | #[get("/")] 4 | #[header("X-AuthUser", "test-uid")] 5 | fn header_guard() -> String { 6 | unreachable!() 7 | } 8 | 9 | #[get("/")] 10 | #[body_size(max = "8192")] 11 | fn body_size() -> String { 12 | unreachable!() 13 | } 14 | 15 | #[get("/")] 16 | #[cors(origins("example.com"), max_age = 600)] 17 | fn cors_1() -> String { 18 | unreachable!() 19 | } 20 | 21 | #[get("/")] 22 | #[cors(origins("example.com"), methods(get), max_age = 600)] 23 | fn cors_2() -> String { 24 | unreachable!() 25 | } 26 | 27 | #[get("/")] 28 | #[cors(origins("*"), methods(get), max_age = 600)] 29 | fn cors_3() -> String { 30 | unreachable!() 31 | } 32 | 33 | #[get("/")] 34 | #[cors( 35 | origins("*"), 36 | methods(get, post, patch, delete), 37 | headers("accept"), 38 | max_age = 600 39 | )] 40 | fn cors_4() -> String { 41 | unreachable!() 42 | } 43 | -------------------------------------------------------------------------------- /tests/mixed.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "openapi"))] 2 | 3 | use http::Error; 4 | use rweb::*; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Deserialize)] 8 | struct LoginForm { 9 | id: String, 10 | password: String, 11 | } 12 | 13 | #[get("/param/{foo}")] 14 | fn body_after_path_param(foo: String, #[json] body: LoginForm) -> Result { 15 | assert_eq!(body.id, "TEST_ID"); 16 | assert_eq!(body.password, "TEST_PASSWORD"); 17 | Ok(foo) 18 | } 19 | 20 | #[tokio::test] 21 | async fn test_body_after_path_param() { 22 | let value = warp::test::request() 23 | .path("/param/foo") 24 | .body( 25 | serde_json::to_vec(&LoginForm { 26 | id: "TEST_ID".into(), 27 | password: "TEST_PASSWORD".into(), 28 | }) 29 | .unwrap(), 30 | ) 31 | .reply(&body_after_path_param()) 32 | .await 33 | .into_body(); 34 | 35 | assert_eq!(value, b"foo"[..]); 36 | } 37 | 38 | #[get("/param/{foo}")] 39 | fn path_param_after_body(#[json] body: LoginForm, foo: String) -> Result { 40 | assert_eq!(body.id, "TEST_ID"); 41 | assert_eq!(body.password, "TEST_PASSWORD"); 42 | Ok(foo) 43 | } 44 | 45 | #[tokio::test] 46 | async fn test_path_param_after_body() { 47 | let value = warp::test::request() 48 | .path("/param/foo") 49 | .body( 50 | serde_json::to_vec(&LoginForm { 51 | id: "TEST_ID".into(), 52 | password: "TEST_PASSWORD".into(), 53 | }) 54 | .unwrap(), 55 | ) 56 | .reply(&path_param_after_body()) 57 | .await 58 | .into_body(); 59 | 60 | assert_eq!(value, b"foo"[..]); 61 | } 62 | 63 | #[get("/param/{a}/{b}")] 64 | fn body_between_path_params(a: u32, #[json] body: LoginForm, b: u32) -> String { 65 | assert_eq!(body.id, "TEST_ID"); 66 | assert_eq!(body.password, "TEST_PASSWORD"); 67 | (a + b).to_string() 68 | } 69 | 70 | #[tokio::test] 71 | async fn test_body_between_path_params() { 72 | let value = warp::test::request() 73 | .path("/param/3/4") 74 | .body( 75 | serde_json::to_vec(&LoginForm { 76 | id: "TEST_ID".into(), 77 | password: "TEST_PASSWORD".into(), 78 | }) 79 | .unwrap(), 80 | ) 81 | .reply(&body_between_path_params()) 82 | .await 83 | .into_body(); 84 | 85 | assert_eq!(value, b"7"[..]); 86 | } 87 | -------------------------------------------------------------------------------- /tests/openapi.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_yaml; 6 | 7 | #[derive(Debug, Default, Serialize, Deserialize, Schema)] 8 | pub struct Product { 9 | pub id: String, 10 | pub title: String, 11 | } 12 | 13 | #[derive(Debug, Default, Serialize, Deserialize, Schema)] 14 | pub struct SearchReq { 15 | pub query: String, 16 | pub limit: usize, 17 | /// paging-token-example 18 | pub paging_token: String, 19 | } 20 | 21 | #[get("/products")] 22 | fn products(_: Query) -> Json> { 23 | vec![].into() 24 | } 25 | 26 | #[get("/product/{id}")] 27 | fn product(id: String) -> Json { 28 | Product { 29 | title: format!("Title of {}", id), 30 | id, 31 | } 32 | .into() 33 | } 34 | 35 | #[test] 36 | fn simple() { 37 | let (spec, _) = openapi::spec().build(|| { 38 | // 39 | product().or(products()) 40 | }); 41 | 42 | assert!(spec.paths.get("/products").is_some()); 43 | assert!(spec.paths.get("/products").unwrap().get.is_some()); 44 | 45 | assert!(spec.paths.get("/product/{id}").is_some()); 46 | assert!(spec.paths.get("/product/{id}").unwrap().get.is_some()); 47 | 48 | let yaml = serde_yaml::to_string(&spec).unwrap(); 49 | println!("{}", yaml); 50 | 51 | assert!(yaml.contains("paging-token-example")); 52 | } 53 | 54 | #[derive(Debug, Default, Serialize, Schema)] 55 | struct Resp { 56 | /// http-status-code 57 | status: usize, 58 | data: T, 59 | } 60 | 61 | #[derive(Debug, Default, Serialize, Schema)] 62 | struct Data {} 63 | 64 | #[get("/proxy")] 65 | fn proxy() -> Json> { 66 | Resp { 67 | status: 200, 68 | data: Data::default(), 69 | } 70 | .into() 71 | } 72 | 73 | // TODO: enum 74 | 75 | #[test] 76 | fn generic() { 77 | let (spec, _) = openapi::spec().build(|| { 78 | // 79 | proxy() 80 | }); 81 | 82 | assert!(spec.paths.get("/proxy").is_some()); 83 | assert!(spec.paths.get("/proxy").unwrap().get.is_some()); 84 | 85 | let yaml = serde_yaml::to_string(&spec).unwrap(); 86 | println!("{}", yaml); 87 | 88 | assert!(yaml.contains("http-status-code")); 89 | } 90 | 91 | /// Doc comment 92 | #[get("/")] 93 | #[openapi(description = "foo-bar")] 94 | /// Doc comment 95 | fn index() -> String { 96 | String::new() 97 | } 98 | 99 | #[test] 100 | fn description() { 101 | let (spec, _) = openapi::spec().build(|| { 102 | // 103 | index() 104 | }); 105 | 106 | assert!(spec.paths.get("/").is_some()); 107 | assert!(spec.paths.get("/").unwrap().get.is_some()); 108 | assert_eq!( 109 | spec.paths 110 | .get("/") 111 | .unwrap() 112 | .get 113 | .as_ref() 114 | .unwrap() 115 | .description, 116 | "foo-bar" 117 | ); 118 | 119 | let yaml = serde_yaml::to_string(&spec).unwrap(); 120 | println!("{}", yaml); 121 | } 122 | -------------------------------------------------------------------------------- /tests/openapi_arr_length.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use std::collections::HashSet; 4 | 5 | use rweb::*; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize, rweb::Schema)] 9 | #[schema(component = "Things")] 10 | struct Things { 11 | yarr: [u64; 24], 12 | yarr0: [u64; 0], 13 | tuple: (String, String, String), 14 | set: HashSet, 15 | } 16 | 17 | #[get("/")] 18 | fn index(_: Query) -> String { 19 | String::new() 20 | } 21 | 22 | #[test] 23 | fn test_skip() { 24 | let (spec, _) = openapi::spec().build(|| index()); 25 | let schemas = &spec.components.as_ref().unwrap().schemas; 26 | let things = match schemas.get("Things").unwrap() { 27 | rweb::openapi::ObjectOrReference::Object(s) => s, 28 | _ => panic!(), 29 | }; 30 | macro_rules! unpack { 31 | ($opt:expr) => { 32 | $opt.unwrap().unwrap().unwrap() 33 | }; 34 | } 35 | macro_rules! prop { 36 | ($prop:expr) => { 37 | unpack!(things.properties.get($prop)) 38 | }; 39 | } 40 | assert!(things.properties.contains_key("yarr")); 41 | assert!(things.properties.contains_key("yarr0")); 42 | assert!(things.properties.contains_key("tuple")); 43 | assert_eq!(prop!("yarr").min_items, Some(24)); 44 | assert_eq!(prop!("yarr").max_items, Some(24)); 45 | assert_eq!( 46 | unpack!(prop!("yarr").items.as_ref()).schema_type, 47 | Some(openapi::Type::Integer) 48 | ); 49 | assert_eq!(prop!("yarr0").min_items, Some(0)); 50 | assert_eq!(prop!("yarr0").max_items, Some(0)); 51 | assert_eq!( 52 | unpack!(prop!("yarr0").items.as_ref()).schema_type, 53 | Some(openapi::Type::Integer) 54 | ); 55 | assert_eq!(prop!("tuple").min_items, Some(3)); 56 | assert_eq!(prop!("tuple").max_items, Some(3)); 57 | assert_eq!( 58 | unpack!(prop!("tuple").items.as_ref()).schema_type, 59 | Some(openapi::Type::String) 60 | ); 61 | assert_eq!(prop!("set").unique_items, Some(true)); 62 | assert_eq!( 63 | unpack!(prop!("set").items.as_ref()).schema_type, 64 | Some(openapi::Type::String) 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /tests/openapi_clike_enum.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[get("/")] 7 | fn index(_: Json) -> String { 8 | String::new() 9 | } 10 | 11 | #[derive(Debug, Serialize, Deserialize, Schema)] 12 | struct Colors { 13 | colordef: Color, 14 | coloradj: ColorAdjtagged, 15 | colorint: ColorInttagged, 16 | } 17 | 18 | #[derive(Debug, Serialize, Deserialize, Schema)] 19 | #[schema(component = "Color")] 20 | pub enum Color { 21 | #[serde(rename = "black")] 22 | Black, 23 | #[serde(rename = "blue")] 24 | Blue, 25 | } 26 | 27 | impl std::str::FromStr for Color { 28 | type Err = &'static str; 29 | fn from_str(s: &str) -> Result { 30 | match s { 31 | "black" => Ok(Color::Black), 32 | "blue" => Ok(Color::Blue), 33 | _ => Err("ERR"), 34 | } 35 | } 36 | } 37 | 38 | #[derive(Debug, Serialize, Deserialize, Schema)] 39 | #[serde(tag = "tag", rename_all = "lowercase")] 40 | #[schema(component = "ColorInttagged")] 41 | enum ColorInttagged { 42 | Black, 43 | Blue, 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize, Schema)] 47 | #[serde(rename_all = "lowercase")] 48 | #[serde(tag = "tag", content = "content")] 49 | #[schema(component = "ColorAdjtagged")] 50 | enum ColorAdjtagged { 51 | Black, 52 | Blue, 53 | } 54 | 55 | #[test] 56 | fn description() { 57 | let (spec, _) = openapi::spec().build(|| { 58 | // 59 | index() 60 | }); 61 | let schemas = &spec.components.as_ref().unwrap().schemas; 62 | macro_rules! component { 63 | ($cn:expr) => { 64 | match schemas.get($cn) { 65 | Some(openapi::ObjectOrReference::Object(s)) => s, 66 | Some(..) => panic!("Component schema can't be a reference"), 67 | None => panic!("No component schema for {}", $cn), 68 | } 69 | }; 70 | } 71 | let schema = component!("Color"); 72 | assert_eq!(schema.schema_type, Some(openapi::Type::String)); 73 | assert_eq!(schema.enum_values, vec!["black", "blue"]); 74 | let schema = component!("ColorInttagged"); 75 | assert_eq!(schema.schema_type, Some(openapi::Type::Object)); 76 | assert_eq!(schema.required, vec!["tag"]); 77 | let schema = schema.properties["tag"].unwrap().unwrap(); 78 | assert_eq!(schema.schema_type, Some(openapi::Type::String)); 79 | assert_eq!(schema.enum_values, vec!["black", "blue"]); 80 | let schema = component!("ColorAdjtagged"); 81 | assert_eq!(schema.schema_type, Some(openapi::Type::Object)); 82 | assert_eq!(schema.required, vec!["tag"]); 83 | let schema = schema.properties["tag"].unwrap().unwrap(); 84 | assert_eq!(schema.schema_type, Some(openapi::Type::String)); 85 | assert_eq!(schema.enum_values, vec!["black", "blue"]); 86 | } 87 | -------------------------------------------------------------------------------- /tests/openapi_components_recursive.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Deserialize, Serialize, Schema)] 7 | #[schema(component = "Bar")] 8 | pub struct Bar { 9 | pub foo: Box, 10 | } 11 | 12 | #[derive(Debug, Deserialize, Serialize, Schema)] 13 | pub struct NotAComponent { 14 | pub foo: Box, 15 | pub bar: Vec, 16 | } 17 | 18 | #[derive(Debug, Deserialize, Serialize, Schema)] 19 | #[schema(component = "Foo")] 20 | pub struct Foo { 21 | pub interim: Option>, 22 | } 23 | 24 | #[get("/")] 25 | fn test_r(_: Json) -> String { 26 | String::new() 27 | } 28 | 29 | #[test] 30 | fn test_component_recursion_compile() { 31 | let (spec, _) = openapi::spec().build(|| test_r()); 32 | let schemas = &spec.components.as_ref().unwrap().schemas; 33 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 34 | for (name, _) in schemas { 35 | assert!(name 36 | .chars() 37 | .all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')) 38 | } 39 | assert!(schemas.contains_key("Foo")); 40 | assert!(schemas.contains_key("Bar")); 41 | assert!(!schemas.contains_key("NotAComponent")); 42 | } 43 | -------------------------------------------------------------------------------- /tests/openapi_description_example.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Schema)] 7 | #[schema(component = "TestStruct")] 8 | pub struct TestStruct { 9 | ///a description 10 | #[schema(example = "\"an example\"")] 11 | d1: String, 12 | #[schema(example = "\"an example\"")] 13 | #[schema(description = "a description")] 14 | d2: String, 15 | #[schema(example = "\"an example\"", description = "a description")] 16 | d3: String, 17 | } 18 | 19 | #[get("/")] 20 | fn test_r(_: Query) -> String { 21 | String::new() 22 | } 23 | 24 | #[test] 25 | fn test_description_example() { 26 | let (spec, _) = openapi::spec().build(|| test_r()); 27 | let schema = match spec 28 | .components 29 | .as_ref() 30 | .unwrap() 31 | .schemas 32 | .get("TestStruct") 33 | .unwrap() 34 | { 35 | openapi::ObjectOrReference::Object(s) => s, 36 | _ => panic!(), 37 | }; 38 | println!("{}", serde_yaml::to_string(&schema).unwrap()); 39 | for (_, p) in &schema.properties { 40 | assert_eq!(p.unwrap().unwrap().description, "a description"); 41 | assert_eq!( 42 | p.unwrap().unwrap().example, 43 | Some(serde_json::from_str("\"an example\"").unwrap()) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/openapi_enum.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_yaml; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Schema)] 8 | #[schema(component = "EExttagged")] 9 | enum EExttagged { 10 | A(String), 11 | B(usize), 12 | Stru { field: String }, 13 | Plain, 14 | } 15 | 16 | #[derive(Debug, Serialize, Deserialize, Schema)] 17 | #[serde(tag = "tag")] 18 | #[schema(component = "EInttagged")] 19 | enum EInttagged { 20 | Stru { field: String }, 21 | Plain, 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize, Schema)] 25 | #[serde(tag = "tag", content = "content")] 26 | #[schema(component = "EAdjtagged")] 27 | enum EAdjtagged { 28 | A(String), 29 | B(usize), 30 | Stru { field: String }, 31 | Plain, 32 | } 33 | 34 | #[derive(Debug, Serialize, Deserialize, Schema)] 35 | #[serde(untagged)] 36 | #[schema(component = "EUntagged")] 37 | enum EUntagged { 38 | A(String), 39 | B(usize), 40 | Stru { field: String }, 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize, Schema)] 44 | struct Enums { 45 | ext: EExttagged, 46 | adj: EAdjtagged, 47 | int: EInttagged, 48 | unt: EUntagged, 49 | } 50 | 51 | #[get("/")] 52 | fn index(_: Json) -> String { 53 | String::new() 54 | } 55 | 56 | #[test] 57 | fn description() { 58 | let (spec, _) = openapi::spec().build(|| { 59 | // 60 | index() 61 | }); 62 | 63 | println!("{}", serde_yaml::to_string(&spec).unwrap()); 64 | let schemas = &spec.components.as_ref().unwrap().schemas; 65 | macro_rules! component { 66 | ($cn:expr) => { 67 | match schemas.get($cn) { 68 | Some(openapi::ObjectOrReference::Object(s)) => s, 69 | Some(..) => panic!("Component schema can't be a reference"), 70 | None => panic!("No component schema for {}", $cn), 71 | } 72 | }; 73 | } 74 | println!("{}", serde_yaml::to_string(&EExttagged::B(55)).unwrap()); 75 | println!("{}", serde_yaml::to_string(&EExttagged::Plain).unwrap()); 76 | let schema = component!("EExttagged"); 77 | assert_eq!(schema.one_of.len(), 4); 78 | assert_eq!( 79 | schema.one_of[0].unwrap().unwrap().schema_type, 80 | Some(openapi::Type::Object) 81 | ); 82 | assert_eq!(schema.one_of[0].unwrap().unwrap().required, vec!["A"]); 83 | assert_eq!( 84 | schema.one_of[0].unwrap().unwrap().properties["A"] 85 | .unwrap() 86 | .unwrap() 87 | .schema_type, 88 | Some(openapi::Type::String) 89 | ); 90 | assert_eq!( 91 | schema.one_of[1].unwrap().unwrap().schema_type, 92 | Some(openapi::Type::Object) 93 | ); 94 | assert_eq!(schema.one_of[1].unwrap().unwrap().required, vec!["B"]); 95 | assert_eq!( 96 | schema.one_of[1].unwrap().unwrap().properties["B"] 97 | .unwrap() 98 | .unwrap() 99 | .schema_type, 100 | Some(openapi::Type::Integer) 101 | ); 102 | assert_eq!( 103 | schema.one_of[2].unwrap().unwrap().schema_type, 104 | Some(openapi::Type::Object) 105 | ); 106 | assert_eq!(schema.one_of[2].unwrap().unwrap().required, vec!["Stru"]); 107 | assert_eq!( 108 | schema.one_of[2].unwrap().unwrap().properties["Stru"] 109 | .unwrap() 110 | .unwrap() 111 | .schema_type, 112 | Some(openapi::Type::Object) 113 | ); 114 | assert_eq!( 115 | schema.one_of[3].unwrap().unwrap().schema_type, 116 | Some(openapi::Type::String) 117 | ); 118 | assert_eq!( 119 | schema.one_of[3].unwrap().unwrap().enum_values, 120 | vec!["Plain"] 121 | ); 122 | let schema = component!("EInttagged"); 123 | assert_eq!(schema.one_of.len(), 2); 124 | assert_eq!( 125 | schema.one_of[0].unwrap().unwrap().schema_type, 126 | Some(openapi::Type::Object) 127 | ); 128 | assert_eq!( 129 | schema.one_of[0].unwrap().unwrap().required, 130 | vec!["field", "tag"] 131 | ); 132 | assert_eq!( 133 | schema.one_of[0].unwrap().unwrap().properties["field"] 134 | .unwrap() 135 | .unwrap() 136 | .schema_type, 137 | Some(openapi::Type::String) 138 | ); 139 | assert_eq!( 140 | schema.one_of[0].unwrap().unwrap().properties["tag"] 141 | .unwrap() 142 | .unwrap() 143 | .schema_type, 144 | Some(openapi::Type::String) 145 | ); 146 | assert_eq!( 147 | schema.one_of[0].unwrap().unwrap().properties["tag"] 148 | .unwrap() 149 | .unwrap() 150 | .enum_values, 151 | vec!["Stru"] 152 | ); 153 | assert_eq!( 154 | schema.one_of[1].unwrap().unwrap().schema_type, 155 | Some(openapi::Type::Object) 156 | ); 157 | assert_eq!(schema.one_of[1].unwrap().unwrap().required, vec!["tag"]); 158 | assert_eq!( 159 | schema.one_of[1].unwrap().unwrap().properties["tag"] 160 | .unwrap() 161 | .unwrap() 162 | .schema_type, 163 | Some(openapi::Type::String) 164 | ); 165 | assert_eq!( 166 | schema.one_of[1].unwrap().unwrap().properties["tag"] 167 | .unwrap() 168 | .unwrap() 169 | .enum_values, 170 | vec!["Plain"] 171 | ); 172 | let schema = component!("EAdjtagged"); 173 | assert_eq!(schema.one_of.len(), 4); 174 | for s in &schema.one_of { 175 | assert_eq!(s.unwrap().unwrap().schema_type, Some(openapi::Type::Object)); 176 | assert_eq!( 177 | s.unwrap().unwrap().properties["tag"] 178 | .unwrap() 179 | .unwrap() 180 | .schema_type, 181 | Some(openapi::Type::String) 182 | ); 183 | } 184 | assert_eq!( 185 | schema.one_of[0].unwrap().unwrap().required, 186 | vec!["tag", "content"] 187 | ); 188 | assert_eq!( 189 | schema.one_of[0].unwrap().unwrap().properties["tag"] 190 | .unwrap() 191 | .unwrap() 192 | .enum_values, 193 | vec!["A"] 194 | ); 195 | assert_eq!( 196 | schema.one_of[0].unwrap().unwrap().properties["content"] 197 | .unwrap() 198 | .unwrap() 199 | .schema_type, 200 | Some(openapi::Type::String) 201 | ); 202 | assert_eq!( 203 | schema.one_of[1].unwrap().unwrap().required, 204 | vec!["tag", "content"] 205 | ); 206 | assert_eq!( 207 | schema.one_of[1].unwrap().unwrap().properties["tag"] 208 | .unwrap() 209 | .unwrap() 210 | .enum_values, 211 | vec!["B"] 212 | ); 213 | assert_eq!( 214 | schema.one_of[1].unwrap().unwrap().properties["content"] 215 | .unwrap() 216 | .unwrap() 217 | .schema_type, 218 | Some(openapi::Type::Integer) 219 | ); 220 | assert_eq!( 221 | schema.one_of[2].unwrap().unwrap().required, 222 | vec!["tag", "content"] 223 | ); 224 | assert_eq!( 225 | schema.one_of[2].unwrap().unwrap().properties["tag"] 226 | .unwrap() 227 | .unwrap() 228 | .enum_values, 229 | vec!["Stru"] 230 | ); 231 | assert_eq!( 232 | schema.one_of[2].unwrap().unwrap().properties["content"] 233 | .unwrap() 234 | .unwrap() 235 | .schema_type, 236 | Some(openapi::Type::Object) 237 | ); 238 | assert_eq!(schema.one_of[3].unwrap().unwrap().required, vec!["tag"]); 239 | assert_eq!( 240 | schema.one_of[3].unwrap().unwrap().properties["tag"] 241 | .unwrap() 242 | .unwrap() 243 | .enum_values, 244 | vec!["Plain"] 245 | ); 246 | let schema = component!("EUntagged"); 247 | assert_eq!(schema.one_of.len(), 3); 248 | assert_eq!( 249 | schema.one_of[0].unwrap().unwrap().schema_type, 250 | Some(openapi::Type::String) 251 | ); 252 | assert_eq!( 253 | schema.one_of[1].unwrap().unwrap().schema_type, 254 | Some(openapi::Type::Integer) 255 | ); 256 | assert_eq!( 257 | schema.one_of[2].unwrap().unwrap().schema_type, 258 | Some(openapi::Type::Object) 259 | ); 260 | } 261 | -------------------------------------------------------------------------------- /tests/openapi_enumset.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | #![cfg(feature = "enumset")] 3 | 4 | use enumset::*; 5 | use rweb::*; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(EnumSetType, Schema, Serialize, Deserialize, Debug)] 9 | pub enum Flagged { 10 | A, 11 | B, 12 | C, 13 | } 14 | 15 | #[derive(EnumSetType, Schema, Serialize, Deserialize, Debug)] 16 | #[enumset(serialize_as_list)] 17 | pub enum Named { 18 | A, 19 | B, 20 | C, 21 | } 22 | 23 | #[derive(Schema, Serialize, Deserialize, Debug)] 24 | #[schema(component = "Components")] 25 | struct Components { 26 | flagged: EnumSet, 27 | named: EnumSet, 28 | } 29 | 30 | #[get("/")] 31 | fn index(_: Json) -> String { 32 | String::new() 33 | } 34 | 35 | #[test] 36 | fn description() { 37 | let (spec, _) = openapi::spec().build(|| { 38 | // 39 | index() 40 | }); 41 | let schemas = &spec.components.as_ref().unwrap().schemas; 42 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 43 | macro_rules! component { 44 | ($cn:expr) => { 45 | match schemas.get($cn) { 46 | Some(openapi::ObjectOrReference::Object(s)) => s, 47 | Some(..) => panic!("Component schema can't be a reference"), 48 | None => panic!("No component schema for {}", $cn), 49 | } 50 | }; 51 | } 52 | let components = component!("Components"); 53 | macro_rules! unpack { 54 | ($opt:expr) => { 55 | $opt.unwrap().unwrap().unwrap() 56 | }; 57 | } 58 | macro_rules! prop { 59 | ($prop:expr) => { 60 | unpack!(components.properties.get($prop)) 61 | }; 62 | } 63 | let flagged = prop!("flagged"); 64 | assert_eq!(flagged.schema_type, Some(openapi::Type::Integer)); 65 | let named = prop!("named"); 66 | assert_eq!(named.schema_type, Some(openapi::Type::Array)); 67 | assert!(named.items.is_some()); 68 | } 69 | -------------------------------------------------------------------------------- /tests/openapi_generic_multi_struct.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::{openapi::*, *}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Schema)] 8 | #[schema(component = "One")] 9 | pub struct One {} 10 | 11 | #[derive(Debug, Serialize, Deserialize, Schema)] 12 | #[schema(component = "Two")] 13 | pub struct Two {} 14 | 15 | #[derive(Debug, Serialize, Deserialize, Schema)] 16 | #[schema(component = "GenericStruct")] 17 | struct GenericStruct { 18 | a: A, 19 | b: HashMap, 20 | } 21 | 22 | #[get("/")] 23 | fn test_r( 24 | _: Query, u64>>, 25 | _: Json, Option, One>>>>>, 26 | ) -> String { 27 | String::new() 28 | } 29 | 30 | #[test] 31 | fn test_multi_generics_compile() { 32 | let (spec, _) = openapi::spec().build(|| test_r()); 33 | let schemas = &spec.components.as_ref().unwrap().schemas; 34 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 35 | for (name, _) in schemas { 36 | assert!(name 37 | .chars() 38 | .all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')) 39 | } 40 | assert!(schemas.contains_key("One")); 41 | assert!(schemas.contains_key("Two")); 42 | assert!(schemas.contains_key("GenericStruct-string_Opt_uinteger-")); 43 | assert!(schemas.contains_key("One_Opt")); 44 | assert!(schemas.contains_key("GenericStruct-One_Opt_One-_Opt")); 45 | macro_rules! component { 46 | ($cn:expr) => { 47 | match schemas.get($cn) { 48 | Some(ObjectOrReference::Object(s)) => s, 49 | Some(..) => panic!("Component schema can't be a reference"), 50 | None => panic!("No component schema for {}", $cn), 51 | } 52 | }; 53 | } 54 | assert_eq!( 55 | &component!("GenericStruct-One_Opt_One-_Opt").nullable, 56 | &Some(true) 57 | ); 58 | assert_eq!( 59 | &component!("GenericStruct-One_Opt_One-_Opt").properties["a"], 60 | &ComponentOrInlineSchema::Component { 61 | name: rt::Cow::Borrowed("One_Opt") 62 | } 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /tests/openapi_generic_struct.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Schema)] 7 | #[schema(component = "GenericStruct")] 8 | struct GenericStruct { 9 | a: A, 10 | b: B, 11 | } 12 | 13 | #[derive(Debug, Serialize, Deserialize, Schema)] 14 | #[schema(component = "GenericStructWithConst")] 15 | struct GenericStructWithConst { 16 | a: A, 17 | } 18 | 19 | #[get("/")] 20 | fn test_r( 21 | _: Query>, 22 | _: Json< 23 | GenericStruct< 24 | GenericStruct>, 25 | GenericStruct>, 16>>, 26 | >, 27 | >, 28 | ) -> String { 29 | String::new() 30 | } 31 | 32 | #[test] 33 | fn test_generics_compile() { 34 | let (spec, _) = openapi::spec().build(|| test_r()); 35 | let schemas = &spec.components.as_ref().unwrap().schemas; 36 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 37 | for (name, _) in schemas { 38 | assert!(name 39 | .chars() 40 | .all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/openapi_multi_struct.rs: -------------------------------------------------------------------------------- 1 | //! https://github.com/kdy1/rweb/issues/38 2 | 3 | #![cfg(feature = "openapi")] 4 | 5 | use rweb::*; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Debug, Serialize, Deserialize, Schema)] 9 | #[schema(component = "One")] 10 | pub struct One {} 11 | 12 | #[derive(Debug, Serialize, Deserialize, Schema)] 13 | #[schema(component = "Two")] 14 | pub struct Two {} 15 | 16 | #[derive(Debug, Serialize, Deserialize, Schema)] 17 | #[schema(component = "Three")] 18 | pub struct Three { 19 | two: Two, 20 | list_of_opt_of_one: Vec>, 21 | wrapper: Wrapper, 22 | unit: Unit, 23 | } 24 | 25 | #[derive(Debug, Serialize, Deserialize, Schema)] 26 | #[schema(component = "Wrapper")] 27 | pub struct Wrapper(One); 28 | 29 | #[derive(Debug, Serialize, Deserialize, Schema)] 30 | pub struct Unit; 31 | 32 | #[derive(Debug, Serialize, Deserialize, Schema)] 33 | struct Relevant { 34 | one: One, 35 | three: Three, 36 | } 37 | 38 | #[get("/")] 39 | fn index(_: Query) -> String { 40 | String::new() 41 | } 42 | 43 | #[test] 44 | fn description() { 45 | let (spec, _) = openapi::spec().build(|| { 46 | // 47 | index() 48 | }); 49 | let schemas = &spec.components.as_ref().unwrap().schemas; 50 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 51 | macro_rules! component { 52 | ($cn:expr) => { 53 | match schemas.get($cn) { 54 | Some(openapi::ObjectOrReference::Object(s)) => s, 55 | Some(..) => panic!("Component schema can't be a reference"), 56 | None => panic!("No component schema for {}", $cn), 57 | } 58 | }; 59 | } 60 | assert!(schemas.contains_key("One")); 61 | assert!(schemas.contains_key("Two")); 62 | assert!(schemas.contains_key("Three")); 63 | assert!(schemas.contains_key("One_Opt")); 64 | assert!(!schemas.contains_key("One_Opt_List")); 65 | assert!(!schemas.contains_key("One_List")); 66 | assert!(!schemas.contains_key("Two_List")); 67 | assert!(!schemas.contains_key("Three_List")); 68 | assert!(!schemas.contains_key("Two_Opt")); 69 | assert!(!schemas.contains_key("Three_Opt")); 70 | assert!(!schemas.contains_key("Wrapper")); 71 | assert!(!schemas.contains_key("Unit")); 72 | let schema = component!("Three"); 73 | assert_eq!( 74 | schema.properties["wrapper"], 75 | openapi::ComponentOrInlineSchema::Component { 76 | name: std::borrow::Cow::Borrowed("One") 77 | } 78 | ); 79 | assert_eq!( 80 | schema.properties["unit"].unwrap().unwrap().schema_type, 81 | Some(openapi::Type::Object) 82 | ); 83 | assert_eq!( 84 | schema.properties["unit"].unwrap().unwrap().nullable, 85 | Some(true) 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /tests/openapi_rename.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[test] 7 | fn struct_field() { 8 | #[derive(Debug, Serialize, Deserialize, Schema)] 9 | struct Data { 10 | #[serde(rename = "result")] 11 | data: String, 12 | } 13 | 14 | #[get("/")] 15 | fn index(_: Query) -> String { 16 | String::new() 17 | } 18 | 19 | let (spec, _) = openapi::spec().build(|| { 20 | // 21 | index() 22 | }); 23 | 24 | assert!(spec.paths.get("/").is_some()); 25 | assert!(spec.paths.get("/").unwrap().get.is_some()); 26 | 27 | let yaml = serde_yaml::to_string(&spec).unwrap(); 28 | println!("{}", yaml); 29 | 30 | assert!(yaml.contains("result")); 31 | } 32 | 33 | #[test] 34 | fn struct_rename_all() { 35 | #[derive(Debug, Serialize, Deserialize, Schema)] 36 | #[serde(deny_unknown_fields)] 37 | #[serde(rename_all = "camelCase")] 38 | struct Data { 39 | data_msg: String, 40 | } 41 | #[derive(Debug, Serialize, Deserialize, Schema)] 42 | #[serde(deny_unknown_fields, rename_all = "SCREAMING_SNAKE_CASE")] 43 | struct Data2 { 44 | data2_msg: String, 45 | } 46 | 47 | #[get("/")] 48 | fn index(_: Query) -> Json { 49 | Json::from(Data2 { 50 | data2_msg: String::new(), 51 | }) 52 | } 53 | 54 | let (spec, _) = openapi::spec().build(|| { 55 | // 56 | index() 57 | }); 58 | 59 | assert!(spec.paths.get("/").is_some()); 60 | assert!(spec.paths.get("/").unwrap().get.is_some()); 61 | 62 | let yaml = serde_yaml::to_string(&spec).unwrap(); 63 | println!("{}", yaml); 64 | 65 | assert!(yaml.contains("dataMsg")); 66 | assert!(yaml.contains("DATA2_MSG")); 67 | } 68 | 69 | #[test] 70 | fn clike_enum() { 71 | #[derive(Debug, Serialize, Deserialize, Schema)] 72 | enum Enum { 73 | #[serde(rename = "a-a-a")] 74 | A, 75 | #[serde(rename = "b-b-b")] 76 | B, 77 | #[serde(rename = "c-c-c")] 78 | C, 79 | } 80 | 81 | #[derive(Debug, Serialize, Deserialize, Schema)] 82 | struct Data { 83 | e: Enum, 84 | } 85 | 86 | #[get("/")] 87 | fn index(_: Query) -> String { 88 | String::new() 89 | } 90 | 91 | let (spec, _) = openapi::spec().build(|| { 92 | // 93 | index() 94 | }); 95 | 96 | assert!(spec.paths.get("/").is_some()); 97 | assert!(spec.paths.get("/").unwrap().get.is_some()); 98 | 99 | let yaml = serde_yaml::to_string(&spec).unwrap(); 100 | println!("{}", yaml); 101 | 102 | assert!(yaml.contains("a-a-a")); 103 | assert!(yaml.contains("b-b-b")); 104 | assert!(yaml.contains("c-c-c")); 105 | } 106 | 107 | #[test] 108 | fn enum_field() { 109 | #[derive(Debug, Serialize, Deserialize, Schema)] 110 | enum Enum { 111 | Msg { 112 | #[serde(rename = "msg")] 113 | message: String, 114 | }, 115 | } 116 | 117 | #[derive(Debug, Serialize, Deserialize, Schema)] 118 | struct Data { 119 | #[serde(rename = "result")] 120 | data: Enum, 121 | } 122 | 123 | #[get("/")] 124 | fn index(_: Query) -> String { 125 | String::new() 126 | } 127 | 128 | let (spec, _) = openapi::spec().build(|| { 129 | // 130 | index() 131 | }); 132 | 133 | assert!(spec.paths.get("/").is_some()); 134 | assert!(spec.paths.get("/").unwrap().get.is_some()); 135 | 136 | let yaml = serde_yaml::to_string(&spec).unwrap(); 137 | println!("{}", yaml); 138 | 139 | assert!(yaml.contains("msg")); 140 | } 141 | 142 | #[test] 143 | fn enum_rename_all() { 144 | #[derive(Debug, Serialize, Deserialize, Schema)] 145 | struct Resp { 146 | data: String, 147 | } 148 | 149 | #[derive(Debug, Serialize, Deserialize, Schema)] 150 | #[serde(rename_all = "camelCase")] 151 | enum Enum { 152 | A(String), 153 | B { resp_data: Resp }, 154 | } 155 | 156 | #[derive(Debug, Serialize, Deserialize, Schema)] 157 | struct Data { 158 | data: Enum, 159 | } 160 | 161 | #[get("/")] 162 | fn index(_: Query) -> String { 163 | String::new() 164 | } 165 | 166 | let (spec, _) = openapi::spec().build(|| { 167 | // 168 | index() 169 | }); 170 | 171 | assert!(spec.paths.get("/").is_some()); 172 | assert!(spec.paths.get("/").unwrap().get.is_some()); 173 | 174 | let yaml = serde_yaml::to_string(&spec).unwrap(); 175 | println!("{}", yaml); 176 | 177 | assert!(yaml.contains("respData")); 178 | } 179 | 180 | #[test] 181 | fn enum_rename_all_variant() { 182 | #[derive(Debug, Serialize, Deserialize, Schema)] 183 | #[serde(rename_all = "camelCase")] 184 | enum Enum { 185 | Foo, 186 | Bar, 187 | } 188 | 189 | #[derive(Debug, Serialize, Deserialize, Schema)] 190 | struct Data { 191 | data: Enum, 192 | } 193 | 194 | #[get("/")] 195 | fn index(_: Query) -> String { 196 | String::new() 197 | } 198 | 199 | let (spec, _) = openapi::spec().build(|| { 200 | // 201 | index() 202 | }); 203 | 204 | assert!(spec.paths.get("/").is_some()); 205 | assert!(spec.paths.get("/").unwrap().get.is_some()); 206 | 207 | let yaml = serde_yaml::to_string(&spec).unwrap(); 208 | println!("{}", yaml); 209 | 210 | assert!(yaml.contains("foo")); 211 | assert!(yaml.contains("bar")); 212 | } 213 | -------------------------------------------------------------------------------- /tests/openapi_request.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use rweb_openapi::v3_0::ObjectOrReference; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_yaml; 7 | 8 | #[derive(Debug, Default, Serialize, Deserialize, Schema)] 9 | pub struct Product { 10 | pub id: String, 11 | pub title: String, 12 | } 13 | 14 | #[post("/json")] 15 | fn json(_: Json) -> String { 16 | String::new() 17 | } 18 | 19 | #[post("/form")] 20 | fn form(_: Form) -> String { 21 | String::new() 22 | } 23 | 24 | #[test] 25 | fn description() { 26 | let (spec, _) = openapi::spec().build(|| json().or(form())); 27 | 28 | assert!(spec.paths.get("/json").is_some()); 29 | assert!(spec.paths.get("/form").is_some()); 30 | 31 | assert!(spec.paths.get("/json").unwrap().post.is_some()); 32 | assert!(spec.paths.get("/form").unwrap().post.is_some()); 33 | 34 | assert!(spec 35 | .paths 36 | .get("/json") 37 | .unwrap() 38 | .post 39 | .as_ref() 40 | .unwrap() 41 | .request_body 42 | .is_some()); 43 | assert!(spec 44 | .paths 45 | .get("/form") 46 | .unwrap() 47 | .post 48 | .as_ref() 49 | .unwrap() 50 | .request_body 51 | .is_some()); 52 | 53 | match spec 54 | .paths 55 | .get("/json") 56 | .unwrap() 57 | .post 58 | .as_ref() 59 | .unwrap() 60 | .request_body 61 | .as_ref() 62 | .unwrap() 63 | { 64 | ObjectOrReference::Object(request_body) => { 65 | assert!(request_body.content.contains_key("application/json")); 66 | } 67 | ObjectOrReference::Ref { .. } => { 68 | panic!("Struct Product dont have `#[schema(component = \"...\")]`") 69 | } 70 | } 71 | 72 | match spec 73 | .paths 74 | .get("/form") 75 | .unwrap() 76 | .post 77 | .as_ref() 78 | .unwrap() 79 | .request_body 80 | .as_ref() 81 | .unwrap() 82 | { 83 | ObjectOrReference::Object(request_body) => { 84 | assert!(request_body.content.contains_key("x-www-form-urlencoded")); 85 | } 86 | ObjectOrReference::Ref { .. } => { 87 | panic!("Struct Product dont have `#[schema(component = \"...\")]`") 88 | } 89 | } 90 | 91 | let yaml = serde_yaml::to_string(&spec).unwrap(); 92 | 93 | println!("{}", yaml); 94 | } 95 | -------------------------------------------------------------------------------- /tests/openapi_required.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | use std::borrow::Cow; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Schema)] 8 | #[schema(component = "TestStruct")] 9 | struct TestStruct { 10 | afield: String, 11 | optfield: Option, 12 | } 13 | 14 | #[get("/")] 15 | fn test_struct_r(_: Query) -> String { 16 | String::new() 17 | } 18 | 19 | #[test] 20 | fn test_struct() { 21 | let (spec, _) = openapi::spec().build(|| test_struct_r()); 22 | let schema = match spec 23 | .components 24 | .as_ref() 25 | .unwrap() 26 | .schemas 27 | .get("TestStruct") 28 | .unwrap() 29 | { 30 | openapi::ObjectOrReference::Object(s) => s, 31 | _ => panic!(), 32 | }; 33 | println!("{}", serde_yaml::to_string(&schema).unwrap()); 34 | assert!(schema.required.contains(&Cow::Borrowed("afield"))); 35 | assert!(!schema.required.contains(&Cow::Borrowed("optfield"))); 36 | } 37 | 38 | #[derive(Debug, Serialize, Deserialize, Schema)] 39 | #[schema(component = "TestEnum")] 40 | enum TestEnum { 41 | AThing { 42 | afield: String, 43 | }, 44 | MaybeThing { 45 | optfield: Option, 46 | }, 47 | TwoThings { 48 | afield: String, 49 | optfield: Option, 50 | }, 51 | } 52 | 53 | #[get("/")] 54 | fn test_enum_r(_: Json) -> String { 55 | String::new() 56 | } 57 | 58 | #[test] 59 | fn test_enum() { 60 | let (spec, _) = openapi::spec().build(|| test_enum_r()); 61 | let schema = match spec 62 | .components 63 | .as_ref() 64 | .unwrap() 65 | .schemas 66 | .get("TestEnum") 67 | .unwrap() 68 | { 69 | openapi::ObjectOrReference::Object(s) => s, 70 | _ => panic!(), 71 | }; 72 | println!("{}", serde_yaml::to_string(&schema).unwrap()); 73 | for variant in &schema.one_of { 74 | match variant { 75 | openapi::ComponentOrInlineSchema::Inline(vs) => { 76 | if vs.properties.contains_key(&Cow::Borrowed("afield")) { 77 | assert!(vs.required.contains(&Cow::Borrowed("afield"))); 78 | } 79 | if vs.properties.contains_key(&Cow::Borrowed("optfield")) { 80 | assert!(!vs.required.contains(&Cow::Borrowed("optfield"))); 81 | } 82 | } 83 | _ => panic!(), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/openapi_response.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::{ 4 | rt::{Cow, IndexMap}, 5 | *, 6 | }; 7 | use rweb_openapi::v3_0::Response; 8 | use serde::Serialize; 9 | 10 | #[derive(Debug, Schema)] 11 | enum Error {} 12 | 13 | impl openapi::ResponseEntity for Error { 14 | fn describe_responses(_: &mut openapi::ComponentDescriptor) -> openapi::Responses { 15 | let mut map = IndexMap::new(); 16 | 17 | map.insert( 18 | Cow::Borrowed("404"), 19 | Response { 20 | description: Cow::Borrowed("Product not found"), 21 | ..Default::default() 22 | }, 23 | ); 24 | 25 | map 26 | } 27 | } 28 | 29 | #[derive(Debug, Serialize, Schema)] 30 | struct Resp { 31 | status: usize, 32 | data: T, 33 | } 34 | 35 | #[get("/")] 36 | fn index() -> Result>, Error> { 37 | unimplemented!() 38 | } 39 | 40 | #[derive(Debug, Serialize, Schema)] 41 | struct Product {} 42 | 43 | #[get("/product")] 44 | fn product() -> Result, Error> { 45 | unimplemented!() 46 | } 47 | 48 | #[get("/product")] 49 | fn products() -> Result>, Error> { 50 | unimplemented!() 51 | } 52 | 53 | #[test] 54 | fn component_test() { 55 | let (spec, _) = openapi::spec().build(|| { 56 | // 57 | index() 58 | }); 59 | 60 | assert!(spec.paths.get("/").is_some()); 61 | assert!(spec.paths.get("/").unwrap().get.is_some()); 62 | 63 | let yaml = serde_yaml::to_string(&spec).unwrap(); 64 | println!("{}", yaml); 65 | } 66 | 67 | #[derive(Debug, Serialize, Schema)] 68 | #[schema(component = "Item")] 69 | struct Item {} 70 | 71 | #[get("/item")] 72 | fn item() -> Result, Error> { 73 | unimplemented!() 74 | } 75 | 76 | #[test] 77 | fn component_in_response() { 78 | let (spec, _) = openapi::spec().build(|| item()); 79 | assert!(spec.paths.get("/item").is_some()); 80 | assert!(spec.paths.get("/item").unwrap().get.is_some()); 81 | assert!(spec.components.unwrap().schemas.get("Item").is_some()); 82 | } 83 | 84 | #[get("/errable")] 85 | #[openapi(response(code = "417", description = "🍵"))] 86 | #[openapi(response(code = "5XX", description = "😵"))] 87 | #[openapi(response(code = 200, description = "🐛"))] 88 | #[openapi(response(code = 201, description = "✨", schema = "Json>"))] 89 | fn errable() -> Json<()> { 90 | unimplemented!() 91 | } 92 | 93 | #[test] 94 | fn response_code_in_response() { 95 | let (spec, _) = openapi::spec().build(|| errable()); 96 | let op = spec.paths.get("/errable").unwrap().get.as_ref().unwrap(); 97 | assert!(op.responses.get("417").is_some()); 98 | assert_eq!(op.responses.get("417").unwrap().description, "🍵"); 99 | assert!(op.responses.get("5XX").is_some()); 100 | assert_eq!(op.responses.get("5XX").unwrap().description, "😵"); 101 | assert_eq!(op.responses.get("200").unwrap().description, "🐛"); 102 | assert!(op 103 | .responses 104 | .get("201") 105 | .unwrap() 106 | .content 107 | .get("application/json") 108 | .is_some()) 109 | } 110 | -------------------------------------------------------------------------------- /tests/openapi_result_extagged.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Schema, Serialize, Deserialize, Debug)] 7 | #[schema(component = "IHazResult")] 8 | pub struct IHazResult { 9 | result: Result, 10 | } 11 | 12 | #[get("/")] 13 | fn index(_: Json) -> String { 14 | String::new() 15 | } 16 | 17 | #[test] 18 | fn description() { 19 | let (spec, _) = openapi::spec().build(|| { 20 | // 21 | index() 22 | }); 23 | let schemas = &spec.components.as_ref().unwrap().schemas; 24 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 25 | macro_rules! component { 26 | ($cn:expr) => { 27 | match schemas.get($cn) { 28 | Some(openapi::ObjectOrReference::Object(s)) => s, 29 | Some(..) => panic!("Component schema can't be a reference"), 30 | None => panic!("No component schema for {}", $cn), 31 | } 32 | }; 33 | } 34 | macro_rules! object { 35 | ($o:expr) => { 36 | match $o { 37 | openapi::ComponentOrInlineSchema::Inline(s) => s, 38 | _ => panic!("Expected object, not reference"), 39 | } 40 | }; 41 | } 42 | let res = object!(&component!("IHazResult").properties["result"]); 43 | assert_eq!( 44 | object!(&res.one_of[0]).schema_type, 45 | Some(openapi::Type::Object) 46 | ); 47 | assert_eq!( 48 | object!(&object!(&res.one_of[0]).properties["Ok"]).schema_type, 49 | Some(openapi::Type::Integer) 50 | ); 51 | assert_eq!( 52 | object!(&res.one_of[1]).schema_type, 53 | Some(openapi::Type::Object) 54 | ); 55 | assert_eq!( 56 | object!(&object!(&res.one_of[1]).properties["Err"]).schema_type, 57 | Some(openapi::Type::String) 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /tests/openapi_schema.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Schema)] 7 | #[schema(component = "Item")] 8 | struct ComponentTestReq { 9 | data: String, 10 | } 11 | 12 | #[get("/")] 13 | fn component(_: Query) -> String { 14 | String::new() 15 | } 16 | 17 | #[test] 18 | fn component_test() { 19 | let (spec, _) = openapi::spec().build(|| { 20 | // 21 | component() 22 | }); 23 | 24 | assert!(spec.paths.get("/").is_some()); 25 | assert!(spec.paths.get("/").unwrap().get.is_some()); 26 | 27 | let yaml = serde_yaml::to_string(&spec).unwrap(); 28 | println!("{}", yaml); 29 | } 30 | 31 | #[derive(Debug, Deserialize, Schema)] 32 | struct ExampleReq { 33 | #[schema(example = "10")] 34 | limit: usize, 35 | data: String, 36 | } 37 | 38 | #[get("/")] 39 | fn example(_: Query) -> String { 40 | String::new() 41 | } 42 | 43 | #[test] 44 | fn example_test() { 45 | let (spec, _) = openapi::spec().build(|| { 46 | // 47 | example() 48 | }); 49 | 50 | assert!(spec.paths.get("/").is_some()); 51 | assert!(spec.paths.get("/").unwrap().get.is_some()); 52 | 53 | let yaml = serde_yaml::to_string(&spec).unwrap(); 54 | println!("{}", yaml); 55 | 56 | assert!(yaml.contains("10")); 57 | } 58 | -------------------------------------------------------------------------------- /tests/openapi_serde_skip.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | 3 | use rweb::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug, Serialize, Deserialize, Schema)] 7 | #[schema(component = "Item")] 8 | struct ComponentTestReq { 9 | data: String, 10 | #[serde(skip)] 11 | not_schema: NotSchema, 12 | } 13 | 14 | #[derive(Debug, Default, Serialize, Deserialize)] 15 | pub struct NotSchema {} 16 | 17 | #[get("/")] 18 | fn example(_: Query) -> String { 19 | String::new() 20 | } 21 | -------------------------------------------------------------------------------- /tests/openapi_skip.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "openapi")] 2 | #![allow(dead_code)] 3 | 4 | use rweb::*; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Deserialize, rweb::Schema)] 8 | #[schema(component = "Whence")] 9 | struct Whence { 10 | always: u64, 11 | #[serde(skip_deserializing)] 12 | only_yeet: u64, 13 | #[serde(skip_serializing)] 14 | only_take: u64, 15 | #[serde(skip)] 16 | nevah: u64, 17 | } 18 | 19 | #[get("/")] 20 | fn index(_: Query) -> String { 21 | String::new() 22 | } 23 | 24 | #[test] 25 | fn test_skip() { 26 | let (spec, _) = openapi::spec().build(|| index()); 27 | let schemas = &spec.components.as_ref().unwrap().schemas; 28 | println!("{}", serde_yaml::to_string(&schemas).unwrap()); 29 | let whence = match schemas.get("Whence").unwrap() { 30 | rweb::openapi::ObjectOrReference::Object(s) => s, 31 | _ => panic!(), 32 | }; 33 | macro_rules! unpack { 34 | ($opt:expr) => { 35 | $opt.unwrap().unwrap().unwrap() 36 | }; 37 | } 38 | macro_rules! prop { 39 | ($prop:expr) => { 40 | unpack!(whence.properties.get($prop)) 41 | }; 42 | } 43 | assert!(whence.properties.contains_key("always")); 44 | assert!(whence.properties.contains_key("only_yeet")); 45 | assert!(whence.properties.contains_key("only_take")); 46 | assert!(!whence.properties.contains_key("nevah")); 47 | assert_eq!(prop!("always").read_only, None); 48 | assert_eq!(prop!("always").write_only, None); 49 | assert_eq!(prop!("only_yeet").read_only, Some(true)); 50 | assert_eq!(prop!("only_yeet").write_only, None); 51 | assert_eq!(prop!("only_take").read_only, None); 52 | assert_eq!(prop!("only_take").write_only, Some(true)); 53 | } 54 | -------------------------------------------------------------------------------- /tests/path_arg.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "openapi"))] 2 | 3 | use http::Error; 4 | use rweb::{get, Filter}; 5 | 6 | #[get("/")] 7 | fn index() -> Result { 8 | Ok(String::new()) 9 | } 10 | 11 | #[get("/foo")] 12 | fn foo() -> Result { 13 | Ok(String::new()) 14 | } 15 | 16 | #[get("/param/{foo}")] 17 | fn param(foo: String) -> Result { 18 | Ok(foo) 19 | } 20 | 21 | #[get("/param/{v}")] 22 | fn param_typed(v: u32) -> Result { 23 | Ok(v.to_string()) 24 | } 25 | 26 | #[get("/param/{name}/{value}")] 27 | fn multiple_param(name: String, value: String) -> Result { 28 | Ok(format!("{}={}", name, value)) 29 | } 30 | 31 | #[get("/param/{name}/{value}")] 32 | fn multiple_param_ordered(name: String, value: u8) -> Result { 33 | Ok(format!("{}={}", name, value)) 34 | } 35 | 36 | #[get("/param/{name}/{value}")] 37 | fn multiple_param_unordered(value: u8, name: String) -> Result { 38 | Ok(format!("{}={}", name, value)) 39 | } 40 | 41 | #[test] 42 | fn bind() { 43 | rweb::serve( 44 | index() 45 | .or(foo()) 46 | .or(param()) 47 | .or(param_typed()) 48 | .or(multiple_param()) 49 | .or(multiple_param_ordered()) 50 | .or(multiple_param_unordered()), 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /tests/query_arg.rs: -------------------------------------------------------------------------------- 1 | use rweb::*; 2 | 3 | #[get("/")] 4 | fn use_query(#[query] qs: String) -> String { 5 | qs 6 | } 7 | 8 | #[tokio::test] 9 | async fn query_str() { 10 | let value = warp::test::request() 11 | .path("/?q=s") 12 | .reply(&use_query()) 13 | .await 14 | .into_body(); 15 | 16 | assert_eq!(value, b"q=s"[..]); 17 | } 18 | -------------------------------------------------------------------------------- /tests/router.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use rweb::*; 3 | 4 | #[get("/sum/{a}/{b}")] 5 | fn sum(a: usize, b: usize) -> String { 6 | (a + b).to_string() 7 | } 8 | 9 | #[get("/mul/{a}/{b}")] 10 | fn mul(a: usize, b: usize) -> String { 11 | (a * b).to_string() 12 | } 13 | 14 | #[get("/no-arg")] 15 | fn no_arg() -> String { 16 | String::new() 17 | } 18 | 19 | #[tokio::test] 20 | async fn math_test() { 21 | #[router("/math", services(sum, mul))] 22 | fn math() {} 23 | 24 | let value = warp::test::request() 25 | .path("/math/sum/1/2") 26 | .reply(&math()) 27 | .await; 28 | assert_eq!(value.status(), StatusCode::OK); 29 | assert_eq!(value.into_body(), b"3"[..]); 30 | } 31 | 32 | #[tokio::test] 33 | async fn arg_cnt_test() { 34 | #[router("/math/complex", services(sum, mul, no_arg))] 35 | fn arg_cnt() {} 36 | 37 | let value = warp::test::request() 38 | .path("/math/complex/sum/1/2") 39 | .reply(&arg_cnt()) 40 | .await; 41 | assert_eq!(value.status(), StatusCode::OK); 42 | assert_eq!(value.into_body(), b"3"[..]); 43 | } 44 | 45 | #[derive(Default, Clone)] 46 | struct Db {} 47 | 48 | #[get("/use")] 49 | fn use_db(#[data] _db: Db) -> String { 50 | String::new() 51 | } 52 | 53 | #[router("/data", services(use_db))] 54 | fn data_param(#[data] db: Db) {} 55 | 56 | #[tokio::test] 57 | async fn data_param_test() { 58 | let value = warp::test::request() 59 | .path("/data/use") 60 | .reply(&data_param(Db::default())) 61 | .await; 62 | assert_eq!(value.status(), StatusCode::OK); 63 | assert_eq!(value.into_body(), b""[..]); 64 | } 65 | 66 | #[get("/")] 67 | fn admin_index() -> String { 68 | String::new() 69 | } 70 | 71 | #[get("/users")] 72 | fn admin_users() -> String { 73 | String::new() 74 | } 75 | 76 | #[router("/admin", services(admin_index, admin_users))] 77 | #[header("X-User-Admin", "1")] 78 | fn admin() {} 79 | -------------------------------------------------------------------------------- /tests/state.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "openapi"))] 2 | 3 | use futures::lock::Mutex; 4 | use rweb::*; 5 | use std::sync::Arc; 6 | 7 | #[derive(Clone)] 8 | struct Db { 9 | items: Arc>>, 10 | } 11 | 12 | #[get("/")] 13 | async fn index(#[data] db: Db) -> Result { 14 | let items = db.items.lock().await; 15 | 16 | Ok(items.len().to_string()) 17 | } 18 | --------------------------------------------------------------------------------