├── .github └── workflows │ ├── clippy.yml │ ├── code-coverage.yml │ ├── nightly-test.yml │ ├── release.yml │ ├── security-audit.yml │ └── stable-test.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── author.txt ├── cert.pem ├── css │ └── table.css ├── key.pem └── welcome.html ├── examples ├── echo.rs ├── hello-world.rs ├── https.rs ├── restful-api.rs ├── serve-file.rs ├── websocket-echo.rs └── welcome.rs ├── integration ├── diesel-example │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── bin │ │ │ └── api.rs │ │ ├── data_object.rs │ │ ├── endpoints.rs │ │ ├── lib.rs │ │ ├── models.rs │ │ └── schema.rs │ └── tests │ │ └── restful.rs ├── juniper-example │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── main.rs │ │ ├── models.rs │ │ └── schema.rs ├── multipart-example │ ├── Cargo.toml │ ├── README.md │ ├── assets │ │ └── index.html │ ├── src │ │ └── main.rs │ └── upload │ │ └── .gitkeep └── websocket-example │ ├── Cargo.toml │ ├── README.md │ └── src │ └── main.rs ├── roa-async-std ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── listener.rs │ ├── net.rs │ └── runtime.rs ├── roa-core ├── Cargo.toml ├── README.md └── src │ ├── app.rs │ ├── app │ ├── future.rs │ ├── runtime.rs │ └── stream.rs │ ├── body.rs │ ├── context.rs │ ├── context │ └── storage.rs │ ├── err.rs │ ├── executor.rs │ ├── group.rs │ ├── lib.rs │ ├── middleware.rs │ ├── request.rs │ ├── response.rs │ └── state.rs ├── roa-diesel ├── Cargo.toml ├── README.md └── src │ ├── async_ext.rs │ ├── lib.rs │ └── pool.rs ├── roa-juniper ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── roa ├── Cargo.toml ├── README.md ├── src │ ├── body.rs │ ├── body │ │ ├── file.rs │ │ └── file │ │ │ ├── content_disposition.rs │ │ │ └── help.rs │ ├── compress.rs │ ├── cookie.rs │ ├── cors.rs │ ├── forward.rs │ ├── jsonrpc.rs │ ├── jwt.rs │ ├── lib.rs │ ├── logger.rs │ ├── query.rs │ ├── router.rs │ ├── router │ │ ├── endpoints.rs │ │ ├── endpoints │ │ │ ├── dispatcher.rs │ │ │ └── guard.rs │ │ ├── err.rs │ │ └── path.rs │ ├── stream.rs │ ├── tcp.rs │ ├── tcp │ │ ├── incoming.rs │ │ └── listener.rs │ ├── tls.rs │ ├── tls │ │ ├── incoming.rs │ │ └── listener.rs │ └── websocket.rs └── templates │ └── user.html ├── rustfmt.toml ├── src └── lib.rs ├── templates └── directory.html └── tests ├── logger.rs ├── restful.rs └── serve-file.rs /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Clippy 3 | jobs: 4 | clippy_check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install Toolchain 9 | uses: actions-rs/toolchain@v1 10 | with: 11 | toolchain: nightly 12 | override: true 13 | components: clippy 14 | - uses: actions-rs/clippy-check@v1 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | args: --all-targets --all-features -------------------------------------------------------------------------------- /.github/workflows/code-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | check: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | toolchain: nightly 14 | override: true 15 | - name: Check all 16 | uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | args: --all --all-features 20 | cover: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: nightly 27 | override: true 28 | - name: Install libsqlite3-dev 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get install -y libsqlite3-dev 32 | - name: Run cargo-tarpaulin 33 | uses: actions-rs/tarpaulin@v0.1 34 | with: 35 | version: '0.21.0' 36 | args: --avoid-cfg-tarpaulin --out Xml --all --all-features 37 | - name: Upload to codecov.io 38 | uses: codecov/codecov-action@v1.0.2 39 | with: 40 | token: ${{secrets.CODECOV_TOKEN}} 41 | - name: Archive code coverage results 42 | uses: actions/upload-artifact@v1 43 | with: 44 | name: code-coverage-report 45 | path: cobertura.xml 46 | -------------------------------------------------------------------------------- /.github/workflows/nightly-test.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Test 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: nightly 17 | override: true 18 | - name: Check all 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: check 22 | args: --all --all-features 23 | test: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Install libsqlite3-dev 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get -y install libsqlite3-dev 30 | - uses: actions/checkout@v2 31 | - uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: nightly 35 | override: true 36 | - name: Run all tests 37 | uses: actions-rs/cargo@v1 38 | with: 39 | command: test 40 | args: --all --all-features --no-fail-fast 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - release 6 | paths: 7 | - '**/Cargo.toml' 8 | - '.github/workflows/release.yml' 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | max-parallel: 1 16 | matrix: 17 | package: 18 | - name: roa-core 19 | registryName: roa-core 20 | path: roa-core 21 | publishPath: /target/package 22 | - name: roa 23 | registryName: roa 24 | path: roa 25 | publishPath: /target/package 26 | - name: roa-juniper 27 | registryName: roa-juniper 28 | path: roa-juniper 29 | publishPath: /target/package 30 | - name: roa-diesel 31 | registryName: roa-diesel 32 | path: roa-diesel 33 | publishPath: /target/package 34 | - name: roa-async-std 35 | registryName: roa-async-std 36 | path: roa-async-std 37 | publishPath: /target/package 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | - name: Install Toolchain 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | override: true 46 | - name: install libsqlite3-dev 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y libsqlite3-dev 50 | - name: get version 51 | working-directory: ${{ matrix.package.path }} 52 | run: echo "PACKAGE_VERSION=$(sed -nE 's/^\s*version = \"(.*?)\"/\1/p' Cargo.toml)" >> $GITHUB_ENV 53 | - name: check published version 54 | run: echo "PUBLISHED_VERSION=$(cargo search ${{ matrix.package.registryName }} --limit 1 | sed -nE 's/^[^\"]*\"//; s/\".*//1p' -)" >> $GITHUB_ENV 55 | - name: cargo login 56 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 57 | run: cargo login ${{ secrets.CRATE_TOKEN }} 58 | - name: cargo package 59 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 60 | working-directory: ${{ matrix.package.path }} 61 | run: | 62 | echo "package dir:" 63 | ls 64 | cargo package 65 | echo "We will publish:" $PACKAGE_VERSION 66 | echo "This is current latest:" $PUBLISHED_VERSION 67 | echo "post package dir:" 68 | cd ${{ matrix.publishPath }} 69 | ls 70 | - name: Publish ${{ matrix.package.name }} 71 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 72 | working-directory: ${{ matrix.package.path }} 73 | run: | 74 | echo "# Cargo Publish" | tee -a ${{runner.workspace }}/notes.md 75 | echo "\`\`\`" >> ${{runner.workspace }}/notes.md 76 | cargo publish --no-verify 2>&1 | tee -a ${{runner.workspace }}/notes.md 77 | echo "\`\`\`" >> ${{runner.workspace }}/notes.md 78 | - name: Create Release 79 | id: create_crate_release 80 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 81 | uses: jbolda/create-release@v1.1.0 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | tag_name: ${{ matrix.package.name }}-v${{ env.PACKAGE_VERSION }} 86 | release_name: "Release ${{ matrix.package.name }} v${{ env.PACKAGE_VERSION }} [crates.io]" 87 | bodyFromFile: ./../notes.md 88 | draft: false 89 | prerelease: false 90 | - name: Upload Release Asset 91 | id: upload-release-asset 92 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 93 | uses: actions/upload-release-asset@v1.0.1 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 96 | with: 97 | upload_url: ${{ steps.create_crate_release.outputs.upload_url }} 98 | asset_path: ./${{ matrix.package.publishPath }}/${{ matrix.package.registryName }}-${{ env.PACKAGE_VERSION }}.crate 99 | asset_name: ${{ matrix.package.registryName }}-${{ env.PACKAGE_VERSION }}.crate 100 | asset_content_type: application/x-gtar -------------------------------------------------------------------------------- /.github/workflows/security-audit.yml: -------------------------------------------------------------------------------- 1 | name: Security Audit 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | jobs: 6 | audit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions-rs/audit-check@v1 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/stable-test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Stable Test 3 | jobs: 4 | check: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | rust: 9 | - stable 10 | - 1.60.0 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: ${{ matrix.rust }} 17 | override: true 18 | - name: Check all 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: check 22 | args: --all --features "roa/full" 23 | test: 24 | runs-on: ubuntu-latest 25 | strategy: 26 | matrix: 27 | rust: 28 | - stable 29 | - 1.60.0 30 | steps: 31 | - name: Install libsqlite3-dev 32 | run: | 33 | sudo apt-get update 34 | sudo apt-get -y install libsqlite3-dev 35 | - uses: actions/checkout@v2 36 | - uses: actions-rs/toolchain@v1 37 | with: 38 | profile: minimal 39 | toolchain: ${{ matrix.rust }} 40 | override: true 41 | - name: Run all tests 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: --all --features "roa/full" --no-fail-fast 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | **/upload/* 5 | .env 6 | node_modules -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roa-root" 3 | version = "0.6.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | license = "MIT" 7 | publish = false 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [workspace] 11 | members = [ 12 | "roa", 13 | "roa-core", 14 | "roa-diesel", 15 | "roa-async-std", 16 | "roa-juniper", 17 | "integration/diesel-example", 18 | "integration/multipart-example", 19 | "integration/websocket-example", 20 | "integration/juniper-example" 21 | ] 22 | 23 | [dev-dependencies] 24 | tokio = { version = "1.15", features = ["full"] } 25 | reqwest = { version = "0.11", features = ["json", "cookies", "gzip"] } 26 | serde = { version = "1", features = ["derive"] } 27 | roa = { path = "./roa", features = ["full"] } 28 | test-case = "1.2" 29 | once_cell = "1.8" 30 | log = "0.4" 31 | slab = "0.4.2" 32 | multimap = "0.8.0" 33 | hyper = "0.14" 34 | chrono = "0.4" 35 | mime = "0.3" 36 | encoding = "0.2" 37 | askama = "0.10" 38 | http = "0.2" 39 | bytesize = "1.1" 40 | serde_json = "1.0" 41 | tracing = "0.1" 42 | futures = "0.3" 43 | doc-comment = "0.3.3" 44 | anyhow = "1.0" 45 | tracing-futures = "0.2" 46 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Hexilee 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check: 2 | cargo check --all --features "roa/full" 3 | build: 4 | cargo build --all --features "roa/full" 5 | test: 6 | cargo test --all --features "roa/full" 7 | fmt: 8 | cargo +nightly fmt 9 | lint: 10 | cargo clippy --all-targets -- -D warnings 11 | check-all: 12 | cargo +nightly check --all --all-features 13 | test-all: 14 | cargo +nightly test --all --all-features 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Roa

3 |

Roa is an async web framework inspired by koajs, lightweight but powerful.

4 |

5 | 6 | [![Stable Test](https://github.com/Hexilee/roa/workflows/Stable%20Test/badge.svg)](https://github.com/Hexilee/roa/actions) 7 | [![codecov](https://codecov.io/gh/Hexilee/roa/branch/master/graph/badge.svg)](https://codecov.io/gh/Hexilee/roa) 8 | [![wiki](https://img.shields.io/badge/roa-wiki-purple.svg)](https://github.com/Hexilee/roa/wiki) 9 | [![Rust Docs](https://docs.rs/roa/badge.svg)](https://docs.rs/roa) 10 | [![Crate version](https://img.shields.io/crates/v/roa.svg)](https://crates.io/crates/roa) 11 | [![Download](https://img.shields.io/crates/d/roa.svg)](https://crates.io/crates/roa) 12 | [![MSRV-1.54](https://img.shields.io/badge/MSRV-1.54-blue.svg)](https://blog.rust-lang.org/2021/07/29/Rust-1.54.0.html) 13 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Hexilee/roa/blob/master/LICENSE) 14 | 15 |

16 | 17 |

18 | Examples 19 | | 20 | Guide 21 | | 22 | Cookbook 23 |

24 |
25 |
26 | 27 | 28 | #### Feature highlights 29 | 30 | - A lightweight, solid and well extensible core. 31 | - Supports HTTP/1.x and HTTP/2.0 protocols. 32 | - Full streaming. 33 | - Highly extensible middleware system. 34 | - Based on [`hyper`](https://github.com/hyperium/hyper), runtime-independent, you can chose async runtime as you like. 35 | - Many useful extensions. 36 | - Official runtime schemes: 37 | - (Default) [tokio](https://github.com/tokio-rs/tokio) runtime and TcpStream. 38 | - [async-std](https://github.com/async-rs/async-std) runtime and TcpStream. 39 | - Transparent content compression (br, gzip, deflate, zstd). 40 | - Configurable and nestable router. 41 | - Named uri parameters(query and router parameter). 42 | - Cookie and jwt support. 43 | - HTTPS support. 44 | - WebSocket support. 45 | - Asynchronous multipart form support. 46 | - Other middlewares(logger, CORS .etc). 47 | - Integrations 48 | - roa-diesel, integration with [diesel](https://github.com/diesel-rs/diesel). 49 | - roa-juniper, integration with [juniper](https://github.com/graphql-rust/juniper). 50 | - Works on stable Rust. 51 | 52 | #### Get start 53 | 54 | ```toml 55 | # Cargo.toml 56 | 57 | [dependencies] 58 | roa = "0.6" 59 | tokio = { version = "1.15", features = ["rt", "macro"] } 60 | ``` 61 | 62 | ```rust,no_run 63 | use roa::App; 64 | use roa::preload::*; 65 | 66 | #[tokio::main] 67 | async fn main() -> anyhow::Result<()> { 68 | let app = App::new().end("Hello, World"); 69 | app.listen("127.0.0.1:8000", |addr| { 70 | println!("Server is listening on {}", addr) 71 | })? 72 | .await?; 73 | Ok(()) 74 | } 75 | ``` 76 | Refer to [wiki](https://github.com/Hexilee/roa/wiki) for more details. 77 | -------------------------------------------------------------------------------- /assets/author.txt: -------------------------------------------------------------------------------- 1 | Hexilee -------------------------------------------------------------------------------- /assets/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFPjCCAyYCCQDvLYiYD+jqeTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV 3 | UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMRAwDgYDVQQKDAdDb21wYW55MQww 4 | CgYDVQQLDANPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xODAxMjUx 5 | NzQ2MDFaFw0xOTAxMjUxNzQ2MDFaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD 6 | QTELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBhbnkxDDAKBgNVBAsMA09yZzEY 7 | MBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A 8 | MIICCgKCAgEA2WzIA2IpVR9Tb9EFhITlxuhE5rY2a3S6qzYNzQVgSFggxXEPn8k1 9 | sQEcer5BfAP986Sck3H0FvB4Bt/I8PwOtUCmhwcc8KtB5TcGPR4fjXnrpC+MIK5U 10 | NLkwuyBDKziYzTdBj8kUFX1WxmvEHEgqToPOZfBgsS71cJAR/zOWraDLSRM54jXy 11 | voLZN4Ti9rQagQrvTQ44Vz5ycDQy7UxtbUGh1CVv69vNVr7/SOOh/Nw5FNOZWLWr 12 | odGyoec5wh9iqRZgRqiTUc6Lt7V2RWc2X2gjwST2UfI+U46Ip3oaQ7ZD4eAkoqND 13 | xdniBZAykVG3c/99ux4BAESTF8fsNch6UticBxYMuTu+ouvP0psfI9wwwNliJDmA 14 | CRUTB9AgRynbL1AzhqQoDfsb98IZfjfNOpwnwuLwpMAPhbgd5KNdZaIJ4Hb6/stI 15 | yFElOExxd3TAxF2Gshd/lq1JcNHAZ1DSXV5MvOWT/NWgXwbIzUgQ8eIi+HuDYX2U 16 | UuaB6R8tbd52H7rbUv6HrfinuSlKWqjSYLkiKHkwUpoMw8y9UycRSzs1E9nPwPTO 17 | vRXb0mNCQeBCV9FvStNVXdCUTT8LGPv87xSD2pmt7LijlE6mHLG8McfcWkzA69un 18 | CEHIFAFDimTuN7EBljc119xWFTcHMyoZAfFF+oTqwSbBGImruCxnaJECAwEAATAN 19 | BgkqhkiG9w0BAQsFAAOCAgEApavsgsn7SpPHfhDSN5iZs1ILZQRewJg0Bty0xPfk 20 | 3tynSW6bNH3nSaKbpsdmxxomthNSQgD2heOq1By9YzeOoNR+7Pk3s4FkASnf3ToI 21 | JNTUasBFFfaCG96s4Yvs8KiWS/k84yaWuU8c3Wb1jXs5Rv1qE1Uvuwat1DSGXSoD 22 | JNluuIkCsC4kWkyq5pWCGQrabWPRTWsHwC3PTcwSRBaFgYLJaR72SloHB1ot02zL 23 | d2age9dmFRFLLCBzP+D7RojBvL37qS/HR+rQ4SoQwiVc/JzaeqSe7ZbvEH9sZYEu 24 | ALowJzgbwro7oZflwTWunSeSGDSltkqKjvWvZI61pwfHKDahUTmZ5h2y67FuGEaC 25 | CIOUI8dSVSPKITxaq3JL4ze2e9/0Lt7hj19YK2uUmtMAW5Tirz4Yx5lyGH9U8Wur 26 | y/X8VPxTc4A9TMlJgkyz0hqvhbPOT/zSWB10zXh0glKAsSBryAOEDxV1UygmSir7 27 | YV8Qaq+oyKUTMc1MFq5vZ07M51EPaietn85t8V2Y+k/8XYltRp32NxsypxAJuyxh 28 | g/ko6RVTrWa1sMvz/F9LFqAdKiK5eM96lh9IU4xiLg4ob8aS/GRAA8oIFkZFhLrt 29 | tOwjIUPmEPyHWFi8dLpNuQKYalLYhuwZftG/9xV+wqhKGZO9iPrpHSYBRTap8w2y 30 | 1QU= 31 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /assets/css/table.css: -------------------------------------------------------------------------------- 1 | /* spacing */ 2 | 3 | table { 4 | table-layout: fixed; 5 | width: 80%; 6 | border-collapse: collapse; 7 | } 8 | 9 | thead th { 10 | text-align: left 11 | } 12 | 13 | thead th:nth-child(1) { 14 | width: 40%; 15 | } 16 | 17 | thead th:nth-child(2) { 18 | width: 20%; 19 | } 20 | 21 | thead th:nth-child(3) { 22 | width: 40%; 23 | } 24 | 25 | th, td { 26 | padding: 10px; 27 | } -------------------------------------------------------------------------------- /assets/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEA2WzIA2IpVR9Tb9EFhITlxuhE5rY2a3S6qzYNzQVgSFggxXEP 3 | n8k1sQEcer5BfAP986Sck3H0FvB4Bt/I8PwOtUCmhwcc8KtB5TcGPR4fjXnrpC+M 4 | IK5UNLkwuyBDKziYzTdBj8kUFX1WxmvEHEgqToPOZfBgsS71cJAR/zOWraDLSRM5 5 | 4jXyvoLZN4Ti9rQagQrvTQ44Vz5ycDQy7UxtbUGh1CVv69vNVr7/SOOh/Nw5FNOZ 6 | WLWrodGyoec5wh9iqRZgRqiTUc6Lt7V2RWc2X2gjwST2UfI+U46Ip3oaQ7ZD4eAk 7 | oqNDxdniBZAykVG3c/99ux4BAESTF8fsNch6UticBxYMuTu+ouvP0psfI9wwwNli 8 | JDmACRUTB9AgRynbL1AzhqQoDfsb98IZfjfNOpwnwuLwpMAPhbgd5KNdZaIJ4Hb6 9 | /stIyFElOExxd3TAxF2Gshd/lq1JcNHAZ1DSXV5MvOWT/NWgXwbIzUgQ8eIi+HuD 10 | YX2UUuaB6R8tbd52H7rbUv6HrfinuSlKWqjSYLkiKHkwUpoMw8y9UycRSzs1E9nP 11 | wPTOvRXb0mNCQeBCV9FvStNVXdCUTT8LGPv87xSD2pmt7LijlE6mHLG8McfcWkzA 12 | 69unCEHIFAFDimTuN7EBljc119xWFTcHMyoZAfFF+oTqwSbBGImruCxnaJECAwEA 13 | AQKCAgAME3aoeXNCPxMrSri7u4Xnnk71YXl0Tm9vwvjRQlMusXZggP8VKN/KjP0/ 14 | 9AE/GhmoxqPLrLCZ9ZE1EIjgmZ9Xgde9+C8rTtfCG2RFUL7/5J2p6NonlocmxoJm 15 | YkxYwjP6ce86RTjQWL3RF3s09u0inz9/efJk5O7M6bOWMQ9VZXDlBiRY5BYvbqUR 16 | 6FeSzD4MnMbdyMRoVBeXE88gTvZk8xhB6DJnLzYgc0tKiRoeKT0iYv5JZw25VyRM 17 | ycLzfTrFmXCPfB1ylb483d9Ly4fBlM8nkx37PzEnAuukIawDxsPOb9yZC+hfvNJI 18 | 7NFiMN+3maEqG2iC00w4Lep4skHY7eHUEUMl+Wjr+koAy2YGLWAwHZQTm7iXn9Ab 19 | L6adL53zyCKelRuEQOzbeosJAqS+5fpMK0ekXyoFIuskj7bWuIoCX7K/kg6q5IW+ 20 | vC2FrlsrbQ79GztWLVmHFO1I4J9M5r666YS0qdh8c+2yyRl4FmSiHfGxb3eOKpxQ 21 | b6uI97iZlkxPF9LYUCSc7wq0V2gGz+6LnGvTHlHrOfVXqw/5pLAKhXqxvnroDTwz 22 | 0Ay/xFF6ei/NSxBY5t8ztGCBm45wCU3l8pW0X6dXqwUipw5b4MRy1VFRu6rqlmbL 23 | OPSCuLxqyqsigiEYsBgS/icvXz9DWmCQMPd2XM9YhsHvUq+R4QKCAQEA98EuMMXI 24 | 6UKIt1kK2t/3OeJRyDd4iv/fCMUAnuPjLBvFE4cXD/SbqCxcQYqb+pue3PYkiTIC 25 | 71rN8OQAc5yKhzmmnCE5N26br/0pG4pwEjIr6mt8kZHmemOCNEzvhhT83nfKmV0g 26 | 9lNtuGEQMiwmZrpUOF51JOMC39bzcVjYX2Cmvb7cFbIq3lR0zwM+aZpQ4P8LHCIu 27 | bgHmwbdlkLyIULJcQmHIbo6nPFB3ZZE4mqmjwY+rA6Fh9rgBa8OFCfTtrgeYXrNb 28 | IgZQ5U8GoYRPNC2ot0vpTinraboa/cgm6oG4M7FW1POCJTl+/ktHEnKuO5oroSga 29 | /BSg7hCNFVaOhwKCAQEA4Kkys0HtwEbV5mY/NnvUD5KwfXX7BxoXc9lZ6seVoLEc 30 | KjgPYxqYRVrC7dB2YDwwp3qcRTi/uBAgFNm3iYlDzI4xS5SeaudUWjglj7BSgXE2 31 | iOEa7EwcvVPluLaTgiWjlzUKeUCNNHWSeQOt+paBOT+IgwRVemGVpAgkqQzNh/nP 32 | tl3p9aNtgzEm1qVlPclY/XUCtf3bcOR+z1f1b4jBdn0leu5OhnxkC+Htik+2fTXD 33 | jt6JGrMkanN25YzsjnD3Sn+v6SO26H99wnYx5oMSdmb8SlWRrKtfJHnihphjG/YY 34 | l1cyorV6M/asSgXNQfGJm4OuJi0I4/FL2wLUHnU+JwKCAQEAzh4WipcRthYXXcoj 35 | gMKRkMOb3GFh1OpYqJgVExtudNTJmZxq8GhFU51MR27Eo7LycMwKy2UjEfTOnplh 36 | Us2qZiPtW7k8O8S2m6yXlYUQBeNdq9IuuYDTaYD94vsazscJNSAeGodjE+uGvb1q 37 | 1wLqE87yoE7dUInYa1cOA3+xy2/CaNuviBFJHtzOrSb6tqqenQEyQf6h9/12+DTW 38 | t5pSIiixHrzxHiFqOoCLRKGToQB+71rSINwTf0nITNpGBWmSj5VcC3VV3TG5/XxI 39 | fPlxV2yhD5WFDPVNGBGvwPDSh4jSMZdZMSNBZCy4XWFNSKjGEWoK4DFYed3DoSt9 40 | 5IG1YwKCAQA63ntHl64KJUWlkwNbboU583FF3uWBjee5VqoGKHhf3CkKMxhtGqnt 41 | +oN7t5VdUEhbinhqdx1dyPPvIsHCS3K1pkjqii4cyzNCVNYa2dQ00Qq+QWZBpwwc 42 | 3GAkz8rFXsGIPMDa1vxpU6mnBjzPniKMcsZ9tmQDppCEpBGfLpio2eAA5IkK8eEf 43 | cIDB3CM0Vo94EvI76CJZabaE9IJ+0HIJb2+jz9BJ00yQBIqvJIYoNy9gP5Xjpi+T 44 | qV/tdMkD5jwWjHD3AYHLWKUGkNwwkAYFeqT/gX6jpWBP+ZRPOp011X3KInJFSpKU 45 | DT5GQ1Dux7EMTCwVGtXqjO8Ym5wjwwsfAoIBAEcxlhIW1G6BiNfnWbNPWBdh3v/K 46 | 5Ln98Rcrz8UIbWyl7qNPjYb13C1KmifVG1Rym9vWMO3KuG5atK3Mz2yLVRtmWAVc 47 | fxzR57zz9MZFDun66xo+Z1wN3fVxQB4CYpOEI4Lb9ioX4v85hm3D6RpFukNtRQEc 48 | Gfr4scTjJX4jFWDp0h6ffMb8mY+quvZoJ0TJqV9L9Yj6Ksdvqez/bdSraev97bHQ 49 | 4gbQxaTZ6WjaD4HjpPQefMdWp97Metg0ZQSS8b8EzmNFgyJ3XcjirzwliKTAQtn6 50 | I2sd0NCIooelrKRD8EJoDUwxoOctY7R97wpZ7/wEHU45cBCbRV3H4JILS5c= 51 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /assets/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Roa Framework 6 | 7 | 8 |

Welcome!

9 |

Go to roa for more information...

