├── assets
├── author.txt
├── welcome.html
├── css
│ └── table.css
├── cert.pem
└── key.pem
├── integration
├── multipart-example
│ ├── upload
│ │ └── .gitkeep
│ ├── README.md
│ ├── assets
│ │ └── index.html
│ ├── Cargo.toml
│ └── src
│ │ └── main.rs
├── websocket-example
│ ├── README.md
│ ├── Cargo.toml
│ └── src
│ │ └── main.rs
├── juniper-example
│ ├── README.md
│ ├── src
│ │ ├── schema.rs
│ │ ├── models.rs
│ │ └── main.rs
│ └── Cargo.toml
└── diesel-example
│ ├── src
│ ├── schema.rs
│ ├── models.rs
│ ├── data_object.rs
│ ├── bin
│ │ └── api.rs
│ ├── lib.rs
│ └── endpoints.rs
│ ├── README.md
│ ├── Cargo.toml
│ └── tests
│ └── restful.rs
├── src
└── lib.rs
├── .gitignore
├── rustfmt.toml
├── roa
├── templates
│ └── user.html
├── src
│ ├── body
│ │ ├── file
│ │ │ ├── help.rs
│ │ │ └── content_disposition.rs
│ │ └── file.rs
│ ├── router
│ │ ├── endpoints.rs
│ │ ├── endpoints
│ │ │ ├── guard.rs
│ │ │ └── dispatcher.rs
│ │ └── err.rs
│ ├── tcp.rs
│ ├── lib.rs
│ ├── jsonrpc.rs
│ ├── tcp
│ │ ├── listener.rs
│ │ └── incoming.rs
│ ├── tls.rs
│ ├── stream.rs
│ ├── logger.rs
│ ├── tls
│ │ ├── incoming.rs
│ │ └── listener.rs
│ ├── forward.rs
│ └── websocket.rs
└── Cargo.toml
├── .github
└── workflows
│ ├── security-audit.yml
│ ├── clippy.yml
│ ├── nightly-test.yml
│ ├── stable-test.yml
│ ├── code-coverage.yml
│ └── release.yml
├── roa-async-std
├── src
│ ├── lib.rs
│ ├── runtime.rs
│ └── listener.rs
├── README.md
└── Cargo.toml
├── Makefile
├── roa-diesel
├── src
│ ├── lib.rs
│ ├── pool.rs
│ └── async_ext.rs
├── Cargo.toml
└── README.md
├── examples
├── hello-world.rs
├── welcome.rs
├── echo.rs
├── websocket-echo.rs
├── https.rs
├── restful-api.rs
└── serve-file.rs
├── roa-juniper
├── Cargo.toml
├── README.md
└── src
│ └── lib.rs
├── templates
└── directory.html
├── roa-core
├── src
│ ├── state.rs
│ ├── app
│ │ ├── future.rs
│ │ ├── runtime.rs
│ │ └── stream.rs
│ ├── lib.rs
│ ├── response.rs
│ ├── executor.rs
│ ├── request.rs
│ ├── group.rs
│ ├── err.rs
│ ├── context
│ │ └── storage.rs
│ └── body.rs
├── Cargo.toml
└── README.md
├── LICENSE
├── Cargo.toml
├── tests
├── serve-file.rs
└── logger.rs
└── README.md
/assets/author.txt:
--------------------------------------------------------------------------------
1 | Hexilee
--------------------------------------------------------------------------------
/integration/multipart-example/upload/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/integration/websocket-example/README.md:
--------------------------------------------------------------------------------
1 | WIP...
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[cfg(doctest)]
2 | doc_comment::doctest!("../README.md");
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | Cargo.lock
4 | **/upload/*
5 | .env
6 | node_modules
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | group_imports = "StdExternalCrate"
2 | imports_granularity = "Module"
3 | reorder_imports = true
4 | unstable_features = true
5 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/roa/templates/user.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User Homepage
6 |
7 |
8 | {{name}}
9 | {{id}}
10 |
11 |
--------------------------------------------------------------------------------
/assets/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Roa Framework
6 |
7 |
8 | Welcome!
9 | Go to roa for more information...
10 |
11 |
--------------------------------------------------------------------------------
/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/multipart-example/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 | Upload Test
3 |
4 |
8 |
9 |
--------------------------------------------------------------------------------
/.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 }}
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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-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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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"] }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/roa-juniper/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/Hexilee/roa/actions)
2 | [](https://codecov.io/gh/Hexilee/roa)
3 | [](https://docs.rs/roa-juniper)
4 | [](https://crates.io/crates/roa-juniper)
5 | [](https://crates.io/crates/roa-juniper)
6 | [](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).
--------------------------------------------------------------------------------
/templates/directory.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{title}}
6 |
7 |
8 |
9 | {{root}}
10 |
11 |
12 |
13 |
14 | | Name |
15 | Size |
16 | Modified |
17 |
18 |
19 |
20 | {% for dir in dirs %}
21 |
22 | | {{dir.name}} |
23 | - |
24 | {{dir.modified}} |
25 |
26 | {% endfor %}
27 | {% for file in files %}
28 |
29 | | {{file.name}} |
30 | {{file.size}} |
31 | {{file.modified}} |
32 |
33 | {% endfor %}
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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-async-std/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/Hexilee/roa/actions)
2 | [](https://codecov.io/gh/Hexilee/roa)
3 | [](https://docs.rs/roa-async-std)
4 | [](https://crates.io/crates/roa-async-std)
5 | [](https://crates.io/crates/roa-async-std)
6 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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-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-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-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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/roa-diesel/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/Hexilee/roa/actions)
2 | [](https://codecov.io/gh/Hexilee/roa)
3 | [](https://docs.rs/roa-diesel)
4 | [](https://crates.io/crates/roa-diesel)
5 | [](https://crates.io/crates/roa-diesel)
6 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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-----
--------------------------------------------------------------------------------
/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-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/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-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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Roa
3 |
Roa is an async web framework inspired by koajs, lightweight but powerful.
4 |
5 |
6 | [](https://github.com/Hexilee/roa/actions)
7 | [](https://codecov.io/gh/Hexilee/roa)
8 | [](https://github.com/Hexilee/roa/wiki)
9 | [](https://docs.rs/roa)
10 | [](https://crates.io/crates/roa)
11 | [](https://crates.io/crates/roa)
12 | [](https://blog.rust-lang.org/2021/07/29/Rust-1.54.0.html)
13 | [](https://github.com/Hexilee/roa/blob/master/LICENSE)
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-----
--------------------------------------------------------------------------------
/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-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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/roa-core/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/Hexilee/roa/actions)
2 | [](https://codecov.io/gh/Hexilee/roa)
3 | [](https://docs.rs/roa-core)
4 | [](https://crates.io/crates/roa-core)
5 | [](https://crates.io/crates/roa-core)
6 | [](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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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