10 | 11 | -------------------------------------------------------------------------------- /examples/echo.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info Cargo run --example echo, 2 | //! then request http://127.0.0.1:8000 with some payload. 3 | 4 | use std::error::Error as StdError; 5 | 6 | use roa::logger::logger; 7 | use roa::preload::*; 8 | use roa::{App, Context}; 9 | use tracing::info; 10 | use tracing_subscriber::EnvFilter; 11 | 12 | async fn echo(ctx: &mut Context) -> roa::Result { 13 | let stream = ctx.req.stream(); 14 | ctx.resp.write_stream(stream); 15 | Ok(()) 16 | } 17 | 18 | #[tokio::main] 19 | async fn main() -> Result<(), Box> { 20 | tracing_subscriber::fmt() 21 | .with_env_filter(EnvFilter::from_default_env()) 22 | .try_init() 23 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 24 | 25 | let app = App::new().gate(logger).end(echo); 26 | app.listen("127.0.0.1:8000", |addr| { 27 | info!("Server is listening on {}", addr) 28 | })? 29 | .await?; 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /examples/hello-world.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info Cargo run --example hello-world, 2 | //! then request http://127.0.0.1:8000. 3 | 4 | use log::info; 5 | use roa::logger::logger; 6 | use roa::preload::*; 7 | use roa::App; 8 | use tracing_subscriber::EnvFilter; 9 | 10 | #[tokio::main] 11 | async fn main() -> anyhow::Result<()> { 12 | tracing_subscriber::fmt() 13 | .with_env_filter(EnvFilter::from_default_env()) 14 | .try_init() 15 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 16 | let app = App::new().gate(logger).end("Hello, World!"); 17 | app.listen("127.0.0.1:8000", |addr| { 18 | info!("Server is listening on {}", addr) 19 | })? 20 | .await?; 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /examples/https.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info Cargo run --example https, 2 | //! then request https://127.0.0.1:8000. 3 | 4 | use std::error::Error as StdError; 5 | use std::fs::File; 6 | use std::io::BufReader; 7 | 8 | use log::info; 9 | use roa::body::DispositionType; 10 | use roa::logger::logger; 11 | use roa::preload::*; 12 | use roa::tls::pemfile::{certs, rsa_private_keys}; 13 | use roa::tls::{Certificate, PrivateKey, ServerConfig, TlsListener}; 14 | use roa::{App, Context}; 15 | use tracing_subscriber::EnvFilter; 16 | 17 | async fn serve_file(ctx: &mut Context) -> roa::Result { 18 | ctx.write_file("assets/welcome.html", DispositionType::Inline) 19 | .await 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() -> Result<(), Box> { 24 | tracing_subscriber::fmt() 25 | .with_env_filter(EnvFilter::from_default_env()) 26 | .try_init() 27 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 28 | 29 | let mut cert_file = BufReader::new(File::open("assets/cert.pem")?); 30 | let mut key_file = BufReader::new(File::open("assets/key.pem")?); 31 | let cert_chain = certs(&mut cert_file)? 32 | .into_iter() 33 | .map(Certificate) 34 | .collect(); 35 | 36 | let config = ServerConfig::builder() 37 | .with_safe_defaults() 38 | .with_no_client_auth() 39 | .with_single_cert( 40 | cert_chain, 41 | PrivateKey(rsa_private_keys(&mut key_file)?.remove(0)), 42 | )?; 43 | 44 | let app = App::new().gate(logger).end(serve_file); 45 | app.listen_tls("127.0.0.1:8000", config, |addr| { 46 | info!("Server is listening on https://localhost:{}", addr.port()) 47 | })? 48 | .await?; 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /examples/restful-api.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info Cargo run --example restful-api, 2 | //! then: 3 | //! - `curl 127.0.0.1:8000/user/0` 4 | //! query user where id=0 5 | //! - `curl -H "Content-type: application/json" -d '{"name":"Hexilee", "age": 20}' -X POST 127.0.0.1:8000/user` 6 | //! create a new user 7 | //! - `curl -H "Content-type: application/json" -d '{"name":"Alice", "age": 20}' -X PUT 127.0.0.1:8000/user/0` 8 | //! update user where id=0, return the old data 9 | //! - `curl 127.0.0.1:8000/user/0 -X DELETE` 10 | //! delete user where id=0 11 | 12 | use std::result::Result as StdResult; 13 | use std::sync::Arc; 14 | 15 | use roa::http::StatusCode; 16 | use roa::preload::*; 17 | use roa::router::{get, post, Router}; 18 | use roa::{throw, App, Context, Result}; 19 | use serde::{Deserialize, Serialize}; 20 | use serde_json::json; 21 | use slab::Slab; 22 | use tokio::sync::RwLock; 23 | 24 | #[derive(Debug, Serialize, Deserialize, Clone)] 25 | struct User { 26 | name: String, 27 | age: u8, 28 | } 29 | 30 | #[derive(Clone)] 31 | struct Database { 32 | table: Arc>>, 33 | } 34 | 35 | impl Database { 36 | fn new() -> Self { 37 | Self { 38 | table: Arc::new(RwLock::new(Slab::new())), 39 | } 40 | } 41 | 42 | async fn create(&self, user: User) -> usize { 43 | self.table.write().await.insert(user) 44 | } 45 | 46 | async fn retrieve(&self, id: usize) -> Result { 47 | match self.table.read().await.get(id) { 48 | Some(user) => Ok(user.clone()), 49 | None => throw!(StatusCode::NOT_FOUND), 50 | } 51 | } 52 | 53 | async fn update(&self, id: usize, new_user: &mut User) -> Result { 54 | match self.table.write().await.get_mut(id) { 55 | Some(user) => { 56 | std::mem::swap(new_user, user); 57 | Ok(()) 58 | } 59 | None => throw!(StatusCode::NOT_FOUND), 60 | } 61 | } 62 | 63 | async fn delete(&self, id: usize) -> Result { 64 | if !self.table.read().await.contains(id) { 65 | throw!(StatusCode::NOT_FOUND) 66 | } 67 | Ok(self.table.write().await.remove(id)) 68 | } 69 | } 70 | 71 | async fn create_user(ctx: &mut Context) -> Result { 72 | let user: User = ctx.read_json().await?; 73 | let id = ctx.create(user).await; 74 | ctx.write_json(&json!({ "id": id }))?; 75 | ctx.resp.status = StatusCode::CREATED; 76 | Ok(()) 77 | } 78 | 79 | async fn get_user(ctx: &mut Context) -> Result { 80 | let id: usize = ctx.must_param("id")?.parse()?; 81 | let user = ctx.retrieve(id).await?; 82 | ctx.write_json(&user) 83 | } 84 | 85 | async fn update_user(ctx: &mut Context) -> Result { 86 | let id: usize = ctx.must_param("id")?.parse()?; 87 | let mut user: User = ctx.read_json().await?; 88 | ctx.update(id, &mut user).await?; 89 | ctx.write_json(&user) 90 | } 91 | 92 | async fn delete_user(ctx: &mut Context) -> Result { 93 | let id: usize = ctx.must_param("id")?.parse()?; 94 | let user = ctx.delete(id).await?; 95 | ctx.write_json(&user) 96 | } 97 | 98 | #[tokio::main] 99 | async fn main() -> StdResult<(), Box> { 100 | let router = Router::new() 101 | .on("/", post(create_user)) 102 | .on("/:id", get(get_user).put(update_user).delete(delete_user)); 103 | let app = App::state(Database::new()).end(router.routes("/user")?); 104 | app.listen("127.0.0.1:8000", |addr| { 105 | println!("Server is listening on {}", addr) 106 | })? 107 | .await?; 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /examples/serve-file.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info cargo run --example serve-file, 2 | //! then request http://127.0.0.1:8000. 3 | 4 | use std::borrow::Cow; 5 | use std::path::Path; 6 | use std::result::Result as StdResult; 7 | use std::time::SystemTime; 8 | 9 | use askama::Template; 10 | use bytesize::ByteSize; 11 | use chrono::offset::Local; 12 | use chrono::DateTime; 13 | use log::info; 14 | use roa::body::DispositionType::*; 15 | use roa::compress::Compress; 16 | use roa::http::StatusCode; 17 | use roa::logger::logger; 18 | use roa::preload::*; 19 | use roa::router::{get, Router}; 20 | use roa::{throw, App, Context, Next, Result}; 21 | use tokio::fs::{metadata, read_dir}; 22 | use tracing_subscriber::EnvFilter; 23 | 24 | #[derive(Template)] 25 | #[template(path = "directory.html")] 26 | struct Dir<'a> { 27 | title: &'a str, 28 | root: &'a str, 29 | dirs: Vec, 30 | files: Vec, 31 | } 32 | 33 | struct DirInfo { 34 | link: String, 35 | name: String, 36 | modified: String, 37 | } 38 | 39 | struct FileInfo { 40 | link: String, 41 | name: String, 42 | modified: String, 43 | size: String, 44 | } 45 | 46 | impl<'a> Dir<'a> { 47 | fn new(title: &'a str, root: &'a str) -> Self { 48 | Self { 49 | title, 50 | root, 51 | dirs: Vec::new(), 52 | files: Vec::new(), 53 | } 54 | } 55 | } 56 | 57 | async fn path_checker(ctx: &mut Context, next: Next<'_>) -> Result { 58 | if ctx.must_param("path")?.contains("..") { 59 | throw!(StatusCode::BAD_REQUEST, "invalid path") 60 | } else { 61 | next.await 62 | } 63 | } 64 | 65 | async fn serve_path(ctx: &mut Context) -> Result { 66 | let path_value = ctx.must_param("path")?; 67 | let path = path_value.as_ref(); 68 | let file_path = Path::new(".").join(path); 69 | let meta = metadata(&file_path).await?; 70 | if meta.is_file() { 71 | ctx.write_file(file_path, Inline).await 72 | } else if meta.is_dir() { 73 | serve_dir(ctx, path).await 74 | } else { 75 | throw!(StatusCode::NOT_FOUND, "path not found") 76 | } 77 | } 78 | 79 | async fn serve_root(ctx: &mut Context) -> Result { 80 | serve_dir(ctx, "").await 81 | } 82 | 83 | async fn serve_dir(ctx: &mut Context, path: &str) -> Result { 84 | let uri_path = Path::new("/").join(path); 85 | let mut entries = read_dir(Path::new(".").join(path)).await?; 86 | let title = uri_path 87 | .file_name() 88 | .map(|os_str| os_str.to_string_lossy()) 89 | .unwrap_or(Cow::Borrowed("/")); 90 | let root_str = uri_path.to_string_lossy(); 91 | let mut dir = Dir::new(&title, &root_str); 92 | while let Some(entry) = entries.next_entry().await? { 93 | let metadata = entry.metadata().await?; 94 | if metadata.is_dir() { 95 | dir.dirs.push(DirInfo { 96 | link: uri_path 97 | .join(entry.file_name()) 98 | .to_string_lossy() 99 | .to_string(), 100 | name: entry.file_name().to_string_lossy().to_string(), 101 | modified: format_time(metadata.modified()?), 102 | }) 103 | } 104 | if metadata.is_file() { 105 | dir.files.push(FileInfo { 106 | link: uri_path 107 | .join(entry.file_name()) 108 | .to_string_lossy() 109 | .to_string(), 110 | name: entry.file_name().to_string_lossy().to_string(), 111 | modified: format_time(metadata.modified()?), 112 | size: ByteSize(metadata.len()).to_string(), 113 | }) 114 | } 115 | } 116 | ctx.render(&dir) 117 | } 118 | 119 | fn format_time(time: SystemTime) -> String { 120 | let datetime: DateTime = time.into(); 121 | datetime.format("%d/%m/%Y %T").to_string() 122 | } 123 | 124 | #[tokio::main] 125 | async fn main() -> StdResult<(), Box> { 126 | tracing_subscriber::fmt() 127 | .with_env_filter(EnvFilter::from_default_env()) 128 | .try_init() 129 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 130 | 131 | let wildcard_router = Router::new().gate(path_checker).on("/", get(serve_path)); 132 | let router = Router::new() 133 | .on("/", serve_root) 134 | .include("/*{path}", wildcard_router); 135 | let app = App::new() 136 | .gate(logger) 137 | .gate(Compress::default()) 138 | .end(router.routes("/")?); 139 | app.listen("127.0.0.1:8000", |addr| { 140 | info!("Server is listening on {}", addr) 141 | })? 142 | .await 143 | .map_err(Into::into) 144 | } 145 | -------------------------------------------------------------------------------- /examples/websocket-echo.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info cargo run --example websocket-echo, 2 | //! then request ws://127.0.0.1:8000/chat. 3 | 4 | use std::error::Error as StdError; 5 | 6 | use futures::StreamExt; 7 | use http::Method; 8 | use log::{error, info}; 9 | use roa::cors::Cors; 10 | use roa::logger::logger; 11 | use roa::preload::*; 12 | use roa::router::{allow, Router}; 13 | use roa::websocket::Websocket; 14 | use roa::App; 15 | use tracing_subscriber::EnvFilter; 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<(), Box> { 19 | tracing_subscriber::fmt() 20 | .with_env_filter(EnvFilter::from_default_env()) 21 | .try_init() 22 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 23 | 24 | let router = Router::new().on( 25 | "/chat", 26 | allow( 27 | [Method::GET], 28 | Websocket::new(|_ctx, stream| async move { 29 | let (write, read) = stream.split(); 30 | if let Err(err) = read.forward(write).await { 31 | error!("{}", err); 32 | } 33 | }), 34 | ), 35 | ); 36 | let app = App::new() 37 | .gate(logger) 38 | .gate(Cors::new()) 39 | .end(router.routes("/")?); 40 | app.listen("127.0.0.1:8000", |addr| { 41 | info!("Server is listening on {}", addr) 42 | })? 43 | .await?; 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /examples/welcome.rs: -------------------------------------------------------------------------------- 1 | //! RUST_LOG=info Cargo run --example welcome, 2 | //! then request http://127.0.0.1:8000 with some payload. 3 | 4 | use std::error::Error as StdError; 5 | 6 | use log::info; 7 | use roa::logger::logger; 8 | use roa::preload::*; 9 | use roa::App; 10 | use tracing_subscriber::EnvFilter; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<(), Box> { 14 | tracing_subscriber::fmt() 15 | .with_env_filter(EnvFilter::from_default_env()) 16 | .try_init() 17 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 18 | 19 | let app = App::new() 20 | .gate(logger) 21 | .end(include_str!("../assets/welcome.html")); 22 | app.listen("127.0.0.1:8000", |addr| { 23 | info!("Server is listening on {}", addr) 24 | })? 25 | .await?; 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /integration/diesel-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diesel-example" 3 | version = "0.1.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tokio = { version = "1.15", features = ["full"] } 11 | diesel = { version = "1.4", features = ["extras", "sqlite"] } 12 | roa = { path = "../../roa", features = ["router", "json"] } 13 | roa-diesel = { path = "../../roa-diesel" } 14 | tracing-futures = "0.2" 15 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 16 | tracing = "0.1" 17 | serde = { version = "1", features = ["derive"] } 18 | anyhow = "1.0" 19 | 20 | [dev-dependencies] 21 | reqwest = { version = "0.11", features = ["json", "cookies", "gzip"] } -------------------------------------------------------------------------------- /integration/diesel-example/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | RUST_LOG=info cargo run --bin api 3 | ``` 4 | 5 | - `curl 127.0.0.1:8000/post/1` 6 | query post where id=0 and published 7 | - `curl -H "Content-type: application/json" -d '{"title":"Hello", "body": "Hello, world", "published": false}' -X POST 127.0.0.1:8000/post` 8 | create a new post 9 | - `curl -H "Content-type: application/json" -d '{"title":"Hello", "body": "Hello, world", "published": true}' -X PUT 127.0.0.1:8000/post/1` 10 | update post where id=0, return the old data 11 | - `curl 127.0.0.1:8000/post/1 -X DELETE` 12 | delete post where id=0 13 | -------------------------------------------------------------------------------- /integration/diesel-example/src/bin/api.rs: -------------------------------------------------------------------------------- 1 | use diesel_example::{create_pool, post_router}; 2 | use roa::logger::logger; 3 | use roa::preload::*; 4 | use roa::App; 5 | use tracing::info; 6 | use tracing_subscriber::EnvFilter; 7 | 8 | #[tokio::main] 9 | async fn main() -> anyhow::Result<()> { 10 | tracing_subscriber::fmt() 11 | .with_env_filter(EnvFilter::from_default_env()) 12 | .try_init() 13 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 14 | let app = App::state(create_pool()?) 15 | .gate(logger) 16 | .end(post_router().routes("/post")?); 17 | app.listen("127.0.0.1:8000", |addr| { 18 | info!("Server is listening on {}", addr) 19 | })? 20 | .await?; 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /integration/diesel-example/src/data_object.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::schema::posts; 4 | 5 | // for both transfer and access 6 | #[derive(Debug, Insertable, Deserialize)] 7 | #[table_name = "posts"] 8 | pub struct NewPost { 9 | pub title: String, 10 | pub body: String, 11 | pub published: bool, 12 | } 13 | -------------------------------------------------------------------------------- /integration/diesel-example/src/endpoints.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use diesel::result::Error; 3 | use roa::http::StatusCode; 4 | use roa::preload::*; 5 | use roa::router::{get, post, Router}; 6 | use roa::{throw, Context, Result}; 7 | use roa_diesel::preload::*; 8 | 9 | use crate::data_object::NewPost; 10 | use crate::models::*; 11 | use crate::schema::posts::dsl::{self, posts}; 12 | use crate::State; 13 | 14 | pub fn post_router() -> Router { 15 | Router::new() 16 | .on("/", post(create_post)) 17 | .on("/:id", get(get_post).put(update_post).delete(delete_post)) 18 | } 19 | 20 | async fn create_post(ctx: &mut Context) -> Result { 21 | let data: NewPost = ctx.read_json().await?; 22 | let conn = ctx.get_conn().await?; 23 | let post = ctx 24 | .exec 25 | .spawn_blocking(move || { 26 | conn.transaction::(|| { 27 | diesel::insert_into(crate::schema::posts::table) 28 | .values(&data) 29 | .execute(&conn)?; 30 | Ok(posts.order(dsl::id.desc()).first(&conn)?) 31 | }) 32 | }) 33 | .await?; 34 | ctx.resp.status = StatusCode::CREATED; 35 | ctx.write_json(&post) 36 | } 37 | 38 | async fn get_post(ctx: &mut Context) -> Result { 39 | let id: i32 = ctx.must_param("id")?.parse()?; 40 | match ctx 41 | .first::(posts.find(id).filter(dsl::published.eq(true))) 42 | .await? 43 | { 44 | None => throw!(StatusCode::NOT_FOUND, &format!("post({}) not found", id)), 45 | Some(post) => ctx.write_json(&post), 46 | } 47 | } 48 | 49 | async fn update_post(ctx: &mut Context) -> Result { 50 | let id: i32 = ctx.must_param("id")?.parse()?; 51 | let NewPost { 52 | title, 53 | body, 54 | published, 55 | } = ctx.read_json().await?; 56 | 57 | match ctx.first::(posts.find(id)).await? { 58 | None => throw!(StatusCode::NOT_FOUND, &format!("post({}) not found", id)), 59 | Some(post) => { 60 | ctx.execute(diesel::update(posts.find(id)).set(( 61 | dsl::title.eq(title), 62 | dsl::body.eq(body), 63 | dsl::published.eq(published), 64 | ))) 65 | .await?; 66 | ctx.write_json(&post) 67 | } 68 | } 69 | } 70 | 71 | async fn delete_post(ctx: &mut Context) -> Result { 72 | let id: i32 = ctx.must_param("id")?.parse()?; 73 | match ctx.first::(posts.find(id)).await? { 74 | None => throw!(StatusCode::NOT_FOUND, &format!("post({}) not found", id)), 75 | Some(post) => { 76 | ctx.execute(diesel::delete(posts.find(id))).await?; 77 | ctx.write_json(&post) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /integration/diesel-example/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | mod data_object; 5 | mod endpoints; 6 | pub mod models; 7 | pub mod schema; 8 | 9 | use diesel::prelude::*; 10 | use diesel::sqlite::SqliteConnection; 11 | use roa_diesel::{make_pool, Pool}; 12 | 13 | #[derive(Clone)] 14 | pub struct State(pub Pool); 15 | 16 | impl AsRef> for State { 17 | fn as_ref(&self) -> &Pool { 18 | &self.0 19 | } 20 | } 21 | 22 | pub fn create_pool() -> anyhow::Result { 23 | let pool = make_pool(":memory:")?; 24 | diesel::sql_query( 25 | r" 26 | CREATE TABLE posts ( 27 | id INTEGER PRIMARY KEY, 28 | title VARCHAR NOT NULL, 29 | body TEXT NOT NULL, 30 | published BOOLEAN NOT NULL DEFAULT 'f' 31 | ) 32 | ", 33 | ) 34 | .execute(&*pool.get()?)?; 35 | Ok(State(pool)) 36 | } 37 | 38 | pub use endpoints::post_router; 39 | -------------------------------------------------------------------------------- /integration/diesel-example/src/models.rs: -------------------------------------------------------------------------------- 1 | use diesel::Queryable; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Queryable, Serialize, Deserialize)] 5 | pub struct Post { 6 | pub id: i32, 7 | pub title: String, 8 | pub body: String, 9 | pub published: bool, 10 | } 11 | -------------------------------------------------------------------------------- /integration/diesel-example/src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | posts (id) { 3 | id -> Integer, 4 | title -> Text, 5 | body -> Text, 6 | published -> Bool, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration/diesel-example/tests/restful.rs: -------------------------------------------------------------------------------- 1 | use diesel_example::models::Post; 2 | use diesel_example::{create_pool, post_router}; 3 | use roa::http::StatusCode; 4 | use roa::preload::*; 5 | use roa::App; 6 | use serde::Serialize; 7 | use tracing::{debug, info}; 8 | use tracing_subscriber::EnvFilter; 9 | 10 | #[derive(Debug, Serialize, Copy, Clone)] 11 | pub struct NewPost<'a> { 12 | pub title: &'a str, 13 | pub body: &'a str, 14 | pub published: bool, 15 | } 16 | 17 | impl PartialEq for NewPost<'_> { 18 | fn eq(&self, other: &Post) -> bool { 19 | self.title == other.title && self.body == other.body && self.published == other.published 20 | } 21 | } 22 | 23 | #[tokio::test] 24 | async fn test() -> anyhow::Result<()> { 25 | tracing_subscriber::fmt() 26 | .with_env_filter(EnvFilter::from_default_env()) 27 | .try_init() 28 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 29 | 30 | let app = App::state(create_pool()?).end(post_router().routes("/post")?); 31 | let (addr, server) = app.run()?; 32 | tokio::task::spawn(server); 33 | info!("server is running on {}", addr); 34 | let base_url = format!("http://{}/post", addr); 35 | let client = reqwest::Client::new(); 36 | 37 | // Not Found 38 | let resp = client.get(&format!("{}/{}", &base_url, 0)).send().await?; 39 | assert_eq!(StatusCode::NOT_FOUND, resp.status()); 40 | debug!("{}/{} not found", &base_url, 0); 41 | 42 | // Create 43 | let first_post = NewPost { 44 | title: "Hello", 45 | body: "Welcome to roa-diesel", 46 | published: false, 47 | }; 48 | 49 | let resp = client.post(&base_url).json(&first_post).send().await?; 50 | assert_eq!(StatusCode::CREATED, resp.status()); 51 | let created_post: Post = resp.json().await?; 52 | let id = created_post.id; 53 | assert_eq!(&first_post, &created_post); 54 | 55 | // Post isn't published, get nothing 56 | let resp = client.get(&format!("{}/{}", &base_url, id)).send().await?; 57 | assert_eq!(StatusCode::NOT_FOUND, resp.status()); 58 | 59 | // Update 60 | let second_post = NewPost { 61 | published: true, 62 | ..first_post 63 | }; 64 | let resp = client 65 | .put(&format!("{}/{}", &base_url, id)) 66 | .json(&second_post) 67 | .send() 68 | .await?; 69 | assert_eq!(StatusCode::OK, resp.status()); 70 | 71 | // Return old post 72 | let updated_post: Post = resp.json().await?; 73 | assert_eq!(&first_post, &updated_post); 74 | 75 | // Get it 76 | let resp = client.get(&format!("{}/{}", &base_url, id)).send().await?; 77 | assert_eq!(StatusCode::OK, resp.status()); 78 | let query_post: Post = resp.json().await?; 79 | assert_eq!(&second_post, &query_post); 80 | 81 | // Delete 82 | let resp = client 83 | .delete(&format!("{}/{}", &base_url, id)) 84 | .send() 85 | .await?; 86 | assert_eq!(StatusCode::OK, resp.status()); 87 | let deleted_post: Post = resp.json().await?; 88 | assert_eq!(&second_post, &deleted_post); 89 | 90 | // Post is deleted, get nothing 91 | let resp = client.get(&format!("{}/{}", &base_url, id)).send().await?; 92 | assert_eq!(StatusCode::NOT_FOUND, resp.status()); 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /integration/juniper-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "juniper-example" 3 | version = "0.1.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | diesel = "1.4" 11 | roa = { path = "../../roa", features = ["router"] } 12 | roa-diesel = { path = "../../roa-diesel" } 13 | roa-juniper = { path = "../../roa-juniper" } 14 | diesel-example = { path = "../diesel-example" } 15 | tokio = { version = "1.15", features = ["full"] } 16 | tracing = "0.1" 17 | serde = { version = "1", features = ["derive"] } 18 | futures = "0.3" 19 | juniper = { version = "0.15", default-features = false } 20 | tracing-futures = "0.2" 21 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 22 | anyhow = "1.0" -------------------------------------------------------------------------------- /integration/juniper-example/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | RUST_LOG=info cargo run 3 | ``` 4 | 5 | Then request http://127.0.0.1:8000, play with the GraphQL playground! -------------------------------------------------------------------------------- /integration/juniper-example/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | mod models; 5 | mod schema; 6 | use std::error::Error as StdError; 7 | 8 | use diesel::prelude::*; 9 | use diesel::result::Error; 10 | use diesel_example::{create_pool, State}; 11 | use juniper::http::playground::playground_source; 12 | use juniper::{ 13 | graphql_value, EmptySubscription, FieldError, FieldResult, GraphQLInputObject, RootNode, 14 | }; 15 | use roa::http::Method; 16 | use roa::logger::logger; 17 | use roa::preload::*; 18 | use roa::router::{allow, get, Router}; 19 | use roa::App; 20 | use roa_diesel::preload::*; 21 | use roa_juniper::{GraphQL, JuniperContext}; 22 | use serde::Serialize; 23 | use tracing::info; 24 | use tracing_subscriber::EnvFilter; 25 | 26 | use crate::models::Post; 27 | use crate::schema::posts; 28 | 29 | #[derive(Debug, Insertable, Serialize, GraphQLInputObject)] 30 | #[table_name = "posts"] 31 | #[graphql(description = "A new post")] 32 | struct NewPost { 33 | title: String, 34 | body: String, 35 | published: bool, 36 | } 37 | 38 | struct Query; 39 | 40 | #[juniper::graphql_object( 41 | Context = JuniperContext, 42 | )] 43 | impl Query { 44 | async fn post( 45 | &self, 46 | ctx: &JuniperContext, 47 | id: i32, 48 | published: bool, 49 | ) -> FieldResult { 50 | use crate::schema::posts::dsl::{self, posts}; 51 | match ctx 52 | .first(posts.find(id).filter(dsl::published.eq(published))) 53 | .await? 54 | { 55 | Some(post) => Ok(post), 56 | None => Err(FieldError::new( 57 | "post not found", 58 | graphql_value!({ "status": 404, "id": id }), 59 | )), 60 | } 61 | } 62 | } 63 | 64 | struct Mutation; 65 | 66 | #[juniper::graphql_object( 67 | Context = JuniperContext, 68 | )] 69 | impl Mutation { 70 | async fn create_post( 71 | &self, 72 | ctx: &JuniperContext, 73 | new_post: NewPost, 74 | ) -> FieldResult { 75 | use crate::schema::posts::dsl::{self, posts}; 76 | let conn = ctx.get_conn().await?; 77 | let post = ctx 78 | .exec 79 | .spawn_blocking(move || { 80 | conn.transaction::(|| { 81 | diesel::insert_into(crate::schema::posts::table) 82 | .values(&new_post) 83 | .execute(&conn)?; 84 | Ok(posts.order(dsl::id.desc()).first(&conn)?) 85 | }) 86 | }) 87 | .await?; 88 | Ok(post) 89 | } 90 | 91 | async fn update_post( 92 | &self, 93 | id: i32, 94 | ctx: &JuniperContext, 95 | new_post: NewPost, 96 | ) -> FieldResult { 97 | use crate::schema::posts::dsl::{self, posts}; 98 | match ctx.first(posts.find(id)).await? { 99 | None => Err(FieldError::new( 100 | "post not found", 101 | graphql_value!({ "status": 404, "id": id }), 102 | )), 103 | Some(old_post) => { 104 | let NewPost { 105 | title, 106 | body, 107 | published, 108 | } = new_post; 109 | ctx.execute(diesel::update(posts.find(id)).set(( 110 | dsl::title.eq(title), 111 | dsl::body.eq(body), 112 | dsl::published.eq(published), 113 | ))) 114 | .await?; 115 | Ok(old_post) 116 | } 117 | } 118 | } 119 | 120 | async fn delete_post(&self, ctx: &JuniperContext, id: i32) -> FieldResult { 121 | use crate::schema::posts::dsl::posts; 122 | match ctx.first(posts.find(id)).await? { 123 | None => Err(FieldError::new( 124 | "post not found", 125 | graphql_value!({ "status": 404, "id": id }), 126 | )), 127 | Some(old_post) => { 128 | ctx.execute(diesel::delete(posts.find(id))).await?; 129 | Ok(old_post) 130 | } 131 | } 132 | } 133 | } 134 | 135 | #[tokio::main] 136 | async fn main() -> Result<(), Box> { 137 | tracing_subscriber::fmt() 138 | .with_env_filter(EnvFilter::from_default_env()) 139 | .try_init() 140 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 141 | 142 | let router = Router::new() 143 | .on("/", get(playground_source("/api", None))) 144 | .on( 145 | "/api", 146 | allow( 147 | [Method::GET, Method::POST], 148 | GraphQL(RootNode::new(Query, Mutation, EmptySubscription::new())), 149 | ), 150 | ); 151 | let app = App::state(create_pool()?) 152 | .gate(logger) 153 | .end(router.routes("/")?); 154 | app.listen("127.0.0.1:8000", |addr| { 155 | info!("Server is listening on {}", addr) 156 | })? 157 | .await?; 158 | Ok(()) 159 | } 160 | -------------------------------------------------------------------------------- /integration/juniper-example/src/models.rs: -------------------------------------------------------------------------------- 1 | use diesel::Queryable; 2 | use juniper::GraphQLObject; 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Clone, Queryable, Deserialize, GraphQLObject)] 6 | #[graphql(description = "A post")] 7 | pub struct Post { 8 | pub id: i32, 9 | pub title: String, 10 | pub body: String, 11 | pub published: bool, 12 | } 13 | -------------------------------------------------------------------------------- /integration/juniper-example/src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | posts (id) { 3 | id -> Integer, 4 | title -> Text, 5 | body -> Text, 6 | published -> Bool, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /integration/multipart-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multipart-example" 3 | version = "0.1.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | roa = { path = "../../roa", features = ["router", "file", "multipart"] } 11 | tokio = { version = "1.15", features = ["full"] } 12 | tracing = "0.1" 13 | futures = "0.3" 14 | tracing-futures = "0.2" 15 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 16 | anyhow = "1.0" 17 | -------------------------------------------------------------------------------- /integration/multipart-example/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | RUST_LOG=info cargo run 3 | ``` 4 | 5 | Then visit `http://127.0.0.1:8000`, files will be stored in `./upload`. -------------------------------------------------------------------------------- /integration/multipart-example/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | Upload Test 3 | 4 |
5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /integration/multipart-example/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::path::Path; 3 | 4 | use roa::body::{DispositionType, PowerBody}; 5 | use roa::logger::logger; 6 | use roa::preload::*; 7 | use roa::router::{get, post, Router}; 8 | use roa::{App, Context}; 9 | use tokio::fs::File; 10 | use tokio::io::AsyncWriteExt; 11 | use tracing::info; 12 | use tracing_subscriber::EnvFilter; 13 | 14 | async fn get_form(ctx: &mut Context) -> roa::Result { 15 | ctx.write_file("./assets/index.html", DispositionType::Inline) 16 | .await 17 | } 18 | 19 | async fn post_file(ctx: &mut Context) -> roa::Result { 20 | let mut form = ctx.read_multipart().await?; 21 | while let Some(mut field) = form.next_field().await? { 22 | info!("{:?}", field.content_type()); 23 | match field.file_name() { 24 | None => continue, // ignore non-file field 25 | Some(filename) => { 26 | let path = Path::new("./upload"); 27 | let mut file = File::create(path.join(filename)).await?; 28 | while let Some(c) = field.chunk().await? { 29 | file.write_all(&c).await?; 30 | } 31 | } 32 | } 33 | } 34 | Ok(()) 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() -> Result<(), Box> { 39 | tracing_subscriber::fmt() 40 | .with_env_filter(EnvFilter::from_default_env()) 41 | .try_init() 42 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 43 | 44 | let router = Router::new() 45 | .on("/", get(get_form)) 46 | .on("/file", post(post_file)); 47 | let app = App::new().gate(logger).end(router.routes("/")?); 48 | app.listen("127.0.0.1:8000", |addr| { 49 | info!("Server is listening on {}", addr) 50 | })? 51 | .await?; 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /integration/multipart-example/upload/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hexilee/roa/979d9783b9cafd5c4b4caa60255390547c41034b/integration/multipart-example/upload/.gitkeep -------------------------------------------------------------------------------- /integration/websocket-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket-example" 3 | version = "0.1.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | roa = { path = "../../roa", features = ["router", "file", "websocket"] } 11 | tokio = { version = "1.15", features = ["full"] } 12 | tracing = "0.1" 13 | futures = "0.3" 14 | http = "0.2" 15 | slab = "0.4" 16 | tracing-futures = "0.2" 17 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 18 | anyhow = "1.0" 19 | 20 | [dev-dependencies] 21 | tokio-tungstenite = { version = "0.15", features = ["connect"] } 22 | -------------------------------------------------------------------------------- /integration/websocket-example/README.md: -------------------------------------------------------------------------------- 1 | WIP... -------------------------------------------------------------------------------- /integration/websocket-example/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::error::Error as StdError; 3 | use std::sync::Arc; 4 | 5 | use futures::stream::{SplitSink, SplitStream}; 6 | use futures::{SinkExt, StreamExt}; 7 | use http::Method; 8 | use roa::logger::logger; 9 | use roa::preload::*; 10 | use roa::router::{allow, RouteTable, Router, RouterError}; 11 | use roa::websocket::tungstenite::protocol::frame::coding::CloseCode; 12 | use roa::websocket::tungstenite::protocol::frame::CloseFrame; 13 | use roa::websocket::tungstenite::Error as WsError; 14 | use roa::websocket::{Message, SocketStream, Websocket}; 15 | use roa::{App, Context}; 16 | use slab::Slab; 17 | use tokio::sync::{Mutex, RwLock}; 18 | use tracing::{debug, error, info, warn}; 19 | use tracing_subscriber::EnvFilter; 20 | 21 | type Sender = SplitSink; 22 | type Channel = Slab>; 23 | #[derive(Clone)] 24 | struct SyncChannel(Arc>); 25 | 26 | impl SyncChannel { 27 | fn new() -> Self { 28 | Self(Arc::new(RwLock::new(Slab::new()))) 29 | } 30 | 31 | async fn broadcast(&self, message: Message) { 32 | let channel = self.0.read().await; 33 | for (_, sender) in channel.iter() { 34 | if let Err(err) = sender.lock().await.send(message.clone()).await { 35 | error!("broadcast error: {}", err); 36 | } 37 | } 38 | } 39 | 40 | async fn send(&self, index: usize, message: Message) { 41 | if let Err(err) = self.0.read().await[index].lock().await.send(message).await { 42 | error!("message send error: {}", err) 43 | } 44 | } 45 | 46 | async fn register(&self, sender: Sender) -> usize { 47 | self.0.write().await.insert(Mutex::new(sender)) 48 | } 49 | 50 | async fn deregister(&self, index: usize) -> Sender { 51 | self.0.write().await.remove(index).into_inner() 52 | } 53 | } 54 | 55 | async fn handle_message( 56 | ctx: &Context, 57 | index: usize, 58 | mut receiver: SplitStream, 59 | ) -> Result<(), WsError> { 60 | while let Some(message) = receiver.next().await { 61 | let message = message?; 62 | match message { 63 | Message::Close(frame) => { 64 | debug!("websocket connection close: {:?}", frame); 65 | break; 66 | } 67 | Message::Ping(data) => ctx.send(index, Message::Pong(data)).await, 68 | Message::Pong(data) => warn!("ignored pong: {:?}", data), 69 | msg => ctx.broadcast(msg).await, 70 | } 71 | } 72 | Ok(()) 73 | } 74 | 75 | fn route(prefix: &'static str) -> Result, RouterError> { 76 | Router::new() 77 | .on( 78 | "/chat", 79 | allow( 80 | [Method::GET], 81 | Websocket::new(|ctx: Context, stream| async move { 82 | let (sender, receiver) = stream.split(); 83 | let index = ctx.register(sender).await; 84 | let result = handle_message(&ctx, index, receiver).await; 85 | let mut sender = ctx.deregister(index).await; 86 | if let Err(err) = result { 87 | let result = sender 88 | .send(Message::Close(Some(CloseFrame { 89 | code: CloseCode::Invalid, 90 | reason: Cow::Owned(err.to_string()), 91 | }))) 92 | .await; 93 | if let Err(err) = result { 94 | warn!("send close message error: {}", err) 95 | } 96 | } 97 | }), 98 | ), 99 | ) 100 | .routes(prefix) 101 | } 102 | 103 | #[tokio::main] 104 | async fn main() -> Result<(), Box> { 105 | tracing_subscriber::fmt() 106 | .with_env_filter(EnvFilter::from_default_env()) 107 | .try_init() 108 | .map_err(|err| anyhow::anyhow!("fail to init tracing subscriber: {}", err))?; 109 | 110 | let app = App::state(SyncChannel::new()).gate(logger).end(route("/")?); 111 | app.listen("127.0.0.1:8000", |addr| { 112 | info!("Server is listening on {}", addr) 113 | })? 114 | .await?; 115 | Ok(()) 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use std::time::Duration; 121 | 122 | use roa::preload::*; 123 | use tokio_tungstenite::connect_async; 124 | 125 | use super::{route, App, Message, SinkExt, StdError, StreamExt, SyncChannel}; 126 | 127 | #[tokio::test] 128 | async fn echo() -> Result<(), Box> { 129 | let channel = SyncChannel::new(); 130 | let app = App::state(channel.clone()).end(route("/")?); 131 | let (addr, server) = app.run()?; 132 | tokio::task::spawn(server); 133 | let (ws_stream, _) = connect_async(format!("ws://{}/chat", addr)).await?; 134 | let (mut sender, mut recv) = ws_stream.split(); 135 | tokio::time::sleep(Duration::from_secs(1)).await; 136 | assert_eq!(1, channel.0.read().await.len()); 137 | 138 | // ping 139 | sender 140 | .send(Message::Ping(b"Hello, World!".to_vec())) 141 | .await?; 142 | let msg = recv.next().await.unwrap()?; 143 | assert!(msg.is_pong()); 144 | assert_eq!(b"Hello, World!".as_ref(), msg.into_data().as_slice()); 145 | 146 | // close 147 | sender.send(Message::Close(None)).await?; 148 | tokio::time::sleep(Duration::from_secs(1)).await; 149 | assert_eq!(0, channel.0.read().await.len()); 150 | Ok(()) 151 | } 152 | 153 | #[tokio::test] 154 | async fn broadcast() -> Result<(), Box> { 155 | let channel = SyncChannel::new(); 156 | let app = App::state(channel.clone()).end(route("/")?); 157 | let (addr, server) = app.run()?; 158 | tokio::task::spawn(server); 159 | let url = format!("ws://{}/chat", addr); 160 | for _ in 0..100 { 161 | let url = url.clone(); 162 | tokio::task::spawn(async move { 163 | if let Ok((ws_stream, _)) = connect_async(url).await { 164 | let (mut sender, mut recv) = ws_stream.split(); 165 | if let Some(Ok(message)) = recv.next().await { 166 | assert!(sender.send(message).await.is_ok()); 167 | } 168 | tokio::time::sleep(Duration::from_secs(1)).await; 169 | assert!(sender.send(Message::Close(None)).await.is_ok()); 170 | } 171 | }); 172 | } 173 | tokio::time::sleep(Duration::from_secs(1)).await; 174 | assert_eq!(100, channel.0.read().await.len()); 175 | 176 | let (ws_stream, _) = connect_async(url).await?; 177 | let (mut sender, mut recv) = ws_stream.split(); 178 | assert!(sender 179 | .send(Message::Text("Hello, World!".to_string())) 180 | .await 181 | .is_ok()); 182 | tokio::time::sleep(Duration::from_secs(2)).await; 183 | assert_eq!(1, channel.0.read().await.len()); 184 | 185 | let mut counter = 0i32; 186 | while let Some(item) = recv.next().await { 187 | if let Ok(Message::Text(message)) = item { 188 | assert_eq!("Hello, World!", message); 189 | } 190 | counter += 1; 191 | if counter == 101 { 192 | break; 193 | } 194 | } 195 | Ok(()) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /roa-async-std/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Hexilee "] 3 | categories = [ 4 | "network-programming", 5 | "asynchronous", 6 | "web-programming::http-server", 7 | ] 8 | description = "tokio-based runtime and acceptor" 9 | documentation = "https://docs.rs/roa-tokio" 10 | edition = "2018" 11 | homepage = "https://github.com/Hexilee/roa/wiki" 12 | keywords = ["http", "web", "framework", "async"] 13 | license = "MIT" 14 | name = "roa-async-std" 15 | readme = "./README.md" 16 | repository = "https://github.com/Hexilee/roa" 17 | version = "0.6.0" 18 | 19 | [package.metadata.docs.rs] 20 | features = ["docs"] 21 | rustdoc-args = ["--cfg", "feature=\"docs\""] 22 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 23 | 24 | [dependencies] 25 | futures = "0.3" 26 | tracing = "0.1" 27 | roa = {path = "../roa", version = "0.6.0", default-features = false} 28 | async-std = {version = "1.10", features = ["unstable"]} 29 | futures-timer = "3.0" 30 | 31 | [dev-dependencies] 32 | reqwest = "0.11" 33 | roa = {path = "../roa", version = "0.6.0"} 34 | tracing-subscriber = { version = "0.3", features = ["env-filter"]} 35 | tokio = { version = "1.15", features = ["full"] } 36 | async-std = {version = "1.10", features = ["attributes", "unstable"]} 37 | 38 | [features] 39 | docs = ["roa/docs"] 40 | -------------------------------------------------------------------------------- /roa-async-std/README.md: -------------------------------------------------------------------------------- 1 | [![Stable Test](https://github.com/Hexilee/roa/workflows/Stable%20Test/badge.svg)](https://github.com/Hexilee/roa/actions) 2 | [![codecov](https://codecov.io/gh/Hexilee/roa/branch/master/graph/badge.svg)](https://codecov.io/gh/Hexilee/roa) 3 | [![Rust Docs](https://docs.rs/roa-async-std/badge.svg)](https://docs.rs/roa-async-std) 4 | [![Crate version](https://img.shields.io/crates/v/roa-async-std.svg)](https://crates.io/crates/roa-async-std) 5 | [![Download](https://img.shields.io/crates/d/roa-async-std.svg)](https://crates.io/crates/roa-async-std) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Hexilee/roa/blob/master/LICENSE) 7 | 8 | This crate provides async-std-based runtime and acceptor for roa. 9 | 10 | ```rust,no_run 11 | use roa::http::StatusCode; 12 | use roa::{App, Context}; 13 | use roa_async_std::{Listener, Exec}; 14 | use std::error::Error; 15 | 16 | async fn end(_ctx: &mut Context) -> roa::Result { 17 | Ok(()) 18 | } 19 | 20 | #[async_std::main] 21 | async fn main() -> Result<(), Box> { 22 | let (addr, server) = App::with_exec((), Exec).end(end).run()?; 23 | println!("server is listening on {}", addr); 24 | server.await?; 25 | Ok(()) 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /roa-async-std/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "docs", doc = include_str!("../README.md"))] 2 | #![cfg_attr(feature = "docs", warn(missing_docs))] 3 | 4 | mod listener; 5 | mod net; 6 | mod runtime; 7 | 8 | #[doc(inline)] 9 | pub use listener::Listener; 10 | #[doc(inline)] 11 | pub use net::TcpIncoming; 12 | #[doc(inline)] 13 | pub use runtime::Exec; 14 | -------------------------------------------------------------------------------- /roa-async-std/src/listener.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, ToSocketAddrs}; 2 | use std::sync::Arc; 3 | 4 | use roa::{App, Endpoint, Executor, Server, State}; 5 | 6 | use super::TcpIncoming; 7 | 8 | /// An app extension. 9 | pub trait Listener { 10 | /// http server 11 | type Server; 12 | 13 | /// Listen on a socket addr, return a server and the real addr it binds. 14 | fn bind(self, addr: impl ToSocketAddrs) -> std::io::Result<(SocketAddr, Self::Server)>; 15 | 16 | /// Listen on a socket addr, return a server, and pass real addr to the callback. 17 | fn listen( 18 | self, 19 | addr: impl ToSocketAddrs, 20 | callback: impl Fn(SocketAddr), 21 | ) -> std::io::Result; 22 | 23 | /// Listen on an unused port of 127.0.0.1, return a server and the real addr it binds. 24 | /// ### Example 25 | /// ```rust,no_run 26 | /// use roa::{App, Context, Status}; 27 | /// use roa_async_std::{Exec, Listener}; 28 | /// use roa::http::StatusCode; 29 | /// use async_std::task::spawn; 30 | /// use std::time::Instant; 31 | /// 32 | /// async fn end(_ctx: &mut Context) -> Result<(), Status> { 33 | /// Ok(()) 34 | /// } 35 | /// 36 | /// #[async_std::main] 37 | /// async fn main() -> Result<(), Box> { 38 | /// let (_, server) = App::with_exec((), Exec).end(end).run()?; 39 | /// server.await?; 40 | /// Ok(()) 41 | /// } 42 | /// ``` 43 | fn run(self) -> std::io::Result<(SocketAddr, Self::Server)>; 44 | } 45 | 46 | impl Listener for App> 47 | where 48 | S: State, 49 | E: for<'a> Endpoint<'a, S>, 50 | { 51 | type Server = Server; 52 | fn bind(self, addr: impl ToSocketAddrs) -> std::io::Result<(SocketAddr, Self::Server)> { 53 | let incoming = TcpIncoming::bind(addr)?; 54 | let local_addr = incoming.local_addr(); 55 | Ok((local_addr, self.accept(incoming))) 56 | } 57 | 58 | fn listen( 59 | self, 60 | addr: impl ToSocketAddrs, 61 | callback: impl Fn(SocketAddr), 62 | ) -> std::io::Result { 63 | let (addr, server) = self.bind(addr)?; 64 | callback(addr); 65 | Ok(server) 66 | } 67 | 68 | fn run(self) -> std::io::Result<(SocketAddr, Self::Server)> { 69 | self.bind("127.0.0.1:0") 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use std::error::Error; 76 | 77 | use roa::http::StatusCode; 78 | use roa::App; 79 | 80 | use super::Listener; 81 | use crate::Exec; 82 | 83 | #[tokio::test] 84 | async fn incoming() -> Result<(), Box> { 85 | let (addr, server) = App::with_exec((), Exec).end(()).run()?; 86 | tokio::task::spawn(server); 87 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 88 | assert_eq!(StatusCode::OK, resp.status()); 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /roa-async-std/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | 4 | use roa::Spawn; 5 | 6 | /// Future Object 7 | pub type FutureObj = Pin>>; 8 | 9 | /// Blocking task Object 10 | pub type BlockingObj = Box; 11 | 12 | /// Tokio-based executor. 13 | /// 14 | /// ``` 15 | /// use roa::App; 16 | /// use roa_async_std::Exec; 17 | /// 18 | /// let app = App::with_exec((), Exec); 19 | /// ``` 20 | pub struct Exec; 21 | 22 | impl Spawn for Exec { 23 | #[inline] 24 | fn spawn(&self, fut: FutureObj) { 25 | async_std::task::spawn(fut); 26 | } 27 | 28 | #[inline] 29 | fn spawn_blocking(&self, task: BlockingObj) { 30 | async_std::task::spawn_blocking(task); 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use std::error::Error; 37 | 38 | use roa::http::StatusCode; 39 | use roa::tcp::Listener; 40 | use roa::App; 41 | 42 | use super::Exec; 43 | 44 | #[tokio::test] 45 | async fn exec() -> Result<(), Box> { 46 | let app = App::with_exec((), Exec).end(()); 47 | let (addr, server) = app.bind("127.0.0.1:0")?; 48 | tokio::spawn(server); 49 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 50 | assert_eq!(StatusCode::OK, resp.status()); 51 | Ok(()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /roa-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roa-core" 3 | version = "0.6.1" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | license = "MIT" 7 | readme = "./README.md" 8 | repository = "https://github.com/Hexilee/roa" 9 | documentation = "https://docs.rs/roa-core" 10 | homepage = "https://github.com/Hexilee/roa/wiki" 11 | description = "core components of roa web framework" 12 | keywords = ["http", "web", "framework", "async"] 13 | categories = ["network-programming", "asynchronous", 14 | "web-programming::http-server"] 15 | 16 | 17 | [package.metadata.docs.rs] 18 | features = ["docs"] 19 | rustdoc-args = ["--cfg", "feature=\"docs\""] 20 | 21 | [badges] 22 | codecov = { repository = "Hexilee/roa" } 23 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 24 | 25 | [dependencies] 26 | futures = "0.3" 27 | bytes = "1.1" 28 | http = "0.2" 29 | hyper = { version = "0.14", default-features = false, features = ["stream", "server", "http1", "http2"] } 30 | tracing = "0.1" 31 | tokio = "1.15" 32 | tokio-util = { version = "0.6.9", features = ["io"] } 33 | async-trait = "0.1.51" 34 | crossbeam-queue = "0.3" 35 | 36 | [dev-dependencies] 37 | tokio = { version = "1.15", features = ["fs", "macros", "rt"] } 38 | 39 | [features] 40 | runtime = ["tokio/rt"] 41 | docs = ["runtime"] 42 | -------------------------------------------------------------------------------- /roa-core/README.md: -------------------------------------------------------------------------------- 1 | [![Stable Test](https://github.com/Hexilee/roa/workflows/Stable%20Test/badge.svg)](https://github.com/Hexilee/roa/actions) 2 | [![codecov](https://codecov.io/gh/Hexilee/roa/branch/master/graph/badge.svg)](https://codecov.io/gh/Hexilee/roa) 3 | [![Rust Docs](https://docs.rs/roa-core/badge.svg)](https://docs.rs/roa-core) 4 | [![Crate version](https://img.shields.io/crates/v/roa-core.svg)](https://crates.io/crates/roa-core) 5 | [![Download](https://img.shields.io/crates/d/roa-core.svg)](https://crates.io/crates/roa-core) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Hexilee/roa/blob/master/LICENSE) 7 | 8 | ### Introduction 9 | 10 | Core components of Roa framework. 11 | 12 | If you are new to roa, please go to the documentation of [roa framework](https://docs.rs/roa). 13 | 14 | ### Application 15 | 16 | A Roa application is a structure composing and executing middlewares and an endpoint in a stack-like manner. 17 | 18 | The obligatory hello world application: 19 | 20 | ```rust 21 | use roa_core::App; 22 | let app = App::new().end("Hello, World"); 23 | ``` 24 | 25 | #### Endpoint 26 | 27 | An endpoint is a request handler. 28 | 29 | There are some build-in endpoints in roa_core. 30 | 31 | - Functional endpoint 32 | 33 | A normal functional endpoint is an async function with signature: 34 | `async fn(&mut Context) -> Result`. 35 | 36 | ```rust 37 | use roa_core::{App, Context, Result}; 38 | 39 | async fn endpoint(ctx: &mut Context) -> Result { 40 | Ok(()) 41 | } 42 | 43 | let app = App::new().end(endpoint); 44 | ``` 45 | 46 | - Ok endpoint 47 | 48 | `()` is an endpoint always return `Ok(())` 49 | 50 | ```rust 51 | let app = roa_core::App::new().end(()); 52 | ``` 53 | 54 | - Status endpoint 55 | 56 | `Status` is an endpoint always return `Err(Status)` 57 | 58 | ```rust 59 | use roa_core::{App, status}; 60 | use roa_core::http::StatusCode; 61 | let app = App::new().end(status!(StatusCode::BAD_REQUEST)); 62 | ``` 63 | 64 | - String endpoint 65 | 66 | Write string to body. 67 | 68 | ```rust 69 | use roa_core::App; 70 | 71 | let app = App::new().end("Hello, world"); // static slice 72 | let app = App::new().end("Hello, world".to_owned()); // string 73 | ``` 74 | 75 | - Redirect endpoint 76 | 77 | Redirect to an uri. 78 | 79 | ```rust 80 | use roa_core::App; 81 | use roa_core::http::Uri; 82 | 83 | let app = App::new().end("/target".parse::().unwrap()); 84 | ``` 85 | 86 | 87 | #### Cascading 88 | 89 | The following example responds with "Hello World", however, the request flows through 90 | the `logging` middleware to mark when the request started, then continue 91 | to yield control through the endpoint. When a middleware invokes `next.await` 92 | the function suspends and passes control to the next middleware or endpoint. After the endpoint is called, 93 | the stack will unwind and each middleware is resumed to perform 94 | its upstream behaviour. 95 | 96 | ```rust 97 | use roa_core::{App, Context, Result, Status, MiddlewareExt, Next}; 98 | use std::time::Instant; 99 | use tracing::info; 100 | 101 | let app = App::new().gate(logging).end("Hello, World"); 102 | 103 | async fn logging(ctx: &mut Context, next: Next<'_>) -> Result { 104 | let inbound = Instant::now(); 105 | next.await?; 106 | info!("time elapsed: {} ms", inbound.elapsed().as_millis()); 107 | Ok(()) 108 | } 109 | ``` 110 | 111 | ### Status Handling 112 | 113 | You can catch or straightly throw a status returned by next. 114 | 115 | ```rust 116 | use roa_core::{App, Context, Result, Status, MiddlewareExt, Next, throw}; 117 | use roa_core::http::StatusCode; 118 | 119 | let app = App::new().gate(catch).gate(gate).end(end); 120 | 121 | async fn catch(ctx: &mut Context, next: Next<'_>) -> Result { 122 | // catch 123 | if let Err(status) = next.await { 124 | // teapot is ok 125 | if status.status_code != StatusCode::IM_A_TEAPOT { 126 | return Err(status); 127 | } 128 | } 129 | Ok(()) 130 | } 131 | async fn gate(ctx: &mut Context, next: Next<'_>) -> Result { 132 | next.await?; // just throw 133 | unreachable!() 134 | } 135 | 136 | async fn end(ctx: &mut Context) -> Result { 137 | throw!(StatusCode::IM_A_TEAPOT, "I'm a teapot!") 138 | } 139 | ``` 140 | 141 | #### status_handler 142 | App has an status_handler to handle `Status` thrown by the top middleware. 143 | This is the status_handler: 144 | 145 | ```rust 146 | use roa_core::{Context, Status, Result, State}; 147 | pub fn status_handler(ctx: &mut Context, status: Status) { 148 | ctx.resp.status = status.status_code; 149 | if status.expose { 150 | ctx.resp.write(status.message); 151 | } else { 152 | tracing::error!("{}", status); 153 | } 154 | } 155 | ``` 156 | 157 | ### HTTP Server. 158 | 159 | Use `roa_core::accept` to construct a http server. 160 | Please refer to `roa::tcp` for more information. -------------------------------------------------------------------------------- /roa-core/src/app/future.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | 4 | use futures::task::{Context, Poll}; 5 | 6 | /// A wrapper to make future `Send`. It's used to wrap future returned by top middleware. 7 | /// So the future returned by each middleware or endpoint can be `?Send`. 8 | /// 9 | /// But how to ensure thread safety? Because the middleware and the context must be `Sync + Send`, 10 | /// which means the only factor causing future `!Send` is the variables generated in `Future::poll`. 11 | /// And these variable mustn't be accessed from other threads. 12 | #[allow(clippy::non_send_fields_in_send_ty)] 13 | pub struct SendFuture(pub F); 14 | 15 | impl Future for SendFuture 16 | where 17 | F: 'static + Future + Unpin, 18 | { 19 | type Output = F::Output; 20 | #[inline] 21 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 22 | Pin::new(&mut self.0).poll(cx) 23 | } 24 | } 25 | 26 | unsafe impl Send for SendFuture {} 27 | -------------------------------------------------------------------------------- /roa-core/src/app/runtime.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::{BlockingObj, FutureObj}; 2 | use crate::{App, Spawn}; 3 | 4 | impl App { 5 | /// Construct app with default runtime. 6 | #[cfg_attr(feature = "docs", doc(cfg(feature = "runtime")))] 7 | #[inline] 8 | pub fn state(state: S) -> Self { 9 | Self::with_exec(state, Exec) 10 | } 11 | } 12 | 13 | impl App<(), ()> { 14 | /// Construct app with default runtime. 15 | #[cfg_attr(feature = "docs", doc(cfg(feature = "runtime")))] 16 | #[inline] 17 | pub fn new() -> Self { 18 | Self::state(()) 19 | } 20 | } 21 | 22 | impl Default for App<(), ()> { 23 | /// Construct app with default runtime. 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | pub struct Exec; 30 | 31 | impl Spawn for Exec { 32 | #[inline] 33 | fn spawn(&self, fut: FutureObj) { 34 | tokio::task::spawn(fut); 35 | } 36 | 37 | #[inline] 38 | fn spawn_blocking(&self, task: BlockingObj) { 39 | tokio::task::spawn_blocking(task); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /roa-core/src/app/stream.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::io; 3 | use std::net::SocketAddr; 4 | use std::pin::Pin; 5 | use std::task::{self, Poll}; 6 | 7 | use futures::ready; 8 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 9 | use tracing::{instrument, trace}; 10 | 11 | /// A transport returned yieled by `AddrIncoming`. 12 | pub struct AddrStream { 13 | /// The remote address of this stream. 14 | pub remote_addr: SocketAddr, 15 | 16 | /// The inner stream. 17 | pub stream: IO, 18 | } 19 | 20 | impl AddrStream { 21 | /// Construct an AddrStream from an addr and a AsyncReadWriter. 22 | #[inline] 23 | pub fn new(remote_addr: SocketAddr, stream: IO) -> AddrStream { 24 | AddrStream { 25 | remote_addr, 26 | stream, 27 | } 28 | } 29 | } 30 | 31 | impl AsyncRead for AddrStream 32 | where 33 | IO: Unpin + AsyncRead, 34 | { 35 | #[inline] 36 | #[instrument(skip(cx, buf))] 37 | fn poll_read( 38 | mut self: Pin<&mut Self>, 39 | cx: &mut task::Context<'_>, 40 | buf: &mut ReadBuf<'_>, 41 | ) -> Poll> { 42 | let poll = Pin::new(&mut self.stream).poll_read(cx, buf); 43 | trace!("poll read: {:?}", poll); 44 | ready!(poll)?; 45 | trace!("read {} bytes", buf.filled().len()); 46 | Poll::Ready(Ok(())) 47 | } 48 | } 49 | 50 | impl AsyncWrite for AddrStream 51 | where 52 | IO: Unpin + AsyncWrite, 53 | { 54 | #[inline] 55 | #[instrument(skip(cx, buf))] 56 | fn poll_write( 57 | mut self: Pin<&mut Self>, 58 | cx: &mut task::Context<'_>, 59 | buf: &[u8], 60 | ) -> Poll> { 61 | let write_size = ready!(Pin::new(&mut self.stream).poll_write(cx, buf))?; 62 | trace!("wrote {} bytes", write_size); 63 | Poll::Ready(Ok(write_size)) 64 | } 65 | 66 | #[inline] 67 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 68 | Pin::new(&mut self.stream).poll_flush(cx) 69 | } 70 | 71 | #[inline] 72 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 73 | Pin::new(&mut self.stream).poll_shutdown(cx) 74 | } 75 | } 76 | 77 | impl Debug for AddrStream { 78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 | f.debug_struct("AddrStream") 80 | .field("remote_addr", &self.remote_addr) 81 | .finish() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /roa-core/src/body.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use bytes::{Bytes, BytesMut}; 6 | use futures::future::ok; 7 | use futures::stream::{once, Stream, StreamExt}; 8 | use tokio::io::{self, AsyncRead, ReadBuf}; 9 | 10 | const DEFAULT_CHUNK_SIZE: usize = 4096; 11 | 12 | /// The body of response. 13 | /// 14 | /// ### Example 15 | /// 16 | /// ```rust 17 | /// use roa_core::Body; 18 | /// use futures::StreamExt; 19 | /// use std::io; 20 | /// use bytes::Bytes; 21 | /// 22 | /// async fn read_body(body: Body) -> io::Result { 23 | /// Ok(match body { 24 | /// Body::Empty => Bytes::new(), 25 | /// Body::Once(bytes) => bytes, 26 | /// Body::Stream(mut stream) => { 27 | /// let mut bytes = Vec::new(); 28 | /// while let Some(item) = stream.next().await { 29 | /// bytes.extend_from_slice(&*item?); 30 | /// } 31 | /// bytes.into() 32 | /// } 33 | /// }) 34 | /// } 35 | /// ``` 36 | pub enum Body { 37 | /// Empty kind 38 | Empty, 39 | 40 | /// Bytes kind. 41 | Once(Bytes), 42 | 43 | /// Stream kind. 44 | Stream(Segment), 45 | } 46 | 47 | /// A boxed stream. 48 | #[derive(Default)] 49 | pub struct Segment(Option> + Sync + Send + 'static>>>); 50 | 51 | impl Body { 52 | /// Construct an empty body. 53 | #[inline] 54 | pub fn empty() -> Self { 55 | Body::Empty 56 | } 57 | 58 | /// Construct a once body. 59 | #[inline] 60 | pub fn once(bytes: impl Into) -> Self { 61 | Body::Once(bytes.into()) 62 | } 63 | 64 | /// Construct an empty body of stream kind. 65 | #[inline] 66 | pub fn stream(stream: S) -> Self 67 | where 68 | S: Stream> + Sync + Send + 'static, 69 | { 70 | Body::Stream(Segment::new(stream)) 71 | } 72 | 73 | /// Write stream. 74 | #[inline] 75 | pub fn write_stream( 76 | &mut self, 77 | stream: impl Stream> + Sync + Send + 'static, 78 | ) -> &mut Self { 79 | match self { 80 | Body::Empty => { 81 | *self = Self::stream(stream); 82 | } 83 | Body::Once(bytes) => { 84 | let stream = once(ok(mem::take(bytes))).chain(stream); 85 | *self = Self::stream(stream); 86 | } 87 | Body::Stream(segment) => { 88 | *self = Self::stream(mem::take(segment).chain(stream)); 89 | } 90 | } 91 | self 92 | } 93 | 94 | /// Write reader with default chunk size. 95 | #[inline] 96 | pub fn write_reader( 97 | &mut self, 98 | reader: impl AsyncRead + Sync + Send + Unpin + 'static, 99 | ) -> &mut Self { 100 | self.write_chunk(reader, DEFAULT_CHUNK_SIZE) 101 | } 102 | 103 | /// Write reader with chunk size. 104 | #[inline] 105 | pub fn write_chunk( 106 | &mut self, 107 | reader: impl AsyncRead + Sync + Send + Unpin + 'static, 108 | chunk_size: usize, 109 | ) -> &mut Self { 110 | self.write_stream(ReaderStream::new(reader, chunk_size)) 111 | } 112 | 113 | /// Write `Bytes`. 114 | #[inline] 115 | pub fn write(&mut self, data: impl Into) -> &mut Self { 116 | match self { 117 | Body::Empty => { 118 | *self = Self::once(data.into()); 119 | self 120 | } 121 | body => body.write_stream(once(ok(data.into()))), 122 | } 123 | } 124 | } 125 | 126 | impl Segment { 127 | #[inline] 128 | fn new(stream: impl Stream> + Sync + Send + 'static) -> Self { 129 | Self(Some(Box::pin(stream))) 130 | } 131 | } 132 | 133 | impl From for hyper::Body { 134 | #[inline] 135 | fn from(body: Body) -> Self { 136 | match body { 137 | Body::Empty => hyper::Body::empty(), 138 | Body::Once(bytes) => hyper::Body::from(bytes), 139 | Body::Stream(stream) => hyper::Body::wrap_stream(stream), 140 | } 141 | } 142 | } 143 | 144 | impl Default for Body { 145 | #[inline] 146 | fn default() -> Self { 147 | Self::empty() 148 | } 149 | } 150 | 151 | pub struct ReaderStream { 152 | chunk_size: usize, 153 | reader: R, 154 | } 155 | 156 | impl ReaderStream { 157 | #[inline] 158 | fn new(reader: R, chunk_size: usize) -> Self { 159 | Self { reader, chunk_size } 160 | } 161 | } 162 | 163 | impl Stream for ReaderStream 164 | where 165 | R: AsyncRead + Unpin, 166 | { 167 | type Item = io::Result; 168 | #[inline] 169 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 170 | let chunk_size = self.chunk_size; 171 | let mut chunk = BytesMut::with_capacity(chunk_size); 172 | unsafe { chunk.set_len(chunk_size) }; 173 | let mut buf = ReadBuf::new(&mut *chunk); 174 | futures::ready!(Pin::new(&mut self.reader).poll_read(cx, &mut buf))?; 175 | let filled_len = buf.filled().len(); 176 | if filled_len == 0 { 177 | Poll::Ready(None) 178 | } else { 179 | Poll::Ready(Some(Ok(chunk.freeze().slice(0..filled_len)))) 180 | } 181 | } 182 | } 183 | 184 | impl Stream for Body { 185 | type Item = io::Result; 186 | #[inline] 187 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 188 | match &mut *self { 189 | Body::Empty => Poll::Ready(None), 190 | Body::Once(bytes) => { 191 | let data = mem::take(bytes); 192 | *self = Body::empty(); 193 | Poll::Ready(Some(Ok(data))) 194 | } 195 | Body::Stream(stream) => Pin::new(stream).poll_next(cx), 196 | } 197 | } 198 | } 199 | 200 | impl Stream for Segment { 201 | type Item = io::Result; 202 | #[inline] 203 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 204 | match self.0 { 205 | None => Poll::Ready(None), 206 | Some(ref mut stream) => stream.as_mut().poll_next(cx), 207 | } 208 | } 209 | } 210 | 211 | #[cfg(test)] 212 | mod tests { 213 | use std::io; 214 | 215 | use futures::{AsyncReadExt, TryStreamExt}; 216 | use tokio::fs::File; 217 | 218 | use super::Body; 219 | 220 | async fn read_body(body: Body) -> io::Result { 221 | let mut data = String::new(); 222 | body.into_async_read().read_to_string(&mut data).await?; 223 | Ok(data) 224 | } 225 | 226 | #[tokio::test] 227 | async fn body_empty() -> std::io::Result<()> { 228 | let body = Body::default(); 229 | assert_eq!("", read_body(body).await?); 230 | Ok(()) 231 | } 232 | 233 | #[tokio::test] 234 | async fn body_single() -> std::io::Result<()> { 235 | let mut body = Body::default(); 236 | body.write("Hello, World"); 237 | assert_eq!("Hello, World", read_body(body).await?); 238 | Ok(()) 239 | } 240 | 241 | #[tokio::test] 242 | async fn body_multiple() -> std::io::Result<()> { 243 | let mut body = Body::default(); 244 | body.write("He").write("llo, ").write("World"); 245 | assert_eq!("Hello, World", read_body(body).await?); 246 | Ok(()) 247 | } 248 | 249 | #[tokio::test] 250 | async fn body_composed() -> std::io::Result<()> { 251 | let mut body = Body::empty(); 252 | body.write("He") 253 | .write("llo, ") 254 | .write_reader(File::open("../assets/author.txt").await?) 255 | .write_reader(File::open("../assets/author.txt").await?) 256 | .write("."); 257 | assert_eq!("Hello, HexileeHexilee.", read_body(body).await?); 258 | Ok(()) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /roa-core/src/context/storage.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | use std::borrow::Cow; 3 | use std::collections::HashMap; 4 | use std::fmt::Display; 5 | use std::ops::Deref; 6 | use std::str::FromStr; 7 | use std::sync::Arc; 8 | 9 | use http::StatusCode; 10 | 11 | use crate::Status; 12 | 13 | pub trait Value: Any + Send + Sync {} 14 | 15 | impl Value for V where V: Any + Send + Sync {} 16 | 17 | /// A context scoped storage. 18 | #[derive(Clone)] 19 | pub struct Storage(HashMap, Arc>>); 20 | 21 | /// A wrapper of Arc. 22 | /// 23 | /// ### Deref 24 | /// 25 | /// ```rust 26 | /// use roa_core::Variable; 27 | /// 28 | /// fn consume(var: Variable) { 29 | /// let value: &V = &var; 30 | /// } 31 | /// ``` 32 | /// 33 | /// ### Parse 34 | /// 35 | /// ```rust 36 | /// use roa_core::{Variable, Result}; 37 | /// 38 | /// fn consume>(var: Variable) -> Result { 39 | /// let value: i32 = var.parse()?; 40 | /// Ok(()) 41 | /// } 42 | /// ``` 43 | #[derive(Debug, Clone)] 44 | pub struct Variable<'a, V> { 45 | key: &'a str, 46 | value: Arc, 47 | } 48 | 49 | impl Deref for Variable<'_, V> { 50 | type Target = V; 51 | #[inline] 52 | fn deref(&self) -> &Self::Target { 53 | &self.value 54 | } 55 | } 56 | 57 | impl<'a, V> Variable<'a, V> { 58 | /// Construct a variable from name and value. 59 | #[inline] 60 | fn new(key: &'a str, value: Arc) -> Self { 61 | Self { key, value } 62 | } 63 | 64 | /// Consume self and get inner Arc. 65 | #[inline] 66 | pub fn value(self) -> Arc { 67 | self.value 68 | } 69 | } 70 | 71 | impl Variable<'_, V> 72 | where 73 | V: AsRef, 74 | { 75 | /// A wrapper of `str::parse`. Converts `T::FromStr::Err` to `roa_core::Error` automatically. 76 | #[inline] 77 | pub fn parse(&self) -> Result 78 | where 79 | T: FromStr, 80 | T::Err: Display, 81 | { 82 | self.as_ref().parse().map_err(|err| { 83 | Status::new( 84 | StatusCode::BAD_REQUEST, 85 | format!( 86 | "{}\ntype of variable `{}` should be {}", 87 | err, 88 | self.key, 89 | std::any::type_name::() 90 | ), 91 | true, 92 | ) 93 | }) 94 | } 95 | } 96 | 97 | impl Storage { 98 | /// Construct an empty Bucket. 99 | #[inline] 100 | pub fn new() -> Self { 101 | Self(HashMap::new()) 102 | } 103 | 104 | /// Inserts a key-value pair into the storage. 105 | /// 106 | /// If the storage did not have this key present, [`None`] is returned. 107 | /// 108 | /// If the storage did have this key present, the value is updated, and the old 109 | /// value is returned. 110 | pub fn insert(&mut self, scope: S, key: K, value: V) -> Option> 111 | where 112 | S: Any, 113 | K: Into>, 114 | V: Value, 115 | { 116 | let id = TypeId::of::(); 117 | match self.0.get_mut(&id) { 118 | Some(bucket) => bucket 119 | .insert(key.into(), Arc::new(value)) 120 | .and_then(|value| value.downcast().ok()), 121 | None => { 122 | self.0.insert(id, HashMap::new()); 123 | self.insert(scope, key, value) 124 | } 125 | } 126 | } 127 | 128 | /// If the storage did not have this key present, [`None`] is returned. 129 | /// 130 | /// If the storage did have this key present, the key-value pair will be returned as a `Variable` 131 | #[inline] 132 | pub fn get<'a, S, V>(&self, key: &'a str) -> Option> 133 | where 134 | S: Any, 135 | V: Value, 136 | { 137 | let value = self.0.get(&TypeId::of::())?.get(key)?.clone(); 138 | Some(Variable::new(key, value.clone().downcast().ok()?)) 139 | } 140 | } 141 | 142 | impl Default for Storage { 143 | #[inline] 144 | fn default() -> Self { 145 | Self::new() 146 | } 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use std::sync::Arc; 152 | 153 | use http::StatusCode; 154 | 155 | use super::{Storage, Variable}; 156 | 157 | #[test] 158 | fn storage() { 159 | struct Scope; 160 | 161 | let mut storage = Storage::default(); 162 | assert!(storage.get::("id").is_none()); 163 | assert!(storage.insert(Scope, "id", "1").is_none()); 164 | let id: i32 = storage 165 | .get::("id") 166 | .unwrap() 167 | .parse() 168 | .unwrap(); 169 | assert_eq!(1, id); 170 | assert_eq!( 171 | 1, 172 | storage 173 | .insert(Scope, "id", "2") 174 | .unwrap() 175 | .parse::() 176 | .unwrap() 177 | ); 178 | } 179 | 180 | #[test] 181 | fn variable() { 182 | assert_eq!( 183 | 1, 184 | Variable::new("id", Arc::new("1")).parse::().unwrap() 185 | ); 186 | let result = Variable::new("id", Arc::new("x")).parse::(); 187 | assert!(result.is_err()); 188 | let status = result.unwrap_err(); 189 | assert_eq!(StatusCode::BAD_REQUEST, status.status_code); 190 | assert!(status 191 | .message 192 | .ends_with("type of variable `id` should be usize")); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /roa-core/src/err.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | use std::result::Result as StdResult; 3 | 4 | pub use http::StatusCode; 5 | 6 | /// Type alias for `StdResult`. 7 | pub type Result = StdResult; 8 | 9 | /// Construct a `Status`. 10 | /// 11 | /// - `status!(status_code)` will be expanded to `status!(status_code, "")` 12 | /// - `status!(status_code, message)` will be expanded to `status!(status_code, message, true)` 13 | /// - `status!(status_code, message, expose)` will be expanded to `Status::new(status_code, message, expose)` 14 | /// 15 | /// ### Example 16 | /// ```rust 17 | /// use roa_core::{App, Context, Next, Result, status}; 18 | /// use roa_core::http::StatusCode; 19 | /// 20 | /// let app = App::new() 21 | /// .gate(gate) 22 | /// .end(status!(StatusCode::IM_A_TEAPOT, "I'm a teapot!")); 23 | /// async fn gate(ctx: &mut Context, next: Next<'_>) -> Result { 24 | /// next.await?; // throw 25 | /// unreachable!(); 26 | /// ctx.resp.status = StatusCode::OK; 27 | /// Ok(()) 28 | /// } 29 | /// ``` 30 | #[macro_export] 31 | macro_rules! status { 32 | ($status_code:expr) => { 33 | $crate::status!($status_code, "") 34 | }; 35 | ($status_code:expr, $message:expr) => { 36 | $crate::status!($status_code, $message, true) 37 | }; 38 | ($status_code:expr, $message:expr, $expose:expr) => { 39 | $crate::Status::new($status_code, $message, $expose) 40 | }; 41 | } 42 | 43 | /// Throw an `Err(Status)`. 44 | /// 45 | /// - `throw!(status_code)` will be expanded to `throw!(status_code, "")` 46 | /// - `throw!(status_code, message)` will be expanded to `throw!(status_code, message, true)` 47 | /// - `throw!(status_code, message, expose)` will be expanded to `return Err(Status::new(status_code, message, expose));` 48 | /// 49 | /// ### Example 50 | /// ```rust 51 | /// use roa_core::{App, Context, Next, Result, throw}; 52 | /// use roa_core::http::StatusCode; 53 | /// 54 | /// let app = App::new().gate(gate).end(end); 55 | /// async fn gate(ctx: &mut Context, next: Next<'_>) -> Result { 56 | /// next.await?; // throw 57 | /// unreachable!(); 58 | /// ctx.resp.status = StatusCode::OK; 59 | /// Ok(()) 60 | /// } 61 | /// 62 | /// async fn end(ctx: &mut Context) -> Result { 63 | /// throw!(StatusCode::IM_A_TEAPOT, "I'm a teapot!"); // throw 64 | /// unreachable!() 65 | /// } 66 | /// ``` 67 | #[macro_export] 68 | macro_rules! throw { 69 | ($status_code:expr) => { 70 | return core::result::Result::Err($crate::status!($status_code)) 71 | }; 72 | ($status_code:expr, $message:expr) => { 73 | return core::result::Result::Err($crate::status!($status_code, $message)) 74 | }; 75 | ($status_code:expr, $message:expr, $expose:expr) => { 76 | return core::result::Result::Err($crate::status!($status_code, $message, $expose)) 77 | }; 78 | } 79 | 80 | /// The `Status` of roa. 81 | #[derive(Debug, Clone, Eq, PartialEq)] 82 | pub struct Status { 83 | /// StatusCode will be responded to client if Error is thrown by the top middleware. 84 | /// 85 | /// ### Example 86 | /// ```rust 87 | /// use roa_core::{App, Context, Next, Result, MiddlewareExt, throw}; 88 | /// use roa_core::http::StatusCode; 89 | /// 90 | /// let app = App::new().gate(gate).end(end); 91 | /// async fn gate(ctx: &mut Context, next: Next<'_>) -> Result { 92 | /// ctx.resp.status = StatusCode::OK; 93 | /// next.await // not caught 94 | /// } 95 | /// 96 | /// async fn end(ctx: &mut Context) -> Result { 97 | /// throw!(StatusCode::IM_A_TEAPOT, "I'm a teapot!"); // throw 98 | /// unreachable!() 99 | /// } 100 | /// ``` 101 | pub status_code: StatusCode, 102 | 103 | /// Data will be written to response body if self.expose is true. 104 | /// StatusCode will be responded to client if Error is thrown by the top middleware. 105 | /// 106 | /// ### Example 107 | /// ```rust 108 | /// use roa_core::{App, Context, Result, Status}; 109 | /// use roa_core::http::StatusCode; 110 | /// 111 | /// let app = App::new().end(end); 112 | /// 113 | /// async fn end(ctx: &mut Context) -> Result { 114 | /// Err(Status::new(StatusCode::IM_A_TEAPOT, "I'm a teapot!", false)) // message won't be exposed to user. 115 | /// } 116 | /// 117 | /// ``` 118 | pub message: String, 119 | 120 | /// if message exposed. 121 | pub expose: bool, 122 | } 123 | 124 | impl Status { 125 | /// Construct an error. 126 | #[inline] 127 | pub fn new(status_code: StatusCode, message: impl ToString, expose: bool) -> Self { 128 | Self { 129 | status_code, 130 | message: message.to_string(), 131 | expose, 132 | } 133 | } 134 | } 135 | 136 | impl From for Status 137 | where 138 | E: std::error::Error, 139 | { 140 | #[inline] 141 | fn from(err: E) -> Self { 142 | Self::new(StatusCode::INTERNAL_SERVER_ERROR, err, false) 143 | } 144 | } 145 | 146 | impl Display for Status { 147 | #[inline] 148 | fn fmt(&self, f: &mut Formatter<'_>) -> StdResult<(), std::fmt::Error> { 149 | f.write_str(&format!("{}: {}", self.status_code, self.message)) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /roa-core/src/executor.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::sync::Arc; 4 | 5 | use futures::channel::oneshot::{channel, Receiver}; 6 | use futures::task::{Context, Poll}; 7 | use hyper::rt; 8 | 9 | /// Future Object 10 | pub type FutureObj = Pin>>; 11 | 12 | /// Blocking task Object 13 | pub type BlockingObj = Box; 14 | 15 | /// Executor constraint. 16 | pub trait Spawn { 17 | /// Spawn a future object 18 | fn spawn(&self, fut: FutureObj); 19 | 20 | /// Spawn a blocking task object 21 | fn spawn_blocking(&self, task: BlockingObj); 22 | } 23 | 24 | /// A type implementing hyper::rt::Executor 25 | #[derive(Clone)] 26 | pub struct Executor(pub(crate) Arc); 27 | 28 | /// A handle that awaits the result of a task. 29 | pub struct JoinHandle(Receiver); 30 | 31 | impl Executor { 32 | /// Spawn a future by app runtime 33 | #[inline] 34 | pub fn spawn(&self, fut: Fut) -> JoinHandle 35 | where 36 | Fut: 'static + Send + Future, 37 | Fut::Output: 'static + Send, 38 | { 39 | let (sender, recv) = channel(); 40 | self.0.spawn(Box::pin(async move { 41 | if sender.send(fut.await).is_err() { 42 | // handler is dropped, do nothing. 43 | }; 44 | })); 45 | JoinHandle(recv) 46 | } 47 | 48 | /// Spawn a blocking task by app runtime 49 | #[inline] 50 | pub fn spawn_blocking(&self, task: T) -> JoinHandle 51 | where 52 | T: 'static + Send + FnOnce() -> R, 53 | R: 'static + Send, 54 | { 55 | let (sender, recv) = channel(); 56 | self.0.spawn_blocking(Box::new(|| { 57 | if sender.send(task()).is_err() { 58 | // handler is dropped, do nothing. 59 | }; 60 | })); 61 | JoinHandle(recv) 62 | } 63 | } 64 | 65 | impl Future for JoinHandle { 66 | type Output = T; 67 | #[inline] 68 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 69 | let ready = futures::ready!(Pin::new(&mut self.0).poll(cx)); 70 | Poll::Ready(ready.expect("receiver in JoinHandle shouldn't be canceled")) 71 | } 72 | } 73 | 74 | impl rt::Executor for Executor 75 | where 76 | F: 'static + Send + Future, 77 | F::Output: 'static + Send, 78 | { 79 | #[inline] 80 | fn execute(&self, fut: F) { 81 | self.0.spawn(Box::pin(async move { 82 | let _ = fut.await; 83 | })); 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use std::sync::Arc; 90 | 91 | use super::{BlockingObj, Executor, FutureObj, Spawn}; 92 | 93 | pub struct Exec; 94 | 95 | impl Spawn for Exec { 96 | fn spawn(&self, fut: FutureObj) { 97 | tokio::task::spawn(fut); 98 | } 99 | 100 | fn spawn_blocking(&self, task: BlockingObj) { 101 | tokio::task::spawn_blocking(task); 102 | } 103 | } 104 | 105 | #[tokio::test] 106 | async fn spawn() { 107 | let exec = Executor(Arc::new(Exec)); 108 | assert_eq!(1, exec.spawn(async { 1 }).await); 109 | } 110 | 111 | #[tokio::test] 112 | async fn spawn_blocking() { 113 | let exec = Executor(Arc::new(Exec)); 114 | assert_eq!(1, exec.spawn_blocking(|| 1).await); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /roa-core/src/group.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{async_trait, Context, Endpoint, Middleware, Next, Result}; 4 | 5 | /// A set of method to chain middleware/endpoint to middleware 6 | /// or make middleware shared. 7 | pub trait MiddlewareExt: Sized + for<'a> Middleware<'a, S> { 8 | /// Chain two middlewares. 9 | fn chain(self, next: M) -> Chain 10 | where 11 | M: for<'a> Middleware<'a, S>, 12 | { 13 | Chain(self, next) 14 | } 15 | 16 | /// Chain an endpoint to a middleware. 17 | fn end(self, next: E) -> Chain 18 | where 19 | E: for<'a> Endpoint<'a, S>, 20 | { 21 | Chain(self, next) 22 | } 23 | 24 | /// Make middleware shared. 25 | fn shared(self) -> Shared 26 | where 27 | S: 'static, 28 | { 29 | Shared(Arc::new(self)) 30 | } 31 | } 32 | 33 | /// Extra methods of endpoint. 34 | pub trait EndpointExt: Sized + for<'a> Endpoint<'a, S> { 35 | /// Box an endpoint. 36 | fn boxed(self) -> Boxed 37 | where 38 | S: 'static, 39 | { 40 | Boxed(Box::new(self)) 41 | } 42 | } 43 | 44 | impl MiddlewareExt for T where T: for<'a> Middleware<'a, S> {} 45 | impl EndpointExt for T where T: for<'a> Endpoint<'a, S> {} 46 | 47 | /// A middleware composing and executing other middlewares in a stack-like manner. 48 | pub struct Chain(T, U); 49 | 50 | /// Shared middleware. 51 | pub struct Shared(Arc Middleware<'a, S>>); 52 | 53 | /// Boxed endpoint. 54 | pub struct Boxed(Box Endpoint<'a, S>>); 55 | 56 | #[async_trait(?Send)] 57 | impl<'a, S, T, U> Middleware<'a, S> for Chain 58 | where 59 | U: Middleware<'a, S>, 60 | T: for<'b> Middleware<'b, S>, 61 | { 62 | #[inline] 63 | async fn handle(&'a self, ctx: &'a mut Context, next: Next<'a>) -> Result { 64 | let ptr = ctx as *mut Context; 65 | let mut next = self.1.handle(unsafe { &mut *ptr }, next); 66 | self.0.handle(ctx, &mut next).await 67 | } 68 | } 69 | 70 | #[async_trait(?Send)] 71 | impl<'a, S> Middleware<'a, S> for Shared 72 | where 73 | S: 'static, 74 | { 75 | #[inline] 76 | async fn handle(&'a self, ctx: &'a mut Context, next: Next<'a>) -> Result { 77 | self.0.handle(ctx, next).await 78 | } 79 | } 80 | 81 | impl Clone for Shared { 82 | #[inline] 83 | fn clone(&self) -> Self { 84 | Self(self.0.clone()) 85 | } 86 | } 87 | 88 | #[async_trait(?Send)] 89 | impl<'a, S> Endpoint<'a, S> for Boxed 90 | where 91 | S: 'static, 92 | { 93 | #[inline] 94 | async fn call(&'a self, ctx: &'a mut Context) -> Result { 95 | self.0.call(ctx).await 96 | } 97 | } 98 | 99 | #[async_trait(?Send)] 100 | impl<'a, S, T, U> Endpoint<'a, S> for Chain 101 | where 102 | U: Endpoint<'a, S>, 103 | T: for<'b> Middleware<'b, S>, 104 | { 105 | #[inline] 106 | async fn call(&'a self, ctx: &'a mut Context) -> Result { 107 | let ptr = ctx as *mut Context; 108 | let mut next = self.1.call(unsafe { &mut *ptr }); 109 | self.0.handle(ctx, &mut next).await 110 | } 111 | } 112 | 113 | #[cfg(all(test, feature = "runtime"))] 114 | mod tests { 115 | use std::sync::Arc; 116 | 117 | use futures::lock::Mutex; 118 | use http::StatusCode; 119 | 120 | use crate::{async_trait, App, Context, Middleware, Next, Request, Status}; 121 | 122 | struct Pusher { 123 | data: usize, 124 | vector: Arc>>, 125 | } 126 | 127 | impl Pusher { 128 | fn new(data: usize, vector: Arc>>) -> Self { 129 | Self { data, vector } 130 | } 131 | } 132 | 133 | #[async_trait(?Send)] 134 | impl<'a> Middleware<'a, ()> for Pusher { 135 | async fn handle(&'a self, _ctx: &'a mut Context, next: Next<'a>) -> Result<(), Status> { 136 | self.vector.lock().await.push(self.data); 137 | next.await?; 138 | self.vector.lock().await.push(self.data); 139 | Ok(()) 140 | } 141 | } 142 | 143 | #[tokio::test] 144 | async fn middleware_order() -> Result<(), Box> { 145 | let vector = Arc::new(Mutex::new(Vec::new())); 146 | let service = App::new() 147 | .gate(Pusher::new(0, vector.clone())) 148 | .gate(Pusher::new(1, vector.clone())) 149 | .gate(Pusher::new(2, vector.clone())) 150 | .gate(Pusher::new(3, vector.clone())) 151 | .gate(Pusher::new(4, vector.clone())) 152 | .gate(Pusher::new(5, vector.clone())) 153 | .gate(Pusher::new(6, vector.clone())) 154 | .gate(Pusher::new(7, vector.clone())) 155 | .gate(Pusher::new(8, vector.clone())) 156 | .gate(Pusher::new(9, vector.clone())) 157 | .end(()) 158 | .http_service(); 159 | let resp = service.serve(Request::default()).await; 160 | assert_eq!(StatusCode::OK, resp.status); 161 | for i in 0..10 { 162 | assert_eq!(i, vector.lock().await[i]); 163 | assert_eq!(i, vector.lock().await[19 - i]); 164 | } 165 | Ok(()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /roa-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "docs", feature(doc_cfg))] 2 | #![cfg_attr(feature = "docs", doc = include_str!("../README.md"))] 3 | #![cfg_attr(feature = "docs", warn(missing_docs))] 4 | 5 | mod app; 6 | mod body; 7 | mod context; 8 | mod err; 9 | mod executor; 10 | mod group; 11 | mod middleware; 12 | mod request; 13 | mod response; 14 | mod state; 15 | 16 | #[doc(inline)] 17 | pub use app::{AddrStream, App}; 18 | pub use async_trait::async_trait; 19 | #[doc(inline)] 20 | pub use body::Body; 21 | #[doc(inline)] 22 | pub use context::{Context, Variable}; 23 | #[doc(inline)] 24 | pub use err::{Result, Status}; 25 | #[doc(inline)] 26 | pub use executor::{Executor, JoinHandle, Spawn}; 27 | #[doc(inline)] 28 | pub use group::{Boxed, Chain, EndpointExt, MiddlewareExt, Shared}; 29 | pub use http; 30 | pub use hyper::server::accept::Accept; 31 | pub use hyper::server::Server; 32 | #[doc(inline)] 33 | pub use middleware::{Endpoint, Middleware, Next}; 34 | #[doc(inline)] 35 | pub use request::Request; 36 | #[doc(inline)] 37 | pub use response::Response; 38 | #[doc(inline)] 39 | pub use state::State; 40 | -------------------------------------------------------------------------------- /roa-core/src/request.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use bytes::Bytes; 4 | use futures::stream::{Stream, TryStreamExt}; 5 | use http::{Extensions, HeaderMap, HeaderValue, Method, Uri, Version}; 6 | use hyper::Body; 7 | use tokio::io::AsyncRead; 8 | use tokio_util::io::StreamReader; 9 | /// Http request type of roa. 10 | pub struct Request { 11 | /// The request's method 12 | pub method: Method, 13 | 14 | /// The request's URI 15 | pub uri: Uri, 16 | 17 | /// The request's version 18 | pub version: Version, 19 | 20 | /// The request's headers 21 | pub headers: HeaderMap, 22 | 23 | extensions: Extensions, 24 | 25 | body: Body, 26 | } 27 | 28 | impl Request { 29 | /// Take raw hyper request. 30 | /// This method will consume inner body and extensions. 31 | #[inline] 32 | pub fn take_raw(&mut self) -> http::Request { 33 | let mut builder = http::Request::builder() 34 | .method(self.method.clone()) 35 | .uri(self.uri.clone()); 36 | *builder.extensions_mut().expect("fail to get extensions") = 37 | std::mem::take(&mut self.extensions); 38 | *builder.headers_mut().expect("fail to get headers") = self.headers.clone(); 39 | builder 40 | .body(self.raw_body()) 41 | .expect("fail to build raw body") 42 | } 43 | 44 | /// Gake raw hyper body. 45 | /// This method will consume inner body. 46 | #[inline] 47 | pub fn raw_body(&mut self) -> Body { 48 | std::mem::take(&mut self.body) 49 | } 50 | /// Get body as Stream. 51 | /// This method will consume inner body. 52 | #[inline] 53 | pub fn stream( 54 | &mut self, 55 | ) -> impl Stream> + Sync + Send + Unpin + 'static { 56 | self.raw_body() 57 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) 58 | } 59 | 60 | /// Get body as AsyncRead. 61 | /// This method will consume inner body. 62 | #[inline] 63 | pub fn reader(&mut self) -> impl AsyncRead + Sync + Send + Unpin + 'static { 64 | StreamReader::new(self.stream()) 65 | } 66 | } 67 | 68 | impl From> for Request { 69 | #[inline] 70 | fn from(req: http::Request) -> Self { 71 | let (parts, body) = req.into_parts(); 72 | Self { 73 | method: parts.method, 74 | uri: parts.uri, 75 | version: parts.version, 76 | headers: parts.headers, 77 | extensions: parts.extensions, 78 | body, 79 | } 80 | } 81 | } 82 | 83 | impl Default for Request { 84 | #[inline] 85 | fn default() -> Self { 86 | http::Request::new(Body::empty()).into() 87 | } 88 | } 89 | 90 | #[cfg(all(test, feature = "runtime"))] 91 | mod tests { 92 | use http::StatusCode; 93 | use hyper::Body; 94 | use tokio::io::AsyncReadExt; 95 | 96 | use crate::{App, Context, Request, Status}; 97 | 98 | async fn test(ctx: &mut Context) -> Result<(), Status> { 99 | let mut data = String::new(); 100 | ctx.req.reader().read_to_string(&mut data).await?; 101 | assert_eq!("Hello, World!", data); 102 | Ok(()) 103 | } 104 | 105 | #[tokio::test] 106 | async fn body_read() -> Result<(), Box> { 107 | let app = App::new().end(test); 108 | let service = app.http_service(); 109 | let req = Request::from(http::Request::new(Body::from("Hello, World!"))); 110 | let resp = service.serve(req).await; 111 | assert_eq!(StatusCode::OK, resp.status); 112 | Ok(()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /roa-core/src/response.rs: -------------------------------------------------------------------------------- 1 | //! A module for Response and its body 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use http::{HeaderMap, HeaderValue, StatusCode, Version}; 5 | 6 | pub use crate::Body; 7 | 8 | /// Http response type of roa. 9 | pub struct Response { 10 | /// Status code. 11 | pub status: StatusCode, 12 | 13 | /// Version of HTTP protocol. 14 | pub version: Version, 15 | 16 | /// Raw header map. 17 | pub headers: HeaderMap, 18 | 19 | /// Response body. 20 | pub body: Body, 21 | } 22 | 23 | impl Response { 24 | #[inline] 25 | pub(crate) fn new() -> Self { 26 | Self { 27 | status: StatusCode::default(), 28 | version: Version::default(), 29 | headers: HeaderMap::default(), 30 | body: Body::default(), 31 | } 32 | } 33 | 34 | #[inline] 35 | fn into_resp(self) -> http::Response { 36 | let (mut parts, _) = http::Response::new(()).into_parts(); 37 | let Response { 38 | status, 39 | version, 40 | headers, 41 | body, 42 | } = self; 43 | parts.status = status; 44 | parts.version = version; 45 | parts.headers = headers; 46 | http::Response::from_parts(parts, body.into()) 47 | } 48 | } 49 | 50 | impl Deref for Response { 51 | type Target = Body; 52 | #[inline] 53 | fn deref(&self) -> &Self::Target { 54 | &self.body 55 | } 56 | } 57 | 58 | impl DerefMut for Response { 59 | #[inline] 60 | fn deref_mut(&mut self) -> &mut Self::Target { 61 | &mut self.body 62 | } 63 | } 64 | 65 | impl From for http::Response { 66 | #[inline] 67 | fn from(value: Response) -> Self { 68 | value.into_resp() 69 | } 70 | } 71 | 72 | impl Default for Response { 73 | #[inline] 74 | fn default() -> Self { 75 | Self::new() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /roa-core/src/state.rs: -------------------------------------------------------------------------------- 1 | /// The `State` trait, should be replace with trait alias. 2 | /// The `App::state` will be cloned when a request inbounds. 3 | /// 4 | /// `State` is designed to share data or handler between middlewares. 5 | /// 6 | /// ### Example 7 | /// ```rust 8 | /// use roa_core::{App, Context, Next, Result}; 9 | /// use roa_core::http::StatusCode; 10 | /// 11 | /// #[derive(Clone)] 12 | /// struct State { 13 | /// id: u64, 14 | /// } 15 | /// 16 | /// let app = App::state(State { id: 0 }).gate(gate).end(end); 17 | /// async fn gate(ctx: &mut Context, next: Next<'_>) -> Result { 18 | /// ctx.id = 1; 19 | /// next.await 20 | /// } 21 | /// 22 | /// async fn end(ctx: &mut Context) -> Result { 23 | /// let id = ctx.id; 24 | /// assert_eq!(1, id); 25 | /// Ok(()) 26 | /// } 27 | /// ``` 28 | pub trait State: 'static + Clone + Send + Sync + Sized {} 29 | 30 | impl State for T {} 31 | -------------------------------------------------------------------------------- /roa-diesel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roa-diesel" 3 | version = "0.6.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | license = "MIT" 7 | readme = "./README.md" 8 | repository = "https://github.com/Hexilee/roa" 9 | documentation = "https://docs.rs/roa-diesel" 10 | homepage = "https://github.com/Hexilee/roa/wiki" 11 | description = "diesel integration with roa framework" 12 | keywords = ["http", "web", "framework", "orm"] 13 | categories = ["database"] 14 | 15 | [package.metadata.docs.rs] 16 | features = ["docs"] 17 | rustdoc-args = ["--cfg", "feature=\"docs\""] 18 | 19 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 20 | 21 | [dependencies] 22 | roa = { path = "../roa", version = "0.6.0", default-features = false } 23 | diesel = { version = "1.4", features = ["extras"] } 24 | r2d2 = "0.8" 25 | 26 | [dev-dependencies] 27 | diesel = { version = "1.4", features = ["extras", "sqlite"] } 28 | 29 | [features] 30 | docs = ["roa/docs"] -------------------------------------------------------------------------------- /roa-diesel/README.md: -------------------------------------------------------------------------------- 1 | [![Stable Test](https://github.com/Hexilee/roa/workflows/Stable%20Test/badge.svg)](https://github.com/Hexilee/roa/actions) 2 | [![codecov](https://codecov.io/gh/Hexilee/roa/branch/master/graph/badge.svg)](https://codecov.io/gh/Hexilee/roa) 3 | [![Rust Docs](https://docs.rs/roa-diesel/badge.svg)](https://docs.rs/roa-diesel) 4 | [![Crate version](https://img.shields.io/crates/v/roa-diesel.svg)](https://crates.io/crates/roa-diesel) 5 | [![Download](https://img.shields.io/crates/d/roa-diesel.svg)](https://crates.io/crates/roa-diesel) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Hexilee/roa/blob/master/LICENSE) 7 | 8 | This crate provides diesel integration with roa framework. 9 | 10 | ### AsyncPool 11 | A context extension to access r2d2 pool asynchronously. 12 | 13 | ```rust 14 | use roa::{Context, Result}; 15 | use diesel::sqlite::SqliteConnection; 16 | use roa_diesel::Pool; 17 | use roa_diesel::preload::*; 18 | use diesel::r2d2::ConnectionManager; 19 | 20 | #[derive(Clone)] 21 | struct State(Pool); 22 | 23 | impl AsRef> for State { 24 | fn as_ref(&self) -> &Pool { 25 | &self.0 26 | } 27 | } 28 | 29 | async fn get(ctx: Context) -> Result { 30 | let conn = ctx.get_conn().await?; 31 | // handle conn 32 | Ok(()) 33 | } 34 | ``` 35 | 36 | ### SqlQuery 37 | A context extension to execute diesel query asynchronously. 38 | 39 | Refer to [integration example](https://github.com/Hexilee/roa/tree/master/integration/diesel-example) 40 | for more use cases. 41 | -------------------------------------------------------------------------------- /roa-diesel/src/async_ext.rs: -------------------------------------------------------------------------------- 1 | use diesel::connection::Connection; 2 | use diesel::helper_types::Limit; 3 | use diesel::query_dsl::methods::{ExecuteDsl, LimitDsl, LoadQuery}; 4 | use diesel::query_dsl::RunQueryDsl; 5 | use diesel::result::{Error as DieselError, OptionalExtension}; 6 | use roa::{async_trait, Context, Result, State}; 7 | 8 | use crate::pool::{AsyncPool, Pool}; 9 | 10 | /// A context extension to execute diesel dsl asynchronously. 11 | #[async_trait] 12 | pub trait SqlQuery { 13 | /// Executes the given command, returning the number of rows affected. 14 | /// 15 | /// `execute` is usually used in conjunction with [`insert_into`](../fn.insert_into.html), 16 | /// [`update`](../fn.update.html) and [`delete`](../fn.delete.html) where the number of 17 | /// affected rows is often enough information. 18 | /// 19 | /// When asking the database to return data from a query, [`load`](#method.load) should 20 | /// probably be used instead. 21 | async fn execute(&self, exec: E) -> Result 22 | where 23 | E: 'static + Send + ExecuteDsl; 24 | /// Executes the given query, returning a `Vec` with the returned rows. 25 | /// 26 | /// When using the query builder, 27 | /// the return type can be 28 | /// a tuple of the values, 29 | /// or a struct which implements [`Queryable`]. 30 | /// 31 | /// When this method is called on [`sql_query`], 32 | /// the return type can only be a struct which implements [`QueryableByName`] 33 | /// 34 | /// For insert, update, and delete operations where only a count of affected is needed, 35 | /// [`execute`] should be used instead. 36 | /// 37 | /// [`Queryable`]: ../deserialize/trait.Queryable.html 38 | /// [`QueryableByName`]: ../deserialize/trait.QueryableByName.html 39 | /// [`execute`]: fn.execute.html 40 | /// [`sql_query`]: ../fn.sql_query.html 41 | /// 42 | async fn load_data(&self, query: Q) -> Result> 43 | where 44 | U: 'static + Send, 45 | Q: 'static + Send + LoadQuery; 46 | 47 | /// Runs the command, and returns the affected row. 48 | /// 49 | /// `Err(NotFound)` will be returned if the query affected 0 rows. You can 50 | /// call `.optional()` on the result of this if the command was optional to 51 | /// get back a `Result>` 52 | /// 53 | /// When this method is called on an insert, update, or delete statement, 54 | /// it will implicitly add a `RETURNING *` to the query, 55 | /// unless a returning clause was already specified. 56 | async fn get_result(&self, query: Q) -> Result> 57 | where 58 | U: 'static + Send, 59 | Q: 'static + Send + LoadQuery; 60 | 61 | /// Runs the command, returning an `Vec` with the affected rows. 62 | /// 63 | /// This method is an alias for [`load`], but with a name that makes more 64 | /// sense for insert, update, and delete statements. 65 | /// 66 | /// [`load`]: #method.load 67 | async fn get_results(&self, query: Q) -> Result> 68 | where 69 | U: 'static + Send, 70 | Q: 'static + Send + LoadQuery; 71 | 72 | /// Attempts to load a single record. 73 | /// 74 | /// This method is equivalent to `.limit(1).get_result()` 75 | /// 76 | /// Returns `Ok(record)` if found, and `Err(NotFound)` if no results are 77 | /// returned. If the query truly is optional, you can call `.optional()` on 78 | /// the result of this to get a `Result>`. 79 | /// 80 | async fn first(&self, query: Q) -> Result> 81 | where 82 | U: 'static + Send, 83 | Q: 'static + Send + LimitDsl, 84 | Limit: LoadQuery; 85 | } 86 | 87 | #[async_trait] 88 | impl SqlQuery for Context 89 | where 90 | S: State + AsRef>, 91 | Conn: 'static + Connection, 92 | { 93 | #[inline] 94 | async fn execute(&self, exec: E) -> Result 95 | where 96 | E: 'static + Send + ExecuteDsl, 97 | { 98 | let conn = self.get_conn().await?; 99 | Ok(self 100 | .exec 101 | .spawn_blocking(move || ExecuteDsl::::execute(exec, &*conn)) 102 | .await?) 103 | } 104 | 105 | /// Executes the given query, returning a `Vec` with the returned rows. 106 | /// 107 | /// When using the query builder, 108 | /// the return type can be 109 | /// a tuple of the values, 110 | /// or a struct which implements [`Queryable`]. 111 | /// 112 | /// When this method is called on [`sql_query`], 113 | /// the return type can only be a struct which implements [`QueryableByName`] 114 | /// 115 | /// For insert, update, and delete operations where only a count of affected is needed, 116 | /// [`execute`] should be used instead. 117 | /// 118 | /// [`Queryable`]: ../deserialize/trait.Queryable.html 119 | /// [`QueryableByName`]: ../deserialize/trait.QueryableByName.html 120 | /// [`execute`]: fn.execute.html 121 | /// [`sql_query`]: ../fn.sql_query.html 122 | /// 123 | #[inline] 124 | async fn load_data(&self, query: Q) -> Result> 125 | where 126 | U: 'static + Send, 127 | Q: 'static + Send + LoadQuery, 128 | { 129 | let conn = self.get_conn().await?; 130 | match self.exec.spawn_blocking(move || query.load(&*conn)).await { 131 | Ok(data) => Ok(data), 132 | Err(DieselError::NotFound) => Ok(Vec::new()), 133 | Err(err) => Err(err.into()), 134 | } 135 | } 136 | 137 | /// Runs the command, and returns the affected row. 138 | /// 139 | /// `Err(NotFound)` will be returned if the query affected 0 rows. You can 140 | /// call `.optional()` on the result of this if the command was optional to 141 | /// get back a `Result>` 142 | /// 143 | /// When this method is called on an insert, update, or delete statement, 144 | /// it will implicitly add a `RETURNING *` to the query, 145 | /// unless a returning clause was already specified. 146 | #[inline] 147 | async fn get_result(&self, query: Q) -> Result> 148 | where 149 | U: 'static + Send, 150 | Q: 'static + Send + LoadQuery, 151 | { 152 | let conn = self.get_conn().await?; 153 | Ok(self 154 | .exec 155 | .spawn_blocking(move || query.get_result(&*conn)) 156 | .await 157 | .optional()?) 158 | } 159 | 160 | /// Runs the command, returning an `Vec` with the affected rows. 161 | /// 162 | /// This method is an alias for [`load`], but with a name that makes more 163 | /// sense for insert, update, and delete statements. 164 | /// 165 | /// [`load`]: #method.load 166 | #[inline] 167 | async fn get_results(&self, query: Q) -> Result> 168 | where 169 | U: 'static + Send, 170 | Q: 'static + Send + LoadQuery, 171 | { 172 | self.load_data(query).await 173 | } 174 | 175 | /// Attempts to load a single record. 176 | /// 177 | /// This method is equivalent to `.limit(1).get_result()` 178 | /// 179 | /// Returns `Ok(record)` if found, and `Err(NotFound)` if no results are 180 | /// returned. If the query truly is optional, you can call `.optional()` on 181 | /// the result of this to get a `Result>`. 182 | /// 183 | #[inline] 184 | async fn first(&self, query: Q) -> Result> 185 | where 186 | U: 'static + Send, 187 | Q: 'static + Send + LimitDsl, 188 | Limit: LoadQuery, 189 | { 190 | let conn = self.get_conn().await?; 191 | Ok(self 192 | .exec 193 | .spawn_blocking(move || query.limit(1).get_result(&*conn)) 194 | .await 195 | .optional()?) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /roa-diesel/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "docs", doc = include_str!("../README.md"))] 2 | #![cfg_attr(feature = "docs", warn(missing_docs))] 3 | 4 | mod async_ext; 5 | mod pool; 6 | 7 | #[doc(inline)] 8 | pub use diesel::r2d2::ConnectionManager; 9 | #[doc(inline)] 10 | pub use pool::{builder, make_pool, Pool, WrapConnection}; 11 | 12 | /// preload ext traits. 13 | pub mod preload { 14 | #[doc(inline)] 15 | pub use crate::async_ext::SqlQuery; 16 | #[doc(inline)] 17 | pub use crate::pool::AsyncPool; 18 | } 19 | -------------------------------------------------------------------------------- /roa-diesel/src/pool.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use diesel::r2d2::{ConnectionManager, PoolError}; 4 | use diesel::Connection; 5 | use r2d2::{Builder, PooledConnection}; 6 | use roa::{async_trait, Context, State, Status}; 7 | 8 | /// An alias for r2d2::Pool>. 9 | pub type Pool = r2d2::Pool>; 10 | 11 | /// An alias for r2d2::PooledConnection>. 12 | pub type WrapConnection = PooledConnection>; 13 | 14 | /// Create a connection pool. 15 | /// 16 | /// ### Example 17 | /// 18 | /// ``` 19 | /// use roa_diesel::{make_pool, Pool}; 20 | /// use diesel::sqlite::SqliteConnection; 21 | /// use std::error::Error; 22 | /// 23 | /// # fn main() -> Result<(), Box> { 24 | /// let pool: Pool = make_pool(":memory:")?; 25 | /// Ok(()) 26 | /// # } 27 | /// ``` 28 | pub fn make_pool(url: impl Into) -> Result, PoolError> 29 | where 30 | Conn: Connection + 'static, 31 | { 32 | r2d2::Pool::new(ConnectionManager::::new(url)) 33 | } 34 | 35 | /// Create a pool builder. 36 | pub fn builder() -> Builder> 37 | where 38 | Conn: Connection + 'static, 39 | { 40 | r2d2::Pool::builder() 41 | } 42 | 43 | /// A context extension to access r2d2 pool asynchronously. 44 | #[async_trait] 45 | pub trait AsyncPool 46 | where 47 | Conn: Connection + 'static, 48 | { 49 | /// Retrieves a connection from the pool. 50 | /// 51 | /// Waits for at most the configured connection timeout before returning an 52 | /// error. 53 | /// 54 | /// ``` 55 | /// use roa::{Context, Result}; 56 | /// use diesel::sqlite::SqliteConnection; 57 | /// use roa_diesel::preload::AsyncPool; 58 | /// use roa_diesel::Pool; 59 | /// use diesel::r2d2::ConnectionManager; 60 | /// 61 | /// #[derive(Clone)] 62 | /// struct State(Pool); 63 | /// 64 | /// impl AsRef> for State { 65 | /// fn as_ref(&self) -> &Pool { 66 | /// &self.0 67 | /// } 68 | /// } 69 | /// 70 | /// async fn get(ctx: Context) -> Result { 71 | /// let conn = ctx.get_conn().await?; 72 | /// // handle conn 73 | /// Ok(()) 74 | /// } 75 | /// ``` 76 | async fn get_conn(&self) -> Result, Status>; 77 | 78 | /// Retrieves a connection from the pool, waiting for at most `timeout` 79 | /// 80 | /// The given timeout will be used instead of the configured connection 81 | /// timeout. 82 | async fn get_timeout(&self, timeout: Duration) -> Result, Status>; 83 | 84 | /// Returns information about the current state of the pool. 85 | async fn pool_state(&self) -> r2d2::State; 86 | } 87 | 88 | #[async_trait] 89 | impl AsyncPool for Context 90 | where 91 | S: State + AsRef>, 92 | Conn: Connection + 'static, 93 | { 94 | #[inline] 95 | async fn get_conn(&self) -> Result, Status> { 96 | let pool = self.as_ref().clone(); 97 | Ok(self.exec.spawn_blocking(move || pool.get()).await?) 98 | } 99 | 100 | #[inline] 101 | async fn get_timeout(&self, timeout: Duration) -> Result, Status> { 102 | let pool = self.as_ref().clone(); 103 | Ok(self 104 | .exec 105 | .spawn_blocking(move || pool.get_timeout(timeout)) 106 | .await?) 107 | } 108 | 109 | #[inline] 110 | async fn pool_state(&self) -> r2d2::State { 111 | let pool = self.as_ref().clone(); 112 | self.exec.spawn_blocking(move || pool.state()).await 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /roa-juniper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roa-juniper" 3 | version = "0.6.0" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | readme = "./README.md" 7 | repository = "https://github.com/Hexilee/roa" 8 | documentation = "https://docs.rs/roa-juniper" 9 | homepage = "https://github.com/Hexilee/roa/wiki" 10 | description = "juniper integration for roa" 11 | keywords = ["http", "web", "framework", "async"] 12 | categories = ["network-programming", "asynchronous", 13 | "web-programming::http-server"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | roa = { path = "../roa", version = "0.6.0", default-features = false, features = ["json"] } 19 | futures = "0.3" 20 | juniper = { version = "0.15", default-features = false } 21 | -------------------------------------------------------------------------------- /roa-juniper/README.md: -------------------------------------------------------------------------------- 1 | [![Stable Test](https://github.com/Hexilee/roa/workflows/Stable%20Test/badge.svg)](https://github.com/Hexilee/roa/actions) 2 | [![codecov](https://codecov.io/gh/Hexilee/roa/branch/master/graph/badge.svg)](https://codecov.io/gh/Hexilee/roa) 3 | [![Rust Docs](https://docs.rs/roa-juniper/badge.svg)](https://docs.rs/roa-juniper) 4 | [![Crate version](https://img.shields.io/crates/v/roa-juniper.svg)](https://crates.io/crates/roa-juniper) 5 | [![Download](https://img.shields.io/crates/d/roa-juniper.svg)](https://crates.io/crates/roa-juniper) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Hexilee/roa/blob/master/LICENSE) 7 | 8 | ## Roa-juniper 9 | 10 | This crate provides a juniper context and a graphql endpoint. 11 | 12 | ### Example 13 | 14 | Refer to [integration-example](https://github.com/Hexilee/roa/tree/master/integration/juniper-example). -------------------------------------------------------------------------------- /roa-juniper/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides a juniper context and a graphql endpoint. 2 | //! 3 | //! ### Example 4 | //! 5 | //! Refer to [integration-example](https://github.com/Hexilee/roa/tree/master/integration/juniper-example) 6 | 7 | #![warn(missing_docs)] 8 | 9 | use std::ops::{Deref, DerefMut}; 10 | 11 | use juniper::http::GraphQLRequest; 12 | use juniper::{GraphQLType, GraphQLTypeAsync, RootNode, ScalarValue}; 13 | use roa::preload::*; 14 | use roa::{async_trait, Context, Endpoint, Result, State}; 15 | 16 | /// A wrapper for `roa_core::SyncContext`. 17 | /// As an implementation of `juniper::Context`. 18 | pub struct JuniperContext(Context); 19 | 20 | impl juniper::Context for JuniperContext {} 21 | 22 | impl Deref for JuniperContext { 23 | type Target = Context; 24 | #[inline] 25 | fn deref(&self) -> &Self::Target { 26 | &self.0 27 | } 28 | } 29 | impl DerefMut for JuniperContext { 30 | #[inline] 31 | fn deref_mut(&mut self) -> &mut Self::Target { 32 | &mut self.0 33 | } 34 | } 35 | 36 | /// An endpoint. 37 | pub struct GraphQL( 38 | pub RootNode<'static, QueryT, MutationT, SubscriptionT, Sca>, 39 | ) 40 | where 41 | QueryT: GraphQLType, 42 | MutationT: GraphQLType, 43 | SubscriptionT: GraphQLType, 44 | Sca: ScalarValue; 45 | 46 | #[async_trait(?Send)] 47 | impl<'a, S, QueryT, MutationT, SubscriptionT, Sca> Endpoint<'a, S> 48 | for GraphQL 49 | where 50 | S: State, 51 | QueryT: GraphQLTypeAsync> + Send + Sync + 'static, 52 | QueryT::TypeInfo: Send + Sync, 53 | MutationT: GraphQLTypeAsync + Send + Sync + 'static, 54 | MutationT::TypeInfo: Send + Sync, 55 | SubscriptionT: GraphQLType + Send + Sync + 'static, 56 | SubscriptionT::TypeInfo: Send + Sync, 57 | Sca: ScalarValue + Send + Sync + 'static, 58 | { 59 | #[inline] 60 | async fn call(&'a self, ctx: &'a mut Context) -> Result { 61 | let request: GraphQLRequest = ctx.read_json().await?; 62 | let juniper_ctx = JuniperContext(ctx.clone()); 63 | let resp = request.execute(&self.0, &juniper_ctx).await; 64 | ctx.write_json(&resp) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /roa/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "roa" 3 | version = "0.6.1" 4 | authors = ["Hexilee "] 5 | edition = "2018" 6 | license = "MIT" 7 | readme = "./README.md" 8 | repository = "https://github.com/Hexilee/roa" 9 | documentation = "https://docs.rs/roa" 10 | homepage = "https://github.com/Hexilee/roa/wiki" 11 | description = """ 12 | async web framework inspired by koajs, lightweight but powerful. 13 | """ 14 | keywords = ["http", "web", "framework", "async"] 15 | categories = ["network-programming", "asynchronous", 16 | "web-programming::http-server"] 17 | 18 | [package.metadata.docs.rs] 19 | features = ["docs"] 20 | rustdoc-args = ["--cfg", "feature=\"docs\""] 21 | 22 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 23 | 24 | [badges] 25 | codecov = { repository = "Hexilee/roa" } 26 | 27 | [dependencies] 28 | tracing = { version = "0.1", features = ["log"] } 29 | futures = "0.3" 30 | bytesize = "1.0" 31 | async-trait = "0.1.51" 32 | url = "2.2" 33 | percent-encoding = "2.1" 34 | bytes = "1.1" 35 | headers = "0.3" 36 | tokio = "1.15" 37 | tokio-util = { version = "0.6.9", features = ["io"] } 38 | once_cell = "1.8" 39 | hyper = { version = "0.14", default-features = false, features = ["stream", "server", "http1", "http2"] } 40 | roa-core = { path = "../roa-core", version = "0.6" } 41 | 42 | cookie = { version = "0.15", features = ["percent-encode"], optional = true } 43 | jsonwebtoken = { version = "7.2", optional = true } 44 | serde = { version = "1", optional = true } 45 | serde_json = { version = "1.0", optional = true } 46 | async-compression = { version = "0.3.8", features = ["all-algorithms", "futures-io"], optional = true } 47 | 48 | # router 49 | radix_trie = { version = "0.2.1", optional = true } 50 | regex = { version = "1.5", optional = true } 51 | 52 | # body 53 | askama = { version = "0.10", optional = true } 54 | doc-comment = { version = "0.3.3", optional = true } 55 | serde_urlencoded = { version = "0.7", optional = true } 56 | mime_guess = { version = "2.0", optional = true } 57 | multer = { version = "2.0", optional = true } 58 | mime = { version = "0.3", optional = true } 59 | 60 | # websocket 61 | tokio-tungstenite = { version = "0.15.0", default-features = false, optional = true } 62 | 63 | 64 | # tls 65 | rustls = { version = "0.20", optional = true } 66 | tokio-rustls = { version = "0.23", optional = true } 67 | rustls-pemfile = { version = "0.2", optional = true } 68 | 69 | # jsonrpc 70 | jsonrpc-v2 = { version = "0.10", default-features = false, features = ["bytes-v10"], optional = true } 71 | 72 | [dev-dependencies] 73 | tokio = { version = "1.15", features = ["full"] } 74 | tokio-native-tls = "0.3" 75 | hyper-tls = "0.5" 76 | reqwest = { version = "0.11", features = ["json", "cookies", "gzip", "multipart"] } 77 | pretty_env_logger = "0.4" 78 | serde = { version = "1", features = ["derive"] } 79 | test-case = "1.2" 80 | slab = "0.4.5" 81 | multimap = "0.8" 82 | hyper = "0.14" 83 | mime = "0.3" 84 | encoding = "0.2" 85 | askama = "0.10" 86 | anyhow = "1.0" 87 | 88 | [features] 89 | default = ["async_rt"] 90 | full = [ 91 | "default", 92 | "json", 93 | "urlencoded", 94 | "file", 95 | "multipart", 96 | "template", 97 | "tls", 98 | "router", 99 | "jwt", 100 | "cookies", 101 | "compress", 102 | "websocket", 103 | "jsonrpc", 104 | ] 105 | 106 | docs = ["full", "roa-core/docs"] 107 | runtime = ["roa-core/runtime"] 108 | json = ["serde", "serde_json"] 109 | multipart = ["multer", "mime"] 110 | urlencoded = ["serde", "serde_urlencoded"] 111 | file = ["mime_guess", "tokio/fs"] 112 | template = ["askama"] 113 | tcp = ["tokio/net", "tokio/time"] 114 | tls = ["rustls", "tokio-rustls", "rustls-pemfile"] 115 | cookies = ["cookie"] 116 | jwt = ["jsonwebtoken", "serde", "serde_json"] 117 | router = ["radix_trie", "regex", "doc-comment"] 118 | websocket = ["tokio-tungstenite"] 119 | compress = ["async-compression"] 120 | async_rt = ["runtime", "tcp"] 121 | jsonrpc = ["jsonrpc-v2"] 122 | -------------------------------------------------------------------------------- /roa/src/body/file.rs: -------------------------------------------------------------------------------- 1 | mod content_disposition; 2 | mod help; 3 | 4 | use std::convert::TryInto; 5 | use std::path::Path; 6 | 7 | use content_disposition::ContentDisposition; 8 | pub use content_disposition::DispositionType; 9 | use tokio::fs::File; 10 | 11 | use crate::{http, Context, Result, State}; 12 | 13 | /// Write file to response body then set "Content-Type" and "Context-Disposition". 14 | #[inline] 15 | pub async fn write_file( 16 | ctx: &mut Context, 17 | path: impl AsRef, 18 | typ: DispositionType, 19 | ) -> Result { 20 | let path = path.as_ref(); 21 | ctx.resp.write_reader(File::open(path).await?); 22 | 23 | if let Some(filename) = path.file_name() { 24 | ctx.resp.headers.insert( 25 | http::header::CONTENT_TYPE, 26 | mime_guess::from_path(&filename) 27 | .first_or_octet_stream() 28 | .as_ref() 29 | .parse() 30 | .map_err(help::bug_report)?, 31 | ); 32 | 33 | let name = filename.to_string_lossy(); 34 | let content_disposition = ContentDisposition::new(typ, Some(&name)); 35 | ctx.resp.headers.insert( 36 | http::header::CONTENT_DISPOSITION, 37 | content_disposition.try_into()?, 38 | ); 39 | } 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /roa/src/body/file/content_disposition.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::fmt::{self, Display, Formatter}; 3 | 4 | use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; 5 | 6 | use super::help::bug_report; 7 | use crate::http::header::HeaderValue; 8 | use crate::Status; 9 | 10 | // This encode set is used for HTTP header values and is defined at 11 | // https://tools.ietf.org/html/rfc5987#section-3.2 12 | const HTTP_VALUE: &AsciiSet = &CONTROLS 13 | .add(b' ') 14 | .add(b'"') 15 | .add(b'%') 16 | .add(b'\'') 17 | .add(b'(') 18 | .add(b')') 19 | .add(b'*') 20 | .add(b',') 21 | .add(b'/') 22 | .add(b':') 23 | .add(b';') 24 | .add(b'<') 25 | .add(b'-') 26 | .add(b'>') 27 | .add(b'?') 28 | .add(b'[') 29 | .add(b'\\') 30 | .add(b']') 31 | .add(b'{') 32 | .add(b'}'); 33 | 34 | /// Type of content-disposition, inline or attachment 35 | #[derive(Clone, Debug, PartialEq)] 36 | pub enum DispositionType { 37 | /// Inline implies default processing 38 | Inline, 39 | /// Attachment implies that the recipient should prompt the user to save the response locally, 40 | /// rather than process it normally (as per its media type). 41 | Attachment, 42 | } 43 | 44 | /// A structure to generate value of "Content-Disposition" 45 | pub struct ContentDisposition { 46 | typ: DispositionType, 47 | encoded_filename: Option, 48 | } 49 | 50 | impl ContentDisposition { 51 | /// Construct by disposition type and optional filename. 52 | #[inline] 53 | pub(crate) fn new(typ: DispositionType, filename: Option<&str>) -> Self { 54 | Self { 55 | typ, 56 | encoded_filename: filename 57 | .map(|name| utf8_percent_encode(name, HTTP_VALUE).to_string()), 58 | } 59 | } 60 | } 61 | 62 | impl TryFrom for HeaderValue { 63 | type Error = Status; 64 | #[inline] 65 | fn try_from(value: ContentDisposition) -> Result { 66 | value 67 | .to_string() 68 | .try_into() 69 | .map_err(|err| bug_report(format!("{}\nNot a valid header value", err))) 70 | } 71 | } 72 | 73 | impl Display for ContentDisposition { 74 | #[inline] 75 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 76 | match &self.encoded_filename { 77 | None => f.write_fmt(format_args!("{}", self.typ)), 78 | Some(name) => f.write_fmt(format_args!( 79 | "{}; filename={}; filename*=UTF-8''{}", 80 | self.typ, name, name 81 | )), 82 | } 83 | } 84 | } 85 | 86 | impl Display for DispositionType { 87 | #[inline] 88 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 89 | match self { 90 | DispositionType::Inline => f.write_str("inline"), 91 | DispositionType::Attachment => f.write_str("attachment"), 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /roa/src/body/file/help.rs: -------------------------------------------------------------------------------- 1 | use crate::http::StatusCode; 2 | use crate::Status; 3 | 4 | const BUG_HELP: &str = 5 | r"This is a bug of roa::body::file, please report it to https://github.com/Hexilee/roa."; 6 | 7 | #[inline] 8 | pub fn bug_report(message: impl ToString) -> Status { 9 | Status::new( 10 | StatusCode::INTERNAL_SERVER_ERROR, 11 | format!("{}\n{}", message.to_string(), BUG_HELP), 12 | false, 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /roa/src/forward.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a context extension `Forward`, 2 | //! which is used to parse `X-Forwarded-*` headers. 3 | 4 | use std::net::IpAddr; 5 | 6 | use crate::http::header::HOST; 7 | use crate::{Context, State}; 8 | 9 | /// A context extension `Forward` used to parse `X-Forwarded-*` request headers. 10 | pub trait Forward { 11 | /// Get true host. 12 | /// - If "x-forwarded-host" is set and valid, use it. 13 | /// - Else if "host" is set and valid, use it. 14 | /// - Else throw Err(400 BAD REQUEST). 15 | /// 16 | /// ### Example 17 | /// ```rust 18 | /// use roa::{Context, Result}; 19 | /// use roa::forward::Forward; 20 | /// 21 | /// async fn get(ctx: &mut Context) -> Result { 22 | /// if let Some(host) = ctx.host() { 23 | /// println!("host: {}", host); 24 | /// } 25 | /// Ok(()) 26 | /// } 27 | /// ``` 28 | fn host(&self) -> Option<&str>; 29 | 30 | /// Get true client ip. 31 | /// - If "x-forwarded-for" is set and valid, use the first ip. 32 | /// - Else use the ip of `Context::remote_addr()`. 33 | /// 34 | /// ### Example 35 | /// ```rust 36 | /// use roa::{Context, Result}; 37 | /// use roa::forward::Forward; 38 | /// 39 | /// async fn get(ctx: &mut Context) -> Result { 40 | /// println!("client ip: {}", ctx.client_ip()); 41 | /// Ok(()) 42 | /// } 43 | /// ``` 44 | fn client_ip(&self) -> IpAddr; 45 | 46 | /// Get true forwarded ips. 47 | /// - If "x-forwarded-for" is set and valid, use it. 48 | /// - Else return an empty vector. 49 | /// 50 | /// ### Example 51 | /// ```rust 52 | /// use roa::{Context, Result}; 53 | /// use roa::forward::Forward; 54 | /// 55 | /// async fn get(ctx: &mut Context) -> Result { 56 | /// println!("forwarded ips: {:?}", ctx.forwarded_ips()); 57 | /// Ok(()) 58 | /// } 59 | /// ``` 60 | fn forwarded_ips(&self) -> Vec; 61 | 62 | /// Try to get forwarded proto. 63 | /// - If "x-forwarded-proto" is not set, return None. 64 | /// - If "x-forwarded-proto" is set but fails to string, return Some(Err(400 BAD REQUEST)). 65 | /// 66 | /// ### Example 67 | /// ```rust 68 | /// use roa::{Context, Result}; 69 | /// use roa::forward::Forward; 70 | /// 71 | /// async fn get(ctx: &mut Context) -> Result { 72 | /// if let Some(proto) = ctx.forwarded_proto() { 73 | /// println!("forwarded proto: {}", proto); 74 | /// } 75 | /// Ok(()) 76 | /// } 77 | /// ``` 78 | fn forwarded_proto(&self) -> Option<&str>; 79 | } 80 | 81 | impl Forward for Context { 82 | #[inline] 83 | fn host(&self) -> Option<&str> { 84 | self.get("x-forwarded-host").or_else(|| self.get(HOST)) 85 | } 86 | 87 | #[inline] 88 | fn client_ip(&self) -> IpAddr { 89 | let addrs = self.forwarded_ips(); 90 | if addrs.is_empty() { 91 | self.remote_addr.ip() 92 | } else { 93 | addrs[0] 94 | } 95 | } 96 | 97 | #[inline] 98 | fn forwarded_ips(&self) -> Vec { 99 | let mut addrs = Vec::new(); 100 | if let Some(value) = self.get("x-forwarded-for") { 101 | for addr_str in value.split(',') { 102 | if let Ok(addr) = addr_str.trim().parse() { 103 | addrs.push(addr) 104 | } 105 | } 106 | } 107 | addrs 108 | } 109 | 110 | #[inline] 111 | fn forwarded_proto(&self) -> Option<&str> { 112 | self.get("x-forwarded-proto") 113 | } 114 | } 115 | 116 | #[cfg(all(test, feature = "tcp"))] 117 | mod tests { 118 | use tokio::task::spawn; 119 | 120 | use super::Forward; 121 | use crate::http::header::HOST; 122 | use crate::http::{HeaderValue, StatusCode}; 123 | use crate::preload::*; 124 | use crate::{App, Context}; 125 | 126 | #[tokio::test] 127 | async fn host() -> Result<(), Box> { 128 | async fn test(ctx: &mut Context) -> crate::Result { 129 | assert_eq!(Some("github.com"), ctx.host()); 130 | Ok(()) 131 | } 132 | let (addr, server) = App::new().end(test).run()?; 133 | spawn(server); 134 | let client = reqwest::Client::new(); 135 | let resp = client 136 | .get(&format!("http://{}", addr)) 137 | .header(HOST, HeaderValue::from_static("github.com")) 138 | .send() 139 | .await?; 140 | assert_eq!(StatusCode::OK, resp.status()); 141 | 142 | let resp = client 143 | .get(&format!("http://{}", addr)) 144 | .header(HOST, "google.com") 145 | .header("x-forwarded-host", "github.com") 146 | .send() 147 | .await?; 148 | assert_eq!(StatusCode::OK, resp.status()); 149 | Ok(()) 150 | } 151 | 152 | #[tokio::test] 153 | async fn host_err() -> Result<(), Box> { 154 | async fn test(ctx: &mut Context) -> crate::Result { 155 | ctx.req.headers.remove(HOST); 156 | assert_eq!(None, ctx.host()); 157 | Ok(()) 158 | } 159 | let (addr, server) = App::new().end(test).run()?; 160 | spawn(server); 161 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 162 | assert_eq!(StatusCode::OK, resp.status()); 163 | Ok(()) 164 | } 165 | 166 | #[tokio::test] 167 | async fn client_ip() -> Result<(), Box> { 168 | async fn remote_addr(ctx: &mut Context) -> crate::Result { 169 | assert_eq!(ctx.remote_addr.ip(), ctx.client_ip()); 170 | Ok(()) 171 | } 172 | let (addr, server) = App::new().end(remote_addr).run()?; 173 | spawn(server); 174 | reqwest::get(&format!("http://{}", addr)).await?; 175 | 176 | async fn forward_addr(ctx: &mut Context) -> crate::Result { 177 | assert_eq!("192.168.0.1", ctx.client_ip().to_string()); 178 | Ok(()) 179 | } 180 | let (addr, server) = App::new().end(forward_addr).run()?; 181 | spawn(server); 182 | let client = reqwest::Client::new(); 183 | client 184 | .get(&format!("http://{}", addr)) 185 | .header("x-forwarded-for", "192.168.0.1, 8.8.8.8") 186 | .send() 187 | .await?; 188 | 189 | Ok(()) 190 | } 191 | 192 | #[tokio::test] 193 | async fn forwarded_proto() -> Result<(), Box> { 194 | async fn test(ctx: &mut Context) -> crate::Result { 195 | assert_eq!(Some("https"), ctx.forwarded_proto()); 196 | Ok(()) 197 | } 198 | let (addr, server) = App::new().end(test).run()?; 199 | spawn(server); 200 | let client = reqwest::Client::new(); 201 | client 202 | .get(&format!("http://{}", addr)) 203 | .header("x-forwarded-proto", "https") 204 | .send() 205 | .await?; 206 | 207 | Ok(()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /roa/src/jsonrpc.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! ## roa::jsonrpc 3 | //! 4 | //! This module provides a json rpc endpoint. 5 | //! 6 | //! ### Example 7 | //! 8 | //! ```rust,no_run 9 | //! use roa::App; 10 | //! use roa::jsonrpc::{RpcEndpoint, Data, Error, Params, Server}; 11 | //! use roa::tcp::Listener; 12 | //! use tracing::info; 13 | //! 14 | //! #[derive(serde::Deserialize)] 15 | //! struct TwoNums { 16 | //! a: usize, 17 | //! b: usize, 18 | //! } 19 | //! 20 | //! async fn add(Params(params): Params) -> Result { 21 | //! Ok(params.a + params.b) 22 | //! } 23 | //! 24 | //! async fn sub(Params(params): Params<(usize, usize)>) -> Result { 25 | //! Ok(params.0 - params.1) 26 | //! } 27 | //! 28 | //! async fn message(data: Data) -> Result { 29 | //! Ok(String::from(&*data)) 30 | //! } 31 | //! 32 | //! #[tokio::main] 33 | //! async fn main() -> anyhow::Result<()> { 34 | //! let rpc = Server::new() 35 | //! .with_data(Data::new(String::from("Hello!"))) 36 | //! .with_method("sub", sub) 37 | //! .with_method("message", message) 38 | //! .finish_unwrapped(); 39 | //! 40 | //! let app = App::new().end(RpcEndpoint(rpc)); 41 | //! app.listen("127.0.0.1:8000", |addr| { 42 | //! info!("Server is listening on {}", addr) 43 | //! })? 44 | //! .await?; 45 | //! Ok(()) 46 | //! } 47 | //! ``` 48 | 49 | use bytes::Bytes; 50 | #[doc(no_inline)] 51 | pub use jsonrpc_v2::*; 52 | 53 | use crate::body::PowerBody; 54 | use crate::{async_trait, Context, Endpoint, Result, State}; 55 | 56 | /// A wrapper for [`jsonrpc_v2::Server`], implemented [`roa::Endpoint`]. 57 | /// 58 | /// [`jsonrpc_v2::Server`]: https://docs.rs/jsonrpc-v2/0.10.1/jsonrpc_v2/struct.Server.html 59 | /// [`roa::Endpoint`]: https://docs.rs/roa/0.6.0/roa/trait.Endpoint.html 60 | pub struct RpcEndpoint(pub Server); 61 | 62 | #[async_trait(? Send)] 63 | impl<'a, S, R> Endpoint<'a, S> for RpcEndpoint 64 | where 65 | S: State, 66 | R: Router + Sync + Send + 'static, 67 | { 68 | #[inline] 69 | async fn call(&'a self, ctx: &'a mut Context) -> Result { 70 | let data = ctx.read().await?; 71 | let resp = self.0.handle(Bytes::from(data)).await; 72 | ctx.write_json(&resp) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /roa/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "docs", feature(doc_cfg))] 2 | #![cfg_attr(feature = "docs", doc = include_str!("../README.md"))] 3 | #![cfg_attr(feature = "docs", warn(missing_docs))] 4 | 5 | pub use roa_core::*; 6 | 7 | #[cfg(feature = "router")] 8 | #[cfg_attr(feature = "docs", doc(cfg(feature = "router")))] 9 | pub mod router; 10 | 11 | #[cfg(feature = "tcp")] 12 | #[cfg_attr(feature = "docs", doc(cfg(feature = "tcp")))] 13 | pub mod tcp; 14 | 15 | #[cfg(feature = "tls")] 16 | #[cfg_attr(feature = "docs", doc(cfg(feature = "tls")))] 17 | pub mod tls; 18 | 19 | #[cfg(feature = "websocket")] 20 | #[cfg_attr(feature = "docs", doc(cfg(feature = "websocket")))] 21 | pub mod websocket; 22 | 23 | #[cfg(feature = "cookies")] 24 | #[cfg_attr(feature = "docs", doc(cfg(feature = "cookies")))] 25 | pub mod cookie; 26 | 27 | #[cfg(feature = "jwt")] 28 | #[cfg_attr(feature = "docs", doc(cfg(feature = "jwt")))] 29 | pub mod jwt; 30 | 31 | #[cfg(feature = "compress")] 32 | #[cfg_attr(feature = "docs", doc(cfg(feature = "compress")))] 33 | pub mod compress; 34 | 35 | #[cfg(feature = "jsonrpc")] 36 | #[cfg_attr(feature = "docs", doc(cfg(feature = "jsonrpc")))] 37 | pub mod jsonrpc; 38 | 39 | pub mod body; 40 | pub mod cors; 41 | pub mod forward; 42 | pub mod logger; 43 | pub mod query; 44 | pub mod stream; 45 | 46 | /// Reexport all extension traits. 47 | pub mod preload { 48 | pub use crate::body::PowerBody; 49 | #[cfg(feature = "cookies")] 50 | pub use crate::cookie::{CookieGetter, CookieSetter}; 51 | pub use crate::forward::Forward; 52 | #[cfg(feature = "jwt")] 53 | pub use crate::jwt::JwtVerifier; 54 | pub use crate::query::Query; 55 | #[cfg(feature = "router")] 56 | pub use crate::router::RouterParam; 57 | #[cfg(feature = "tcp")] 58 | #[doc(no_inline)] 59 | pub use crate::tcp::Listener; 60 | #[cfg(all(feature = "tcp", feature = "tls"))] 61 | #[doc(no_inline)] 62 | pub use crate::tls::TlsListener; 63 | } 64 | -------------------------------------------------------------------------------- /roa/src/logger.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a middleware `logger`. 2 | //! 3 | //! ### Example 4 | //! 5 | //! ```rust 6 | //! use roa::logger::logger; 7 | //! use roa::preload::*; 8 | //! use roa::App; 9 | //! use roa::http::StatusCode; 10 | //! use tokio::task::spawn; 11 | //! 12 | //! #[tokio::main] 13 | //! async fn main() -> Result<(), Box> { 14 | //! pretty_env_logger::init(); 15 | //! let app = App::new() 16 | //! .gate(logger) 17 | //! .end("Hello, World"); 18 | //! let (addr, server) = app.run()?; 19 | //! spawn(server); 20 | //! let resp = reqwest::get(&format!("http://{}", addr)).await?; 21 | //! assert_eq!(StatusCode::OK, resp.status()); 22 | //! Ok(()) 23 | //! } 24 | //! ``` 25 | 26 | use std::pin::Pin; 27 | use std::time::Instant; 28 | use std::{io, mem}; 29 | 30 | use bytes::Bytes; 31 | use bytesize::ByteSize; 32 | use futures::task::{self, Poll}; 33 | use futures::{Future, Stream}; 34 | use roa_core::http::{Method, StatusCode}; 35 | use tracing::{error, info}; 36 | 37 | use crate::http::Uri; 38 | use crate::{Context, Executor, JoinHandle, Next, Result}; 39 | 40 | /// A finite-state machine to log success information in each successful response. 41 | enum StreamLogger { 42 | /// Polling state, as a body stream. 43 | Polling { stream: S, task: LogTask }, 44 | 45 | /// Logging state, as a logger future. 46 | Logging(JoinHandle<()>), 47 | 48 | /// Complete, as a empty stream. 49 | Complete, 50 | } 51 | 52 | /// A task structure to log when polling is complete. 53 | #[derive(Clone)] 54 | struct LogTask { 55 | counter: u64, 56 | method: Method, 57 | status_code: StatusCode, 58 | uri: Uri, 59 | start: Instant, 60 | exec: Executor, 61 | } 62 | 63 | impl LogTask { 64 | #[inline] 65 | fn log(&self) -> JoinHandle<()> { 66 | let LogTask { 67 | counter, 68 | method, 69 | status_code, 70 | uri, 71 | start, 72 | exec, 73 | } = self.clone(); 74 | exec.spawn_blocking(move || { 75 | info!( 76 | "<-- {} {} {}ms {} {}", 77 | method, 78 | uri, 79 | start.elapsed().as_millis(), 80 | ByteSize(counter), 81 | status_code, 82 | ) 83 | }) 84 | } 85 | } 86 | 87 | impl Stream for StreamLogger 88 | where 89 | S: 'static + Send + Send + Unpin + Stream>, 90 | { 91 | type Item = io::Result; 92 | 93 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 94 | match &mut *self { 95 | StreamLogger::Polling { stream, task } => { 96 | match futures::ready!(Pin::new(stream).poll_next(cx)) { 97 | Some(Ok(bytes)) => { 98 | task.counter += bytes.len() as u64; 99 | Poll::Ready(Some(Ok(bytes))) 100 | } 101 | None => { 102 | let handler = task.log(); 103 | *self = StreamLogger::Logging(handler); 104 | self.poll_next(cx) 105 | } 106 | err => Poll::Ready(err), 107 | } 108 | } 109 | 110 | StreamLogger::Logging(handler) => { 111 | futures::ready!(Pin::new(handler).poll(cx)); 112 | *self = StreamLogger::Complete; 113 | self.poll_next(cx) 114 | } 115 | 116 | StreamLogger::Complete => Poll::Ready(None), 117 | } 118 | } 119 | } 120 | 121 | /// A middleware to log information about request and response. 122 | /// 123 | /// Based on crate `log`, the log level must be greater than `INFO` to log all information, 124 | /// and should be greater than `ERROR` when you need error information only. 125 | pub async fn logger(ctx: &mut Context, next: Next<'_>) -> Result { 126 | info!("--> {} {}", ctx.method(), ctx.uri().path()); 127 | let start = Instant::now(); 128 | let mut result = next.await; 129 | 130 | let method = ctx.method().clone(); 131 | let uri = ctx.uri().clone(); 132 | let exec = ctx.exec.clone(); 133 | 134 | match &mut result { 135 | Err(status) => { 136 | let status_code = status.status_code; 137 | let message = if status.expose { 138 | status.message.clone() 139 | } else { 140 | // set expose to true; then root status_handler won't log this status. 141 | status.expose = true; 142 | 143 | // take unexposed message 144 | mem::take(&mut status.message) 145 | }; 146 | ctx.exec 147 | .spawn_blocking(move || { 148 | error!("<-- {} {} {}\n{}", method, uri, status_code, message,); 149 | }) 150 | .await 151 | } 152 | Ok(_) => { 153 | let status_code = ctx.status(); 154 | // logging when body polling complete. 155 | let logger = StreamLogger::Polling { 156 | stream: mem::take(&mut ctx.resp.body), 157 | task: LogTask { 158 | counter: 0, 159 | method, 160 | uri, 161 | status_code, 162 | start, 163 | exec, 164 | }, 165 | }; 166 | ctx.resp.write_stream(logger); 167 | } 168 | } 169 | result 170 | } 171 | -------------------------------------------------------------------------------- /roa/src/router/endpoints.rs: -------------------------------------------------------------------------------- 1 | mod dispatcher; 2 | mod guard; 3 | 4 | use crate::http::{Method, StatusCode}; 5 | use crate::{throw, Result}; 6 | 7 | #[inline] 8 | fn method_not_allowed(method: &Method) -> Result { 9 | throw!( 10 | StatusCode::METHOD_NOT_ALLOWED, 11 | format!("Method {} not allowed", method) 12 | ) 13 | } 14 | 15 | pub use dispatcher::{connect, delete, get, head, options, patch, post, put, trace, Dispatcher}; 16 | pub use guard::{allow, deny, Guard}; 17 | -------------------------------------------------------------------------------- /roa/src/router/endpoints/dispatcher.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use doc_comment::doc_comment; 4 | 5 | use super::method_not_allowed; 6 | use crate::http::Method; 7 | use crate::{async_trait, Context, Endpoint, Result}; 8 | 9 | macro_rules! impl_http_methods { 10 | ($end:ident, $method:expr) => { 11 | doc_comment! { 12 | concat!("Method to add or override endpoint on ", stringify!($method), ". 13 | 14 | You can use it as follow: 15 | 16 | ```rust 17 | use roa::{App, Context, Result}; 18 | use roa::router::get; 19 | 20 | async fn foo(ctx: &mut Context) -> Result { 21 | Ok(()) 22 | } 23 | 24 | async fn bar(ctx: &mut Context) -> Result { 25 | Ok(()) 26 | } 27 | 28 | let app = App::new().end(get(foo).", stringify!($end), "(bar)); 29 | ```"), 30 | pub fn $end(mut self, endpoint: impl for<'a> Endpoint<'a, S>) -> Self { 31 | self.0.insert($method, Box::new(endpoint)); 32 | self 33 | } 34 | } 35 | }; 36 | } 37 | 38 | macro_rules! impl_http_functions { 39 | ($end:ident, $method:expr) => { 40 | doc_comment! { 41 | concat!("Function to construct dispatcher with ", stringify!($method), " and an endpoint. 42 | 43 | You can use it as follow: 44 | 45 | ```rust 46 | use roa::{App, Context, Result}; 47 | use roa::router::", stringify!($end), "; 48 | 49 | async fn end(ctx: &mut Context) -> Result { 50 | Ok(()) 51 | } 52 | 53 | let app = App::new().end(", stringify!($end), "(end)); 54 | ```"), 55 | pub fn $end(endpoint: impl for<'a> Endpoint<'a, S>) -> Dispatcher { 56 | Dispatcher::::default().$end(endpoint) 57 | } 58 | } 59 | }; 60 | } 61 | 62 | /// An endpoint wrapper to dispatch requests by http method. 63 | pub struct Dispatcher(HashMap Endpoint<'a, S>>>); 64 | 65 | impl_http_functions!(get, Method::GET); 66 | impl_http_functions!(post, Method::POST); 67 | impl_http_functions!(put, Method::PUT); 68 | impl_http_functions!(patch, Method::PATCH); 69 | impl_http_functions!(options, Method::OPTIONS); 70 | impl_http_functions!(delete, Method::DELETE); 71 | impl_http_functions!(head, Method::HEAD); 72 | impl_http_functions!(trace, Method::TRACE); 73 | impl_http_functions!(connect, Method::CONNECT); 74 | 75 | impl Dispatcher { 76 | impl_http_methods!(get, Method::GET); 77 | impl_http_methods!(post, Method::POST); 78 | impl_http_methods!(put, Method::PUT); 79 | impl_http_methods!(patch, Method::PATCH); 80 | impl_http_methods!(options, Method::OPTIONS); 81 | impl_http_methods!(delete, Method::DELETE); 82 | impl_http_methods!(head, Method::HEAD); 83 | impl_http_methods!(trace, Method::TRACE); 84 | impl_http_methods!(connect, Method::CONNECT); 85 | } 86 | 87 | /// Empty dispatcher. 88 | impl Default for Dispatcher { 89 | fn default() -> Self { 90 | Self(HashMap::new()) 91 | } 92 | } 93 | 94 | #[async_trait(?Send)] 95 | impl<'a, S> Endpoint<'a, S> for Dispatcher 96 | where 97 | S: 'static, 98 | { 99 | #[inline] 100 | async fn call(&'a self, ctx: &'a mut Context) -> Result<()> { 101 | match self.0.get(ctx.method()) { 102 | Some(endpoint) => endpoint.call(ctx).await, 103 | None => method_not_allowed(ctx.method()), 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /roa/src/router/endpoints/guard.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::iter::FromIterator; 3 | 4 | use super::method_not_allowed; 5 | use crate::http::Method; 6 | use crate::{async_trait, Context, Endpoint, Result}; 7 | 8 | /// Methods allowed in `Guard`. 9 | const ALL_METHODS: [Method; 9] = [ 10 | Method::GET, 11 | Method::POST, 12 | Method::PUT, 13 | Method::PATCH, 14 | Method::OPTIONS, 15 | Method::DELETE, 16 | Method::HEAD, 17 | Method::TRACE, 18 | Method::CONNECT, 19 | ]; 20 | 21 | /// An endpoint wrapper to guard endpoint by http method. 22 | pub struct Guard { 23 | white_list: HashSet, 24 | endpoint: E, 25 | } 26 | 27 | /// Initialize hash set. 28 | fn hash_set(methods: impl AsRef<[Method]>) -> HashSet { 29 | HashSet::from_iter(methods.as_ref().to_vec()) 30 | } 31 | 32 | /// A function to construct guard by white list. 33 | /// 34 | /// Only requests with http method in list can access this endpoint, otherwise will get a 405 METHOD NOT ALLOWED. 35 | /// 36 | /// ``` 37 | /// use roa::{App, Context, Result}; 38 | /// use roa::http::Method; 39 | /// use roa::router::allow; 40 | /// 41 | /// async fn foo(ctx: &mut Context) -> Result { 42 | /// Ok(()) 43 | /// } 44 | /// 45 | /// let app = App::new().end(allow([Method::GET, Method::POST], foo)); 46 | /// ``` 47 | pub fn allow(methods: impl AsRef<[Method]>, endpoint: E) -> Guard { 48 | Guard { 49 | endpoint, 50 | white_list: hash_set(methods), 51 | } 52 | } 53 | 54 | /// A function to construct guard by black list. 55 | /// 56 | /// Only requests with http method not in list can access this endpoint, otherwise will get a 405 METHOD NOT ALLOWED. 57 | /// 58 | /// ``` 59 | /// use roa::{App, Context, Result}; 60 | /// use roa::http::Method; 61 | /// use roa::router::deny; 62 | /// 63 | /// async fn foo(ctx: &mut Context) -> Result { 64 | /// Ok(()) 65 | /// } 66 | /// 67 | /// let app = App::new().end(deny([Method::PUT, Method::DELETE], foo)); 68 | /// ``` 69 | pub fn deny(methods: impl AsRef<[Method]>, endpoint: E) -> Guard { 70 | let white_list = hash_set(ALL_METHODS); 71 | let black_list = &white_list & &hash_set(methods); 72 | Guard { 73 | endpoint, 74 | white_list: &white_list ^ &black_list, 75 | } 76 | } 77 | 78 | #[async_trait(?Send)] 79 | impl<'a, S, E> Endpoint<'a, S> for Guard 80 | where 81 | E: Endpoint<'a, S>, 82 | { 83 | #[inline] 84 | async fn call(&'a self, ctx: &'a mut Context) -> Result { 85 | if self.white_list.contains(ctx.method()) { 86 | self.endpoint.call(ctx).await 87 | } else { 88 | method_not_allowed(ctx.method()) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /roa/src/router/err.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | use roa_core::http; 4 | 5 | /// Error occurring in building route table. 6 | #[derive(Debug)] 7 | pub enum RouterError { 8 | /// Dynamic paths miss variable. 9 | MissingVariable(String), 10 | 11 | /// Variables, methods or paths conflict. 12 | Conflict(Conflict), 13 | } 14 | 15 | /// Router conflict. 16 | #[derive(Debug, Eq, PartialEq)] 17 | pub enum Conflict { 18 | Path(String), 19 | Method(String, http::Method), 20 | Variable { 21 | paths: (String, String), 22 | var_name: String, 23 | }, 24 | } 25 | 26 | impl Display for Conflict { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { 28 | match self { 29 | Conflict::Path(path) => f.write_str(&format!("conflict path: `{}`", path)), 30 | Conflict::Method(path, method) => f.write_str(&format!( 31 | "conflict method: `{}` on `{}` is already set", 32 | method, path 33 | )), 34 | Conflict::Variable { paths, var_name } => f.write_str(&format!( 35 | "conflict variable `{}`: between `{}` and `{}`", 36 | var_name, paths.0, paths.1 37 | )), 38 | } 39 | } 40 | } 41 | 42 | impl Display for RouterError { 43 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { 44 | match self { 45 | RouterError::Conflict(conflict) => f.write_str(&format!("Conflict! {}", conflict)), 46 | RouterError::MissingVariable(path) => { 47 | f.write_str(&format!("missing variable on path {}", path)) 48 | } 49 | } 50 | } 51 | } 52 | 53 | impl From for RouterError { 54 | fn from(conflict: Conflict) -> Self { 55 | RouterError::Conflict(conflict) 56 | } 57 | } 58 | 59 | impl std::error::Error for Conflict {} 60 | impl std::error::Error for RouterError {} 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use roa_core::http; 65 | 66 | use super::{Conflict, RouterError}; 67 | 68 | #[test] 69 | fn conflict_to_string() { 70 | assert_eq!( 71 | "conflict path: `/`", 72 | Conflict::Path("/".to_string()).to_string() 73 | ); 74 | assert_eq!( 75 | "conflict method: `GET` on `/` is already set", 76 | Conflict::Method("/".to_string(), http::Method::GET).to_string() 77 | ); 78 | assert_eq!( 79 | "conflict variable `id`: between `/:id` and `/user/:id`", 80 | Conflict::Variable { 81 | paths: ("/:id".to_string(), "/user/:id".to_string()), 82 | var_name: "id".to_string() 83 | } 84 | .to_string() 85 | ); 86 | } 87 | 88 | #[test] 89 | fn err_to_string() { 90 | assert_eq!( 91 | "Conflict! conflict path: `/`", 92 | RouterError::Conflict(Conflict::Path("/".to_string())).to_string() 93 | ); 94 | assert_eq!( 95 | "missing variable on path /:", 96 | RouterError::MissingVariable("/:".to_string()).to_string() 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /roa/src/stream.rs: -------------------------------------------------------------------------------- 1 | //! this module provides a stream adaptor `AsyncStream` 2 | 3 | use std::io; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use futures::io::{AsyncRead as Read, AsyncWrite as Write}; 8 | use futures::ready; 9 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 10 | use tracing::{instrument, trace}; 11 | 12 | /// A adaptor between futures::io::{AsyncRead, AsyncWrite} and tokio::io::{AsyncRead, AsyncWrite}. 13 | pub struct AsyncStream(pub IO); 14 | 15 | impl AsyncRead for AsyncStream 16 | where 17 | IO: Unpin + Read, 18 | { 19 | #[inline] 20 | fn poll_read( 21 | mut self: Pin<&mut Self>, 22 | cx: &mut Context<'_>, 23 | buf: &mut ReadBuf<'_>, 24 | ) -> Poll> { 25 | let read_size = ready!(Pin::new(&mut self.0).poll_read(cx, buf.initialize_unfilled()))?; 26 | buf.advance(read_size); 27 | Poll::Ready(Ok(())) 28 | } 29 | } 30 | 31 | impl AsyncWrite for AsyncStream 32 | where 33 | IO: Unpin + Write, 34 | { 35 | #[inline] 36 | fn poll_write( 37 | mut self: Pin<&mut Self>, 38 | cx: &mut Context<'_>, 39 | buf: &[u8], 40 | ) -> Poll> { 41 | Pin::new(&mut self.0).poll_write(cx, buf) 42 | } 43 | 44 | #[inline] 45 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 46 | Pin::new(&mut self.0).poll_flush(cx) 47 | } 48 | 49 | #[inline] 50 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 51 | Pin::new(&mut self.0).poll_close(cx) 52 | } 53 | } 54 | 55 | impl Read for AsyncStream 56 | where 57 | IO: Unpin + AsyncRead, 58 | { 59 | #[inline] 60 | #[instrument(skip(self, cx, buf))] 61 | fn poll_read( 62 | mut self: Pin<&mut Self>, 63 | cx: &mut Context<'_>, 64 | buf: &mut [u8], 65 | ) -> Poll> { 66 | let mut read_buf = ReadBuf::new(buf); 67 | ready!(Pin::new(&mut self.0).poll_read(cx, &mut read_buf))?; 68 | trace!("read {} bytes", read_buf.filled().len()); 69 | Poll::Ready(Ok(read_buf.filled().len())) 70 | } 71 | } 72 | 73 | impl Write for AsyncStream 74 | where 75 | IO: Unpin + AsyncWrite, 76 | { 77 | #[inline] 78 | #[instrument(skip(self, cx, buf))] 79 | fn poll_write( 80 | mut self: Pin<&mut Self>, 81 | cx: &mut Context<'_>, 82 | buf: &[u8], 83 | ) -> Poll> { 84 | let size = ready!(Pin::new(&mut self.0).poll_write(cx, buf))?; 85 | trace!("wrote {} bytes", size); 86 | Poll::Ready(Ok(size)) 87 | } 88 | 89 | #[inline] 90 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 91 | Pin::new(&mut self.0).poll_flush(cx) 92 | } 93 | 94 | #[inline] 95 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 96 | Pin::new(&mut self.0).poll_shutdown(cx) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /roa/src/tcp.rs: -------------------------------------------------------------------------------- 1 | //! This module provides an acceptor implementing `roa_core::Accept` and an app extension. 2 | //! 3 | //! ### TcpIncoming 4 | //! 5 | //! ``` 6 | //! use roa::{App, Context, Result}; 7 | //! use roa::tcp::TcpIncoming; 8 | //! use std::io; 9 | //! 10 | //! async fn end(_ctx: &mut Context) -> Result { 11 | //! Ok(()) 12 | //! } 13 | //! # #[tokio::main] 14 | //! # async fn main() -> io::Result<()> { 15 | //! let app = App::new().end(end); 16 | //! let incoming = TcpIncoming::bind("127.0.0.1:0")?; 17 | //! let server = app.accept(incoming); 18 | //! // server.await 19 | //! Ok(()) 20 | //! # } 21 | //! ``` 22 | //! 23 | //! ### Listener 24 | //! 25 | //! ``` 26 | //! use roa::{App, Context, Result}; 27 | //! use roa::tcp::Listener; 28 | //! use std::io; 29 | //! 30 | //! async fn end(_ctx: &mut Context) -> Result { 31 | //! Ok(()) 32 | //! } 33 | //! # #[tokio::main] 34 | //! # async fn main() -> io::Result<()> { 35 | //! let app = App::new().end(end); 36 | //! let (addr, server) = app.bind("127.0.0.1:0")?; 37 | //! // server.await 38 | //! Ok(()) 39 | //! # } 40 | //! ``` 41 | 42 | mod incoming; 43 | mod listener; 44 | 45 | #[doc(inline)] 46 | pub use incoming::TcpIncoming; 47 | #[doc(inline)] 48 | pub use listener::Listener; 49 | -------------------------------------------------------------------------------- /roa/src/tcp/incoming.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::future::Future; 3 | use std::mem::transmute; 4 | use std::net::{SocketAddr, TcpListener as StdListener, ToSocketAddrs}; 5 | use std::pin::Pin; 6 | use std::task::{self, Poll}; 7 | use std::time::Duration; 8 | use std::{fmt, io, matches}; 9 | 10 | use roa_core::{Accept, AddrStream}; 11 | use tokio::net::{TcpListener, TcpStream}; 12 | use tokio::time::{sleep, Sleep}; 13 | use tracing::{debug, error, trace}; 14 | 15 | /// A stream of connections from binding to an address. 16 | /// As an implementation of roa_core::Accept. 17 | #[must_use = "streams do nothing unless polled"] 18 | pub struct TcpIncoming { 19 | addr: SocketAddr, 20 | listener: Box, 21 | sleep_on_errors: bool, 22 | tcp_nodelay: bool, 23 | timeout: Option>>, 24 | accept: Option>>, 25 | } 26 | 27 | type BoxedAccept<'a> = 28 | Box> + Send + Sync>; 29 | 30 | impl TcpIncoming { 31 | /// Creates a new `TcpIncoming` binding to provided socket address. 32 | pub fn bind(addr: impl ToSocketAddrs) -> io::Result { 33 | let listener = StdListener::bind(addr)?; 34 | TcpIncoming::from_std(listener) 35 | } 36 | 37 | /// Creates a new `TcpIncoming` from std TcpListener. 38 | pub fn from_std(listener: StdListener) -> io::Result { 39 | let addr = listener.local_addr()?; 40 | listener.set_nonblocking(true)?; 41 | Ok(TcpIncoming { 42 | listener: Box::new(listener.try_into()?), 43 | addr, 44 | sleep_on_errors: true, 45 | tcp_nodelay: false, 46 | timeout: None, 47 | accept: None, 48 | }) 49 | } 50 | 51 | /// Get the local address bound to this listener. 52 | pub fn local_addr(&self) -> SocketAddr { 53 | self.addr 54 | } 55 | 56 | /// Set the value of `TCP_NODELAY` option for accepted connections. 57 | pub fn set_nodelay(&mut self, enabled: bool) -> &mut Self { 58 | self.tcp_nodelay = enabled; 59 | self 60 | } 61 | 62 | /// Set whether to sleep on accept errors. 63 | /// 64 | /// A possible scenario is that the process has hit the max open files 65 | /// allowed, and so trying to accept a new connection will fail with 66 | /// `EMFILE`. In some cases, it's preferable to just wait for some time, if 67 | /// the application will likely close some files (or connections), and try 68 | /// to accept the connection again. If this option is `true`, the error 69 | /// will be logged at the `error` level, since it is still a big deal, 70 | /// and then the listener will sleep for 1 second. 71 | /// 72 | /// In other cases, hitting the max open files should be treat similarly 73 | /// to being out-of-memory, and simply error (and shutdown). Setting 74 | /// this option to `false` will allow that. 75 | /// 76 | /// Default is `true`. 77 | pub fn set_sleep_on_errors(&mut self, val: bool) { 78 | self.sleep_on_errors = val; 79 | } 80 | 81 | /// Poll TcpStream. 82 | fn poll_stream( 83 | &mut self, 84 | cx: &mut task::Context<'_>, 85 | ) -> Poll> { 86 | // Check if a previous timeout is active that was set by IO errors. 87 | if let Some(ref mut to) = self.timeout { 88 | futures::ready!(Pin::new(to).poll(cx)); 89 | } 90 | self.timeout = None; 91 | 92 | loop { 93 | if self.accept.is_none() { 94 | let accept: Pin> = Box::pin(self.listener.accept()); 95 | self.accept = Some(unsafe { transmute(accept) }); 96 | } 97 | 98 | if let Some(f) = &mut self.accept { 99 | match futures::ready!(f.as_mut().poll(cx)) { 100 | Ok((socket, addr)) => { 101 | if let Err(e) = socket.set_nodelay(self.tcp_nodelay) { 102 | trace!("error trying to set TCP nodelay: {}", e); 103 | } 104 | self.accept = None; 105 | return Poll::Ready(Ok((socket, addr))); 106 | } 107 | Err(e) => { 108 | // Connection errors can be ignored directly, continue by 109 | // accepting the next request. 110 | if is_connection_error(&e) { 111 | debug!("accepted connection already errored: {}", e); 112 | continue; 113 | } 114 | 115 | if self.sleep_on_errors { 116 | error!("accept error: {}", e); 117 | 118 | // Sleep 1s. 119 | let mut timeout = Box::pin(sleep(Duration::from_secs(1))); 120 | 121 | match timeout.as_mut().poll(cx) { 122 | Poll::Ready(()) => { 123 | // Wow, it's been a second already? Ok then... 124 | continue; 125 | } 126 | Poll::Pending => { 127 | self.timeout = Some(timeout); 128 | return Poll::Pending; 129 | } 130 | } 131 | } else { 132 | return Poll::Ready(Err(e)); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | impl Accept for TcpIncoming { 142 | type Conn = AddrStream; 143 | type Error = io::Error; 144 | 145 | #[inline] 146 | fn poll_accept( 147 | mut self: Pin<&mut Self>, 148 | cx: &mut task::Context<'_>, 149 | ) -> Poll>> { 150 | let (stream, addr) = futures::ready!(self.poll_stream(cx))?; 151 | Poll::Ready(Some(Ok(AddrStream::new(addr, stream)))) 152 | } 153 | } 154 | 155 | impl Drop for TcpIncoming { 156 | fn drop(&mut self) { 157 | self.accept = None; 158 | } 159 | } 160 | 161 | /// This function defines errors that are per-connection. Which basically 162 | /// means that if we get this error from `accept()` system call it means 163 | /// next connection might be ready to be accepted. 164 | /// 165 | /// All other errors will incur a timeout before next `accept()` is performed. 166 | /// The timeout is useful to handle resource exhaustion errors like ENFILE 167 | /// and EMFILE. Otherwise, could enter into tight loop. 168 | fn is_connection_error(e: &io::Error) -> bool { 169 | matches!( 170 | e.kind(), 171 | io::ErrorKind::ConnectionRefused 172 | | io::ErrorKind::ConnectionAborted 173 | | io::ErrorKind::ConnectionReset 174 | ) 175 | } 176 | 177 | impl fmt::Debug for TcpIncoming { 178 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 179 | f.debug_struct("TcpIncoming") 180 | .field("addr", &self.addr) 181 | .field("sleep_on_errors", &self.sleep_on_errors) 182 | .field("tcp_nodelay", &self.tcp_nodelay) 183 | .finish() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /roa/src/tcp/listener.rs: -------------------------------------------------------------------------------- 1 | use std::net::{SocketAddr, ToSocketAddrs}; 2 | use std::sync::Arc; 3 | 4 | use roa_core::{App, Endpoint, Executor, Server, State}; 5 | 6 | use super::TcpIncoming; 7 | 8 | /// An app extension. 9 | pub trait Listener { 10 | /// http server 11 | type Server; 12 | 13 | /// Listen on a socket addr, return a server and the real addr it binds. 14 | fn bind(self, addr: impl ToSocketAddrs) -> std::io::Result<(SocketAddr, Self::Server)>; 15 | 16 | /// Listen on a socket addr, return a server, and pass real addr to the callback. 17 | fn listen( 18 | self, 19 | addr: impl ToSocketAddrs, 20 | callback: impl Fn(SocketAddr), 21 | ) -> std::io::Result; 22 | 23 | /// Listen on an unused port of 127.0.0.1, return a server and the real addr it binds. 24 | /// ### Example 25 | /// ```rust 26 | /// use roa::{App, Context, Status}; 27 | /// use roa::tcp::Listener; 28 | /// use roa::http::StatusCode; 29 | /// use tokio::task::spawn; 30 | /// use std::time::Instant; 31 | /// 32 | /// async fn end(_ctx: &mut Context) -> Result<(), Status> { 33 | /// Ok(()) 34 | /// } 35 | /// 36 | /// #[tokio::main] 37 | /// async fn main() -> Result<(), Box> { 38 | /// let (addr, server) = App::new().end(end).run()?; 39 | /// spawn(server); 40 | /// let resp = reqwest::get(&format!("http://{}", addr)).await?; 41 | /// assert_eq!(StatusCode::OK, resp.status()); 42 | /// Ok(()) 43 | /// } 44 | /// ``` 45 | fn run(self) -> std::io::Result<(SocketAddr, Self::Server)>; 46 | } 47 | 48 | impl Listener for App> 49 | where 50 | S: State, 51 | E: for<'a> Endpoint<'a, S>, 52 | { 53 | type Server = Server; 54 | fn bind(self, addr: impl ToSocketAddrs) -> std::io::Result<(SocketAddr, Self::Server)> { 55 | let incoming = TcpIncoming::bind(addr)?; 56 | let local_addr = incoming.local_addr(); 57 | Ok((local_addr, self.accept(incoming))) 58 | } 59 | 60 | fn listen( 61 | self, 62 | addr: impl ToSocketAddrs, 63 | callback: impl Fn(SocketAddr), 64 | ) -> std::io::Result { 65 | let (addr, server) = self.bind(addr)?; 66 | callback(addr); 67 | Ok(server) 68 | } 69 | 70 | fn run(self) -> std::io::Result<(SocketAddr, Self::Server)> { 71 | self.bind("127.0.0.1:0") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /roa/src/tls.rs: -------------------------------------------------------------------------------- 1 | //! This module provides an acceptor implementing `roa_core::Accept` and an app extension. 2 | //! 3 | //! ### TlsIncoming 4 | //! 5 | //! ```rust 6 | //! use roa::{App, Context, Status}; 7 | //! use roa::tls::{TlsIncoming, ServerConfig, Certificate, PrivateKey}; 8 | //! use roa::tls::pemfile::{certs, rsa_private_keys}; 9 | //! use std::fs::File; 10 | //! use std::io::BufReader; 11 | //! 12 | //! async fn end(_ctx: &mut Context) -> Result<(), Status> { 13 | //! Ok(()) 14 | //! } 15 | //! 16 | //! # #[tokio::main] 17 | //! # async fn main() -> Result<(), Box> { 18 | //! let mut cert_file = BufReader::new(File::open("../assets/cert.pem")?); 19 | //! let mut key_file = BufReader::new(File::open("../assets/key.pem")?); 20 | //! let cert_chain = certs(&mut cert_file)?.into_iter().map(Certificate).collect(); 21 | //! 22 | //! let config = ServerConfig::builder() 23 | //! .with_safe_defaults() 24 | //! .with_no_client_auth() 25 | //! .with_single_cert(cert_chain, PrivateKey(rsa_private_keys(&mut key_file)?.remove(0)))?; 26 | //! 27 | //! let incoming = TlsIncoming::bind("127.0.0.1:0", config)?; 28 | //! let server = App::new().end(end).accept(incoming); 29 | //! // server.await 30 | //! Ok(()) 31 | //! # } 32 | //! ``` 33 | //! 34 | //! ### TlsListener 35 | //! 36 | //! ```rust 37 | //! use roa::{App, Context, Status}; 38 | //! use roa::tls::{ServerConfig, TlsListener, Certificate, PrivateKey}; 39 | //! use roa::tls::pemfile::{certs, rsa_private_keys}; 40 | //! use std::fs::File; 41 | //! use std::io::BufReader; 42 | //! 43 | //! async fn end(_ctx: &mut Context) -> Result<(), Status> { 44 | //! Ok(()) 45 | //! } 46 | //! 47 | //! # #[tokio::main] 48 | //! # async fn main() -> Result<(), Box> { 49 | //! let mut cert_file = BufReader::new(File::open("../assets/cert.pem")?); 50 | //! let mut key_file = BufReader::new(File::open("../assets/key.pem")?); 51 | //! let cert_chain = certs(&mut cert_file)?.into_iter().map(Certificate).collect(); 52 | //! 53 | //! let config = ServerConfig::builder() 54 | //! .with_safe_defaults() 55 | //! .with_no_client_auth() 56 | //! .with_single_cert(cert_chain, PrivateKey(rsa_private_keys(&mut key_file)?.remove(0)))?; 57 | //! let (addr, server) = App::new().end(end).bind_tls("127.0.0.1:0", config)?; 58 | //! // server.await 59 | //! Ok(()) 60 | //! # } 61 | //! ``` 62 | 63 | #[doc(no_inline)] 64 | pub use rustls::*; 65 | #[doc(no_inline)] 66 | pub use rustls_pemfile as pemfile; 67 | 68 | mod incoming; 69 | 70 | #[cfg(feature = "tcp")] 71 | mod listener; 72 | 73 | #[doc(inline)] 74 | pub use incoming::TlsIncoming; 75 | #[doc(inline)] 76 | #[cfg(feature = "tcp")] 77 | pub use listener::TlsListener; 78 | -------------------------------------------------------------------------------- /roa/src/tls/incoming.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::ops::{Deref, DerefMut}; 3 | use std::pin::Pin; 4 | use std::sync::Arc; 5 | use std::task::{self, Context, Poll}; 6 | 7 | use futures::Future; 8 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 9 | use tokio_rustls::server::TlsStream; 10 | use tokio_rustls::TlsAcceptor; 11 | 12 | use super::ServerConfig; 13 | use crate::{Accept, AddrStream}; 14 | 15 | /// A stream of connections based on another stream. 16 | /// As an implementation of roa_core::Accept. 17 | pub struct TlsIncoming { 18 | incoming: I, 19 | acceptor: TlsAcceptor, 20 | } 21 | 22 | type AcceptFuture = 23 | dyn 'static + Sync + Send + Unpin + Future>>; 24 | 25 | /// A finite-state machine to do tls handshake. 26 | pub enum WrapTlsStream { 27 | /// Handshaking state. 28 | Handshaking(Box>), 29 | /// Streaming state. 30 | Streaming(Box>), 31 | } 32 | 33 | use WrapTlsStream::*; 34 | 35 | impl WrapTlsStream { 36 | #[inline] 37 | fn poll_handshake( 38 | handshake: &mut AcceptFuture, 39 | cx: &mut Context<'_>, 40 | ) -> Poll> { 41 | let stream = futures::ready!(Pin::new(handshake).poll(cx))?; 42 | Poll::Ready(Ok(Streaming(Box::new(stream)))) 43 | } 44 | } 45 | 46 | impl AsyncRead for WrapTlsStream 47 | where 48 | IO: 'static + Unpin + AsyncRead + AsyncWrite, 49 | { 50 | fn poll_read( 51 | mut self: Pin<&mut Self>, 52 | cx: &mut Context<'_>, 53 | buf: &mut ReadBuf<'_>, 54 | ) -> Poll> { 55 | match &mut *self { 56 | Streaming(stream) => Pin::new(stream).poll_read(cx, buf), 57 | Handshaking(handshake) => { 58 | *self = futures::ready!(Self::poll_handshake(handshake, cx))?; 59 | self.poll_read(cx, buf) 60 | } 61 | } 62 | } 63 | } 64 | 65 | impl AsyncWrite for WrapTlsStream 66 | where 67 | IO: 'static + Unpin + AsyncRead + AsyncWrite, 68 | { 69 | fn poll_write( 70 | mut self: Pin<&mut Self>, 71 | cx: &mut Context<'_>, 72 | buf: &[u8], 73 | ) -> Poll> { 74 | match &mut *self { 75 | Streaming(stream) => Pin::new(stream).poll_write(cx, buf), 76 | Handshaking(handshake) => { 77 | *self = futures::ready!(Self::poll_handshake(handshake, cx))?; 78 | self.poll_write(cx, buf) 79 | } 80 | } 81 | } 82 | 83 | fn poll_write_vectored( 84 | mut self: Pin<&mut Self>, 85 | cx: &mut Context<'_>, 86 | bufs: &[io::IoSlice<'_>], 87 | ) -> Poll> { 88 | match &mut *self { 89 | Streaming(stream) => Pin::new(stream).poll_write_vectored(cx, bufs), 90 | Handshaking(handshake) => { 91 | *self = futures::ready!(Self::poll_handshake(handshake, cx))?; 92 | self.poll_write_vectored(cx, bufs) 93 | } 94 | } 95 | } 96 | 97 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 98 | match &mut *self { 99 | Streaming(stream) => Pin::new(stream).poll_flush(cx), 100 | Handshaking(handshake) => { 101 | *self = futures::ready!(Self::poll_handshake(handshake, cx))?; 102 | self.poll_flush(cx) 103 | } 104 | } 105 | } 106 | 107 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 108 | match &mut *self { 109 | Streaming(stream) => Pin::new(stream).poll_shutdown(cx), 110 | Handshaking(handshake) => { 111 | *self = futures::ready!(Self::poll_handshake(handshake, cx))?; 112 | self.poll_shutdown(cx) 113 | } 114 | } 115 | } 116 | } 117 | 118 | impl TlsIncoming { 119 | /// Construct from inner incoming. 120 | pub fn new(incoming: I, config: ServerConfig) -> Self { 121 | Self { 122 | incoming, 123 | acceptor: Arc::new(config).into(), 124 | } 125 | } 126 | } 127 | 128 | impl Deref for TlsIncoming { 129 | type Target = I; 130 | fn deref(&self) -> &Self::Target { 131 | &self.incoming 132 | } 133 | } 134 | 135 | impl DerefMut for TlsIncoming { 136 | fn deref_mut(&mut self) -> &mut Self::Target { 137 | &mut self.incoming 138 | } 139 | } 140 | 141 | impl Accept for TlsIncoming 142 | where 143 | IO: 'static + Send + Sync + Unpin + AsyncRead + AsyncWrite, 144 | I: Unpin + Accept>, 145 | { 146 | type Conn = AddrStream>; 147 | type Error = I::Error; 148 | 149 | #[inline] 150 | fn poll_accept( 151 | mut self: Pin<&mut Self>, 152 | cx: &mut task::Context<'_>, 153 | ) -> Poll>> { 154 | Poll::Ready( 155 | match futures::ready!(Pin::new(&mut self.incoming).poll_accept(cx)) { 156 | Some(Ok(AddrStream { 157 | stream, 158 | remote_addr, 159 | })) => { 160 | let accept_future = self.acceptor.accept(stream); 161 | Some(Ok(AddrStream::new( 162 | remote_addr, 163 | Handshaking(Box::new(accept_future)), 164 | ))) 165 | } 166 | Some(Err(err)) => Some(Err(err)), 167 | None => None, 168 | }, 169 | ) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /roa/src/tls/listener.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::{SocketAddr, ToSocketAddrs}; 3 | use std::sync::Arc; 4 | 5 | use super::{ServerConfig, TlsIncoming}; 6 | use crate::tcp::TcpIncoming; 7 | use crate::{App, Endpoint, Executor, Server, State}; 8 | 9 | impl TlsIncoming { 10 | /// Bind a socket addr. 11 | #[cfg_attr(feature = "docs", doc(cfg(feature = "tls")))] 12 | pub fn bind(addr: impl ToSocketAddrs, config: ServerConfig) -> io::Result { 13 | Ok(Self::new(TcpIncoming::bind(addr)?, config)) 14 | } 15 | } 16 | 17 | /// An app extension. 18 | #[cfg_attr(feature = "docs", doc(cfg(feature = "tls")))] 19 | pub trait TlsListener { 20 | /// http server 21 | type Server; 22 | 23 | /// Listen on a socket addr, return a server and the real addr it binds. 24 | fn bind_tls( 25 | self, 26 | addr: impl ToSocketAddrs, 27 | config: ServerConfig, 28 | ) -> std::io::Result<(SocketAddr, Self::Server)>; 29 | 30 | /// Listen on a socket addr, return a server, and pass real addr to the callback. 31 | fn listen_tls( 32 | self, 33 | addr: impl ToSocketAddrs, 34 | config: ServerConfig, 35 | callback: impl Fn(SocketAddr), 36 | ) -> std::io::Result; 37 | 38 | /// Listen on an unused port of 127.0.0.1, return a server and the real addr it binds. 39 | /// ### Example 40 | /// ```rust 41 | /// use roa::{App, Context, Status}; 42 | /// use roa::tls::{TlsIncoming, ServerConfig, TlsListener, Certificate, PrivateKey}; 43 | /// use roa::tls::pemfile::{certs, rsa_private_keys}; 44 | /// use roa_core::http::StatusCode; 45 | /// use tokio::task::spawn; 46 | /// use std::time::Instant; 47 | /// use std::fs::File; 48 | /// use std::io::BufReader; 49 | /// 50 | /// async fn end(_ctx: &mut Context) -> Result<(), Status> { 51 | /// Ok(()) 52 | /// } 53 | /// # #[tokio::main] 54 | /// # async fn main() -> Result<(), Box> { 55 | /// let mut cert_file = BufReader::new(File::open("../assets/cert.pem")?); 56 | /// let mut key_file = BufReader::new(File::open("../assets/key.pem")?); 57 | /// let cert_chain = certs(&mut cert_file)?.into_iter().map(Certificate).collect(); 58 | /// 59 | /// let config = ServerConfig::builder() 60 | /// .with_safe_defaults() 61 | /// .with_no_client_auth() 62 | /// .with_single_cert(cert_chain, PrivateKey(rsa_private_keys(&mut key_file)?.remove(0)))?; 63 | /// 64 | /// let server = App::new().end(end).listen_tls("127.0.0.1:8000", config, |addr| { 65 | /// println!("Server is listening on https://localhost:{}", addr.port()); 66 | /// })?; 67 | /// // server.await 68 | /// Ok(()) 69 | /// # } 70 | /// ``` 71 | fn run_tls(self, config: ServerConfig) -> std::io::Result<(SocketAddr, Self::Server)>; 72 | } 73 | 74 | impl TlsListener for App> 75 | where 76 | S: State, 77 | E: for<'a> Endpoint<'a, S>, 78 | { 79 | type Server = Server, Self, Executor>; 80 | fn bind_tls( 81 | self, 82 | addr: impl ToSocketAddrs, 83 | config: ServerConfig, 84 | ) -> std::io::Result<(SocketAddr, Self::Server)> { 85 | let incoming = TlsIncoming::bind(addr, config)?; 86 | let local_addr = incoming.local_addr(); 87 | Ok((local_addr, self.accept(incoming))) 88 | } 89 | 90 | fn listen_tls( 91 | self, 92 | addr: impl ToSocketAddrs, 93 | config: ServerConfig, 94 | callback: impl Fn(SocketAddr), 95 | ) -> std::io::Result { 96 | let (addr, server) = self.bind_tls(addr, config)?; 97 | callback(addr); 98 | Ok(server) 99 | } 100 | 101 | fn run_tls(self, config: ServerConfig) -> std::io::Result<(SocketAddr, Self::Server)> { 102 | self.bind_tls("127.0.0.1:0", config) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use std::fs::File; 109 | use std::io::{self, BufReader}; 110 | 111 | use futures::{AsyncReadExt, TryStreamExt}; 112 | use hyper::client::{Client, HttpConnector}; 113 | use hyper::Body; 114 | use hyper_tls::{native_tls, HttpsConnector}; 115 | use tokio::task::spawn; 116 | use tokio_native_tls::TlsConnector; 117 | 118 | use crate::http::StatusCode; 119 | use crate::tls::pemfile::{certs, rsa_private_keys}; 120 | use crate::tls::{Certificate, PrivateKey, ServerConfig, TlsListener}; 121 | use crate::{App, Context, Status}; 122 | 123 | async fn end(ctx: &mut Context) -> Result<(), Status> { 124 | ctx.resp.write("Hello, World!"); 125 | Ok(()) 126 | } 127 | 128 | #[tokio::test] 129 | async fn run_tls() -> Result<(), Box> { 130 | let mut cert_file = BufReader::new(File::open("../assets/cert.pem")?); 131 | let mut key_file = BufReader::new(File::open("../assets/key.pem")?); 132 | let cert_chain = certs(&mut cert_file)? 133 | .into_iter() 134 | .map(Certificate) 135 | .collect(); 136 | 137 | let config = ServerConfig::builder() 138 | .with_safe_defaults() 139 | .with_no_client_auth() 140 | .with_single_cert( 141 | cert_chain, 142 | PrivateKey(rsa_private_keys(&mut key_file)?.remove(0)), 143 | )?; 144 | 145 | let app = App::new().end(end); 146 | let (addr, server) = app.run_tls(config)?; 147 | spawn(server); 148 | 149 | let native_tls_connector = native_tls::TlsConnector::builder() 150 | .danger_accept_invalid_hostnames(true) 151 | .danger_accept_invalid_certs(true) 152 | .build()?; 153 | let tls_connector = TlsConnector::from(native_tls_connector); 154 | let mut http_connector = HttpConnector::new(); 155 | http_connector.enforce_http(false); 156 | let https_connector = HttpsConnector::from((http_connector, tls_connector)); 157 | let client = Client::builder().build::<_, Body>(https_connector); 158 | let resp = client 159 | .get(format!("https://localhost:{}", addr.port()).parse()?) 160 | .await?; 161 | assert_eq!(StatusCode::OK, resp.status()); 162 | let mut text = String::new(); 163 | resp.into_body() 164 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) 165 | .into_async_read() 166 | .read_to_string(&mut text) 167 | .await?; 168 | assert_eq!("Hello, World!", text); 169 | Ok(()) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /roa/src/websocket.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a websocket endpoint. 2 | //! 3 | //! ### Example 4 | //! ``` 5 | //! use futures::StreamExt; 6 | //! use roa::router::{Router, RouterError}; 7 | //! use roa::websocket::Websocket; 8 | //! use roa::{App, Context}; 9 | //! use roa::http::Method; 10 | //! 11 | //! # fn main() -> Result<(), RouterError> { 12 | //! let router = Router::new().on("/chat", Websocket::new(|_ctx, stream| async move { 13 | //! let (write, read) = stream.split(); 14 | //! // echo 15 | //! if let Err(err) = read.forward(write).await { 16 | //! println!("forward err: {}", err); 17 | //! } 18 | //! })); 19 | //! let app = App::new().end(router.routes("/")?); 20 | //! Ok(()) 21 | //! # } 22 | //! ``` 23 | 24 | use std::future::Future; 25 | use std::marker::PhantomData; 26 | use std::sync::Arc; 27 | 28 | use headers::{ 29 | Connection, HeaderMapExt, SecWebsocketAccept, SecWebsocketKey, SecWebsocketVersion, Upgrade, 30 | }; 31 | use hyper::upgrade::{self, Upgraded}; 32 | pub use tokio_tungstenite::tungstenite; 33 | pub use tokio_tungstenite::tungstenite::protocol::{Message, WebSocketConfig}; 34 | use tokio_tungstenite::WebSocketStream; 35 | 36 | use crate::http::header::UPGRADE; 37 | use crate::http::StatusCode; 38 | use crate::{async_trait, throw, Context, Endpoint, State, Status}; 39 | 40 | /// An alias for WebSocketStream. 41 | pub type SocketStream = WebSocketStream; 42 | 43 | /// The Websocket middleware. 44 | /// 45 | /// ### Example 46 | /// ``` 47 | /// use futures::StreamExt; 48 | /// use roa::router::{Router, RouterError}; 49 | /// use roa::websocket::Websocket; 50 | /// use roa::{App, Context}; 51 | /// use roa::http::Method; 52 | /// 53 | /// # fn main() -> Result<(), RouterError> { 54 | /// let router = Router::new().on("/chat", Websocket::new(|_ctx, stream| async move { 55 | /// let (write, read) = stream.split(); 56 | /// // echo 57 | /// if let Err(err) = read.forward(write).await { 58 | /// println!("forward err: {}", err); 59 | /// } 60 | /// })); 61 | /// let app = App::new().end(router.routes("/")?); 62 | /// Ok(()) 63 | /// # } 64 | /// ``` 65 | /// 66 | /// ### Parameter 67 | /// 68 | /// - Context 69 | /// 70 | /// The context is the same with roa context, 71 | /// however, neither read body from request or write anything to response is unavailing. 72 | /// 73 | /// - SocketStream 74 | /// 75 | /// The websocket stream, implementing `Stream` and `Sink`. 76 | /// 77 | /// ### Return 78 | /// 79 | /// Must be `()`, as roa cannot deal with errors occurring in websocket. 80 | pub struct Websocket 81 | where 82 | F: Fn(Context, SocketStream) -> Fut, 83 | { 84 | task: Arc, 85 | config: Option, 86 | _s: PhantomData, 87 | _fut: PhantomData, 88 | } 89 | 90 | unsafe impl Send for Websocket where 91 | F: Sync + Send + Fn(Context, SocketStream) -> Fut 92 | { 93 | } 94 | unsafe impl Sync for Websocket where 95 | F: Sync + Send + Fn(Context, SocketStream) -> Fut 96 | { 97 | } 98 | 99 | impl Websocket 100 | where 101 | F: Fn(Context, SocketStream) -> Fut, 102 | { 103 | fn config(config: Option, task: F) -> Self { 104 | Self { 105 | task: Arc::new(task), 106 | config, 107 | _s: PhantomData::default(), 108 | _fut: PhantomData::default(), 109 | } 110 | } 111 | 112 | /// Construct a websocket middleware by task closure. 113 | pub fn new(task: F) -> Self { 114 | Self::config(None, task) 115 | } 116 | 117 | /// Construct a websocket middleware with config. 118 | /// ### Example 119 | /// ``` 120 | /// use futures::StreamExt; 121 | /// use roa::router::{Router, RouterError}; 122 | /// use roa::websocket::{Websocket, WebSocketConfig}; 123 | /// use roa::{App, Context}; 124 | /// use roa::http::Method; 125 | /// 126 | /// # fn main() -> Result<(), RouterError> { 127 | /// let router = Router::new().on("/chat", Websocket::with_config( 128 | /// WebSocketConfig::default(), 129 | /// |_ctx, stream| async move { 130 | /// let (write, read) = stream.split(); 131 | /// // echo 132 | /// if let Err(err) = read.forward(write).await { 133 | /// println!("forward err: {}", err); 134 | /// } 135 | /// }) 136 | /// ); 137 | /// let app = App::new().end(router.routes("/")?); 138 | /// # Ok(()) 139 | /// # } 140 | /// ``` 141 | pub fn with_config(config: WebSocketConfig, task: F) -> Self { 142 | Self::config(Some(config), task) 143 | } 144 | } 145 | 146 | #[async_trait(?Send)] 147 | impl<'a, F, S, Fut> Endpoint<'a, S> for Websocket 148 | where 149 | S: State, 150 | F: 'static + Sync + Send + Fn(Context, SocketStream) -> Fut, 151 | Fut: 'static + Send + Future, 152 | { 153 | #[inline] 154 | async fn call(&'a self, ctx: &'a mut Context) -> Result<(), Status> { 155 | let header_map = &ctx.req.headers; 156 | let key = header_map 157 | .typed_get::() 158 | .filter(|upgrade| upgrade == &Upgrade::websocket()) 159 | .and(header_map.typed_get::()) 160 | .filter(|connection| connection.contains(UPGRADE)) 161 | .and(header_map.typed_get::()) 162 | .filter(|version| version == &SecWebsocketVersion::V13) 163 | .and(header_map.typed_get::()); 164 | 165 | match key { 166 | None => throw!(StatusCode::BAD_REQUEST, "invalid websocket upgrade request"), 167 | Some(key) => { 168 | let raw_req = ctx.req.take_raw(); 169 | let context = ctx.clone(); 170 | let task = self.task.clone(); 171 | let config = self.config; 172 | // Setup a future that will eventually receive the upgraded 173 | // connection and talk a new protocol, and spawn the future 174 | // into the runtime. 175 | // 176 | // Note: This can't possibly be fulfilled until the 101 response 177 | // is returned below, so it's better to spawn this future instead 178 | // waiting for it to complete to then return a response. 179 | ctx.exec.spawn(async move { 180 | match upgrade::on(raw_req).await { 181 | Err(err) => tracing::error!("websocket upgrade error: {}", err), 182 | Ok(upgraded) => { 183 | let websocket = WebSocketStream::from_raw_socket( 184 | upgraded, 185 | tungstenite::protocol::Role::Server, 186 | config, 187 | ) 188 | .await; 189 | task(context, websocket).await 190 | } 191 | } 192 | }); 193 | ctx.resp.status = StatusCode::SWITCHING_PROTOCOLS; 194 | ctx.resp.headers.typed_insert(Connection::upgrade()); 195 | ctx.resp.headers.typed_insert(Upgrade::websocket()); 196 | ctx.resp.headers.typed_insert(SecWebsocketAccept::from(key)); 197 | Ok(()) 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /roa/templates/user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | User Homepage 6 | 7 | 8 |

{{name}}

9 |

{{id}}

10 | 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Module" 3 | reorder_imports = true 4 | unstable_features = true 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(doctest)] 2 | doc_comment::doctest!("../README.md"); 3 | -------------------------------------------------------------------------------- /templates/directory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 |

{{root}}

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for dir in dirs %} 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 | {% for file in files %} 28 | 29 | 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
NameSizeModified
{{dir.name}}-{{dir.modified}}
{{file.name}}{{file.size}}{{file.modified}}
36 | 37 | -------------------------------------------------------------------------------- /tests/logger.rs: -------------------------------------------------------------------------------- 1 | use std::sync::RwLock; 2 | 3 | use log::{Level, LevelFilter, Metadata, Record}; 4 | use once_cell::sync::Lazy; 5 | use roa::http::StatusCode; 6 | use roa::logger::logger; 7 | use roa::preload::*; 8 | use roa::{throw, App, Context}; 9 | use tokio::fs::File; 10 | use tokio::task::spawn; 11 | 12 | struct TestLogger { 13 | records: RwLock>, 14 | } 15 | impl log::Log for TestLogger { 16 | fn enabled(&self, metadata: &Metadata) -> bool { 17 | metadata.level() <= Level::Info 18 | } 19 | fn log(&self, record: &Record) { 20 | self.records 21 | .write() 22 | .unwrap() 23 | .push((record.level().to_string(), record.args().to_string())) 24 | } 25 | fn flush(&self) {} 26 | } 27 | 28 | static LOGGER: Lazy = Lazy::new(|| TestLogger { 29 | records: RwLock::new(Vec::new()), 30 | }); 31 | 32 | fn init() -> anyhow::Result<()> { 33 | log::set_logger(&*LOGGER) 34 | .map(|()| log::set_max_level(LevelFilter::Info)) 35 | .map_err(|err| anyhow::anyhow!("fail to init logger: {}", err)) 36 | } 37 | 38 | #[tokio::test] 39 | async fn log() -> anyhow::Result<()> { 40 | init()?; 41 | async fn bytes_info(ctx: &mut Context) -> roa::Result { 42 | ctx.resp.write("Hello, World."); 43 | Ok(()) 44 | } 45 | // bytes info 46 | let (addr, server) = App::new().gate(logger).end(bytes_info).run()?; 47 | spawn(server); 48 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 49 | assert_eq!(StatusCode::OK, resp.status()); 50 | assert_eq!("Hello, World.", resp.text().await?); 51 | let records = LOGGER.records.read().unwrap().clone(); 52 | assert_eq!(2, records.len()); 53 | assert_eq!("INFO", records[0].0); 54 | assert_eq!("--> GET /", records[0].1.trim_end()); 55 | assert_eq!("INFO", records[1].0); 56 | assert!(records[1].1.starts_with("<-- GET /")); 57 | assert!(records[1].1.contains("13 B")); 58 | assert!(records[1].1.trim_end().ends_with("200 OK")); 59 | 60 | // error 61 | async fn err(_ctx: &mut Context) -> roa::Result { 62 | throw!(StatusCode::BAD_REQUEST, "Hello, World!") 63 | } 64 | let (addr, server) = App::new().gate(logger).end(err).run()?; 65 | spawn(server); 66 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 67 | assert_eq!(StatusCode::BAD_REQUEST, resp.status()); 68 | assert_eq!("Hello, World!", resp.text().await?); 69 | let records = LOGGER.records.read().unwrap().clone(); 70 | assert_eq!(4, records.len()); 71 | assert_eq!("INFO", records[2].0); 72 | assert_eq!("--> GET /", records[2].1.trim_end()); 73 | assert_eq!("ERROR", records[3].0); 74 | assert!(records[3].1.starts_with("<-- GET /")); 75 | assert!(records[3].1.contains(&StatusCode::BAD_REQUEST.to_string())); 76 | assert!(records[3].1.trim_end().ends_with("Hello, World!")); 77 | 78 | // stream info 79 | async fn stream_info(ctx: &mut Context) -> roa::Result { 80 | ctx.resp 81 | .write_reader(File::open("assets/welcome.html").await?); 82 | Ok(()) 83 | } 84 | // bytes info 85 | let (addr, server) = App::new().gate(logger).end(stream_info).run()?; 86 | spawn(server); 87 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 88 | assert_eq!(StatusCode::OK, resp.status()); 89 | assert_eq!(236, resp.text().await?.len()); 90 | let records = LOGGER.records.read().unwrap().clone(); 91 | assert_eq!(6, records.len()); 92 | assert_eq!("INFO", records[4].0); 93 | assert_eq!("--> GET /", records[4].1.trim_end()); 94 | assert_eq!("INFO", records[5].0); 95 | assert!(records[5].1.starts_with("<-- GET /")); 96 | assert!(records[5].1.contains("236 B")); 97 | assert!(records[5].1.trim_end().ends_with("200 OK")); 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /tests/serve-file.rs: -------------------------------------------------------------------------------- 1 | use http::header::ACCEPT_ENCODING; 2 | use roa::body::DispositionType; 3 | use roa::compress::Compress; 4 | use roa::preload::*; 5 | use roa::router::{get, Router}; 6 | use roa::{App, Context}; 7 | use tokio::fs::read_to_string; 8 | use tokio::task::spawn; 9 | 10 | #[tokio::test] 11 | async fn serve_static_file() -> Result<(), Box> { 12 | async fn test(ctx: &mut Context) -> roa::Result { 13 | ctx.write_file("assets/author.txt", DispositionType::Inline) 14 | .await 15 | } 16 | let app = App::new().end(get(test)); 17 | let (addr, server) = app.run()?; 18 | spawn(server); 19 | let resp = reqwest::get(&format!("http://{}", addr)).await?; 20 | assert_eq!("Hexilee", resp.text().await?); 21 | Ok(()) 22 | } 23 | 24 | #[tokio::test] 25 | async fn serve_router_variable() -> Result<(), Box> { 26 | async fn test(ctx: &mut Context) -> roa::Result { 27 | let filename = ctx.must_param("filename")?; 28 | ctx.write_file(format!("assets/{}", &*filename), DispositionType::Inline) 29 | .await 30 | } 31 | let router = Router::new().on("/:filename", get(test)); 32 | let app = App::new().end(router.routes("/")?); 33 | let (addr, server) = app.run()?; 34 | spawn(server); 35 | let resp = reqwest::get(&format!("http://{}/author.txt", addr)).await?; 36 | assert_eq!("Hexilee", resp.text().await?); 37 | Ok(()) 38 | } 39 | 40 | #[tokio::test] 41 | async fn serve_router_wildcard() -> Result<(), Box> { 42 | async fn test(ctx: &mut Context) -> roa::Result { 43 | let path = ctx.must_param("path")?; 44 | ctx.write_file(format!("./{}", &*path), DispositionType::Inline) 45 | .await 46 | } 47 | let router = Router::new().on("/*{path}", get(test)); 48 | let app = App::new().end(router.routes("/")?); 49 | let (addr, server) = app.run()?; 50 | spawn(server); 51 | let resp = reqwest::get(&format!("http://{}/assets/author.txt", addr)).await?; 52 | assert_eq!("Hexilee", resp.text().await?); 53 | Ok(()) 54 | } 55 | 56 | #[tokio::test] 57 | async fn serve_gzip() -> Result<(), Box> { 58 | async fn test(ctx: &mut Context) -> roa::Result { 59 | ctx.write_file("assets/welcome.html", DispositionType::Inline) 60 | .await 61 | } 62 | let app = App::new().gate(Compress::default()).end(get(test)); 63 | let (addr, server) = app.run()?; 64 | spawn(server); 65 | let client = reqwest::Client::builder().gzip(true).build()?; 66 | let resp = client 67 | .get(&format!("http://{}", addr)) 68 | .header(ACCEPT_ENCODING, "gzip") 69 | .send() 70 | .await?; 71 | 72 | assert_eq!( 73 | read_to_string("assets/welcome.html").await?, 74 | resp.text().await? 75 | ); 76 | Ok(()) 77 | } 78 | --------------------------------------------------------------------------------