Skip to content

Cookbook

Hexilee edited this page Apr 2, 2020 · 20 revisions

Basic

Application

App is the entry point of roa application, it registers middlewares and endpoint, then serves as a http server.

Endpoint

An endpoint is a request handler.

There are some build-in endpoints in roa.

  • Functional endpoint

    A normal functional endpoint is an async function with signature: async fn(&mut Context) -> Result.

    use roa::{App, Context, Result};
    
    async fn endpoint(ctx: &mut Context) -> Result {
        Ok(())
    }
    
    let app = App::new().end(endpoint);
  • Ok endpoint

    () is an endpoint always return Ok(())

    let app = roa::App::new().end(());
  • Status endpoint

    Status is an endpoint always return Err(Status)

    use roa::{App, status};
    use roa::http::StatusCode;
    let app = App::new().end(status!(StatusCode::BAD_REQUEST));
  • String endpoint

    Write string to body.

    use roa::App;
    
    let app = App::new().end("Hello, world"); // static slice
    let app = App::new().end("Hello, world".to_owned()); // string
  • Redirect endpoint

    Redirect to an uri.

    use roa::App;
    use roa::http::Uri;
    
    let app = App::new().end("/target".parse::<Uri>().unwrap());

Run

  • listen

    Try to listen on a socket addr, return a server, and pass real addr to the callback.

    use roa::App;
    use roa::preload::*;
    use std::error::Error;
    
    #[async_std::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        App::new()
            .end("Hello, world")
            .listen("127.0.0.1:8000", |addr| {
                println!("Server is listening on {}", addr)
            })?
            .await?;
        Ok(())
    }
  • bind

    Try to listen on a socket addr, return a server and the real addr it binds.

    use roa::App;
    use roa::preload::*;
    use std::error::Error;
    
    #[async_std::main]
    async fn main() -> Result<(), Box<dyn Error>> {    
        let (addr, server) = App::new()
            .end("Hello, world")
            .bind("127.0.0.1:8000")?;
        println!("Server is listening on {}", addr);
        server.await?;
        Ok(())
    }
  • run

    Try to listen on a random unused port of 127.0.0.1, return a server and the real addr it binds.

    use roa::App;
    use roa::preload::*;
    use std::error::Error;
    
    #[async_std::main]
    async fn main() -> Result<(), Box<dyn Error>> {    
        let (addr, server) = App::new()
            .end("Hello, world")
            .run()?;
        println!("Server is listening on {}", addr);
        server.await?;
        Ok(())
    }

Status Code

The status of response is 200 OK by default. Roa provides two ways to change it.

  • Normally set status code

    use roa::{Context, Result};
    use roa::http::StatusCode;
    
    async fn create(ctx: &mut Context) -> Result {
        ctx.resp.status = StatusCode::CREATED;
        Ok(())
    }
  • Throw status

    You can throw a status to fail fast.

    use roa::{Context, Result, throw};
    use roa::http::StatusCode;
    
    async fn create(ctx: &mut Context) -> Result {
        throw!(StatusCode::BAD_REQUEST)
    }

    Errors will be converted to 500 INTERNAL SERVER ERROR by default:

    use roa::{Context, Result};
    
    async fn create(ctx: &mut Context) -> Result {
        let id: usize = "x".parse()?; // fail, throw 500 INTERNAL SERVER ERROR
        Ok(())
    }

Request

You can straightly access method, uri and version of request by field name or context method.

use roa::{Context, Result};

async fn handle(ctx: &mut Context) -> Result {
    let method = ctx.method();
    let method = &ctx.req.method;
    let uri = ctx.uri();
    let uri = &ctx.req.uri;
    let version = ctx.version();
    let version = &ctx.req.version;
    unimplemented!()
}

Headers

  • Header map

    You can get raw http HeaderMap of request by field name.

    use roa::{Context, Result, throw};
    use roa::http::header::HOST;
    use roa::http::StatusCode;
    
    async fn handle(ctx: &mut Context) -> Result {
        // get a string value
        let host = match ctx.req.headers.get(HOST).map(|value| value.to_str()) {
            Some(Ok(host)) => host,
            _ => throw!(StatusCode::BAD_REQUEST),
        };
        unimplemented!()
    }

    Raw header map is a little difficult to use, it's not a good choice to deal with header value which is a pure string.

  • Context::get

    The another choice is the get and must_get method of context:

    use roa::{Context, Result, throw};
    use roa::http::header::HOST;
    use roa::http::StatusCode;
    
    async fn handle(ctx: &mut Context) -> Result {
        // try get a string value
        let host = match ctx.get(HOST) {
            Some(host) => host,
            _ => throw!(StatusCode::BAD_REQUEST),
        };
    
        // must get, otherwise throw a 400 BAD REQUEST
        let host = ctx.must_get(HOST)?;
        unimplemented!()
    }

Body

  • Reader

    Read body as futures::io::AsyncRead.

    use roa::{Context, Result};
    use futures::AsyncReadExt;
    
    async fn handle(ctx: &mut Context) -> Result {
        let mut data = String::new();
        ctx.req.reader().read_to_string(&mut data).await?;
        unimplemented!()
    }
  • Stream

    Read body as futures::stream::Stream<Item = io::Result<Bytes>>.

    use roa::{Context, Result};
    
    // an echo endpoint
    async fn echo(ctx: &mut Context) -> Result {
        let stream = ctx.req.stream();
        ctx.resp.write_stream(stream);
        unimplemented!()
    }
  • PowerBody

    roa::body provides a extension trait PowerBody to read and write body more conveniently.

    use roa::{Context, Result};
    use roa::preload::*;
    use serde::Deserialize;
    
    #[derive(Deserialize)]
    struct User {
        id: u64,
        name: String,
    }
    
    // an echo endpoint
    async fn handle(ctx: &mut Context) -> Result {
        let data = ctx.read().await?; // get Vec<u8>
        let user: User = ctx.read_json().await?; // deserialize from json
        let user: User = ctx.read_form().await?; // deserialize from urlencoded form
        unimplemented!()
    }

Query

roa::query provides a middleware query_parser and a extension Query to parse query and get query pairs.

use roa::query::query_parser;
use roa::{App, Context, Result};
// import extension Query
use roa::preload::*;

async fn handle(ctx: &mut Context) -> Result {
    assert_eq!("Hexilee", &*ctx.must_query("name")?);
    Ok(())
}

let app = App::new().gate(query_parser).end(handle);

// get `/?name=Hexilee`

Response

You can straightly access status and version of response by field name.

use roa::{Context, Result};
use roa::http::{StatusCode, Version};

async fn handle(ctx: &mut Context) -> Result {
    ctx.resp.status = StatusCode::CREATED;
    ctx.resp.version = Version::HTTP_11;
    unimplemented!()
}

Headers

You can access the header map of response by field name.

use roa::{Context, Result, throw};
use roa::http::header::LOCATION;
use roa::http::{StatusCode, HeaderValue};

async fn handle(ctx: &mut Context) -> Result {
    // parse
    let location = "/target".parse()?;

    // or static parse
    // let location = HeaderValue::from_static("/target");

    ctx.resp.headers.insert(LOCATION, location);

    // redirect
    throw!(StatusCode::PERMANENT_REDIRECT)
}

Refer to HeaderMap docs for more details.

Body

roa_core provides several methods to write body.

use roa::{Context, Result};
use futures::AsyncReadExt;
use futures::io::BufReader;
use async_std::fs::File;

async fn get(ctx: &mut Context) -> Result {
    ctx.resp
        // write stream; echo
       .write_stream(ctx.req.stream())
       // write object implementing futures::AsyncRead
       .write_reader(File::open("assets/author.txt").await?)
       // write reader with specific chunk size
       .write_chunk(File::open("assets/author.txt").await?, 1024)
       // write text
       .write("I am Roa.")
       .write(b"I am Roa.".as_ref());
    Ok(())
}

These methods are useful, but they do not deal with headers and serialization.

roa::body provides extension PowerBody to handle it.

use roa::{Context, Result};
use roa::body::DispositionType::*;
use roa::preload::*;
use serde::{Serialize, Deserialize};
use askama::Template;
use async_std::fs::File;

#[derive(Debug, Serialize, Deserialize, Template)]
#[template(path = "user.html")]
struct User {
    id: u64,
    name: String,
}

async fn get(ctx: &mut Context) -> Result {
    // serialize object and write it to body,
    // set "Content-Type" = "application/json"
    // require `feature = "json"`
    ctx.write_json(&user)?;

    // open file and write it to body,
    // set "Content-Type" and "Content-Disposition"
    // require `feature = "file"`
    ctx.write_file("assets/welcome.html", Inline).await?;

    // write text,
    // set "Content-Type" = "text/plain"
    ctx.write("Hello, World!");

    // write object implementing AsyncRead,
    // set "Content-Type" = "application/octet-stream"
    ctx.write_reader(File::open("assets/author.txt").await?);

    // render html template, based on [askama](https://github.com/djc/askama)
    // set "Content-Type" = "text/html; charset=utf-8"
    // require `feature = "template"`
    ctx.render(&user)?;
    Ok(())
}

Router

roa::router provides a configurable and nestable router.

Configure router

use roa::{Context, Result, App};
use roa::router::{Router, get, allow, deny};
use roa::query::query_parser;
use roa::http::Method;

async fn create(ctx: &mut Context) -> Result {
    unimplemented!()
}

let router = Router::new()
    .gate(query_parser) // use middleware
    .on("/hello", "Hello, world") // allow all http method on /hello
    .on("/resources", allow([Method::GET, Method::POST], "Hello, world")) // allow get and post
    .on("/dangerous", deny([Method::POST, Method::PUT], create)) // deny post and put
    .on("/api", get("Hello, world").post(create)); // dispatch by http method


let app = App::new().end(router.routes("/prefix").unwrap());

Dynamic path

Routers can route on dynamic path.

use roa::{Context, Result, App};
use roa::router::Router;
use roa::preload::*;

async fn segment_variable(ctx: &mut Context) -> Result {
    let name = ctx.must_param("file")?;
    unimplemented!()
}

async fn wildcard(ctx: &mut Context) -> Result {
    let path = ctx.must_param("file")?;
    unimplemented!()
}

// segment variable
// one variable can only match one path segment.
// "/welcome.html" is matched.
// "/dir/welcome.html" is not matched.
let router = Router::new().
    on("/:file", segment_variable);

// wildcard variable
// "/welcome.html" is matched.
// "/dir/welcome.html" is also matched.
let router = Router::new().
    on("/*{file}", wildcard);

Nested router

Routers can be nested.

use roa::{Context, Result, App};
use roa::router::{Router, get};
use roa::preload::*;
use serde_json::json;

async fn query(ctx: &mut Context) -> Result {
    let id: u64 = ctx.must_param("id")?.parse()?;
    ctx.write_json(&json!({"id": id}))
}

let user_router = Router::new().on("/:id", get(query));

// include
let router = Router::new().include("/user", user_router);

let app = App::new().end(router.routes("/").unwrap());

// request: /user/0 => {"id": 0}

Authentication

Roa provides two solutions for authentication.

Cookie

roa::cookie provides a middleware cookie_parser and context extensions CookieSetter and CookieGetter.

feature = "cookies" is required.

roa = { version = "0.5.0", features = ["cookies"] }
  • Get cookie
use roa::cookie::cookie_parser;
use roa::{App, Result, Context};
use roa::preload::*;
use roa::http::StatusCode;
use roa::http::header::COOKIE;

async fn handle(ctx: &mut Context) -> Result {
    let optional_name = ctx.cookie("name"); // get Option<Cookie>
    let name = ctx.must_cookie("name")?; // must get, otherwise throw 401 UNAUTHORIZED
    unimplemented!()
}

let app = App::new()
    .gate(cookie_parser)
    .end(handle);
  • Set cookie
use roa::cookie::Cookie;
use roa::{Context, Result};
use roa::preload::*;

async fn login(ctx: &mut Context) -> Result {
    // set cookie in percent encoding
    // name=Hexi%20Lee
    ctx.set_cookie(Cookie::new("name", "Hexi Lee"))?;
    unimplemented!()
}

JWT

roa::jwt module provides a middleware JwtGuard and a context extension JwtVerifier.

feature = "jwt" is required.

roa = { version = "0.5.0", features = ["jwt"] }
use roa::App;
use roa::jwt::{guard, DecodingKey};

const SECRET: &[u8] = b"123456";

let app = App::new()
    .gate(guard(DecodingKey::from_secret(SECRET)))
    .end("Hello, world");

The guard function receive a secret and return a middleware to guard its downstream.

The json web token should be deliver by request header "authorization", in format of Authorization: Bearer <token>.

  • Get claims

You can add use roa::jwt::JwtVerifier or use roa::preload::* to use this context extension.

Attention please, this extension must be used in downstream of guard, otherwise you cannot get expected claims.

use roa::{Context, Result};
use roa::preload::*;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    sub: String,
    company: String,
    exp: u64,
    id: u64,
    name: String,
}

async fn get(ctx: &mut Context) -> Result {
    let user: User = ctx.claims()?;
    // do something
    Ok(())
}
  • Configured validation

You can configure Validation to set validation options.

use roa::App;
use roa::jwt::{JwtGuard, DecodingKey, Validation};

const SECRET: &[u8] = b"123456";

let validation = Validation {leeway: 60, ..Default::default()};
let decoding_key = DecodingKey::from_secret(SECRET);

let app = App::new()
    .gate(JwtGuard::new(decoding_key, validation))
    .end("Hello, world");

The JwtVerifier also provides method verify to verify token by custom validation:

use roa::{Context, Result};
use roa::jwt::Validation;
use roa::preload::*;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    sub: String,
    company: String,
    exp: u64,
    id: u64,
    name: String,
    aud: Vec<String>,
}

// this endpoint can only be accessed by admin
async fn delete_user(ctx: &mut Context) -> Result {
    let mut validation = Validation::default();
    validation.set_audience(&["admin"]);
    let user: User = ctx.verify(&validation)?;
    unimplemented!()
}

Other Useful Extensions

Compress

roa::compress provides a transparent content compression middleware.

feature = "compress" is required.

roa = { version = "0.5.0", features = ["compress"] }
use roa::App;
use roa::compress::{Compress, Level};

let app = App::new()
    .gate(Compress(Level::Fastest))
    .end("Hello, world");

It will negotiate with client and compress response body automatically, supports gzip, deflate, brotli, zstd and identity.

Logger

The roa::logger provides a middleware logger.

use roa::App;
use roa::logger::logger;

let mut app = App::new();

// just use it
app.gate(logger);

logger middleware to log information about request and response.

Based on crate log, the log level must be greater than INFO to log all information, and should be greater than ERROR when you need error information only.

CORS

A middleware to deal with Cross-Origin Resource Sharing (CORS).

  • Default

The default Cors middleware will satisfy all needs of a request.

Build a default Cors middleware:

use roa::App;
use roa::cors::Cors;

let app = App::new()
    .gate(Cors::new())
    .end("Hello, world");
  • Config

You can also configure it:

use roa::cors::Cors;
use roa::http::header::{CONTENT_DISPOSITION, AUTHORIZATION, WWW_AUTHENTICATE};
use roa::http::Method;

let cors = Cors::builder()
    .allow_credentials(true)
    .max_age(86400)
    .allow_origin("https://github.com")
    .allow_methods(vec![Method::GET, Method::POST])
    .allow_method(Method::PUT)
    .expose_headers(vec![CONTENT_DISPOSITION])
    .expose_header(WWW_AUTHENTICATE)
    .allow_headers(vec![AUTHORIZATION])
    .allow_header(CONTENT_DISPOSITION)
    .build();

Forward

roa::forward provides a context extension Forward to parse X-Forwarded-* request headers.

  • host
    • If "x-forwarded-host" is set and valid, use it.
    • Else if "host" is set and valid, use it.
use roa::{Context, Result};
use roa::preload::*;

async fn get(ctx: &mut Context) -> Result {
    if let Some(host) = ctx.host() {
        println!("host: {}", host);
    }
    Ok(())
}
  • client_ip
    • If "x-forwarded-for" is set and valid, use the first ip.
    • Else use the ip addr of ctx.remote_addr.
use roa::{Context, Result};
use roa::preload::*;

async fn get(ctx: &mut Context) -> Result {
    println!("client ip: {}", ctx.client_ip());
    Ok(())
}
  • forwarded_ips
use roa::{Context, Result};
use roa::forward::Forward;

async fn get(ctx: Context<()>) -> Result {
    println!("forwarded ips: {:?}", ctx.forwarded_ips());
    Ok(())
}
  • forwarded_proto
use roa::{Context, Result};
use roa::forward::Forward;

async fn get(ctx: Context<()>) -> Result {
    if let Some(proto) = ctx.forwarded_proto() {
        println!("forwarded proto: {}", proto);
    }
    Ok(())
}

Advanced

Middleware

  • Functional middleware

    A functional middleware is an async function with signature: async fn(&mut Context, Next<'_>) -> Result.

    use roa::{App, Context, Next, Result};
    
    async fn middleware(ctx: &mut Context, next: Next<'_>) -> Result {
        next.await
    }
    
    let app = App::new().gate(middleware);
  • Custom middleware

    You can implement custom Middleware for other types.

    use roa::{App, Middleware, Context, Next, Result, async_trait};
    use std::sync::Arc;
    use std::time::Instant;
    
    struct Logger;
    
    #[async_trait(?Send)]
    impl <'a> Middleware<'a> for Logger {
        async fn handle(&'a self, ctx: &'a mut Context, next: Next<'a>) -> Result {
            let start = Instant::now();
            let result = next.await;
            println!("time elapsed: {}ms", start.elapsed().as_millis());
            result
        }
    }
    
    let app = App::new().gate(Logger);

Cascading

Like koajs, middleware suspends and passes control to "downstream" by invoking next.await. Then control flows back "upstream" when next.await returns.

The following example responds with "Hello World", however first the request flows through the x-response-time and logging middleware to mark when the request started, then continue to yield control through the endpoint. When a middleware invokes next the function suspends and passes control to the next middleware or endpoint. After the endpoint is called, the stack will unwind and each middleware is resumed to perform its upstream behaviour.

use roa::{App, Context, Next};
use roa::preload::*;
use std::time::Instant;
use log::info;

let app = App::new()
    .gate(logger)
    .gate(x_response_time)
    .end("Hello, World");

async fn logger(ctx: &mut Context, next: Next<'_>) -> roa::Result {
    next.await?;
    let rt = ctx.must_get("x-response-time")?;
    info!("{} {} - {}", ctx.method(), ctx.uri(), rt);
    Ok(())
}

async fn x_response_time(ctx: &mut Context, next: Next<'_>) -> roa::Result {
    let start = Instant::now();
    next.await?;
    let ms = start.elapsed().as_millis();
    ctx.resp.headers.insert("x-response-time", format!("{}ms", ms).parse()?);
    Ok(())
}

Status Handling

You can catch or straightly throw a status returned by next.

use roa::{App, Context, Next, status};
use roa::preload::*;
use roa::http::StatusCode;

let app = App::new()
    .gate(catch)
    .gate(not_catch)
    .end(status!(StatusCode::IM_A_TEAPOT, "I'm a teapot!"));

async fn catch(_ctx: &mut Context, next: Next<'_>) -> roa::Result {
    // catch
    if let Err(status) = next.await {
        // teapot is ok
        if status.status_code != StatusCode::IM_A_TEAPOT {
            return Err(status);
        }
    }
    Ok(())
}

async fn not_catch(ctx: &mut Context, next: Next<'_>) -> roa::Result {
    next.await?; // just throw
    unreachable!()
}
  • Uncaught status

There is an status handler to handle uncaught status in app.

use roa::{Context, Status};
pub fn status_handler<S>(ctx: &mut Context<S>, status: Status) {
    ctx.resp.status = status.status_code;
    if status.expose {
        ctx.resp.write(status.message);
    } else {
        log::error!("{}", status);
    }
}

State and Storage

Roa provides two solutions to pass data between middlewares and endpoint.

State

Use App::state method to initialize an app with a state.

The app.state will be cloned and passed by context when a request inbounds. You can access state by context,

use roa::{App, Context, Next, Result};
use roa::http::StatusCode;

#[derive(Clone)]
struct State {
    id: u64,
}

let app = App::state(State { id: 0 }).gate(gate).end(end);

async fn gate(ctx: &mut Context<State>, next: Next<'_>) -> Result {
    ctx.id = 1;
    next.await
}

async fn end(ctx: &mut Context<State>) -> Result {
    let id = ctx.id;
    assert_eq!(1, id);
    Ok(())
}

Storage

There is an individual storage in each context, you can store or load any data in it.

use roa::{App, Context, Result, Next};

struct Data(i32);

async fn gate(ctx: &mut Context, next: Next<'_>) -> Result {
    ctx.store("id", Data(1));
    next.await
}

async fn end(ctx: &mut Context) -> Result {
    assert_eq!(1, ctx.load::<Data>("id").unwrap().0);
    Ok(())
}

let app = App::new().gate(gate).end(end);
  • scope

    You can store data in a private scope, then other middlewares or endpoint cannot access it straightly.

    use roa::{App, Context, Result, Next};
    
    struct Scope;
    
    struct Data(i32);
    
    async fn gate(ctx: &mut Context, next: Next<'_>) -> Result {
        ctx.store_scoped(Scope, "id", Data(1));
        next.await
    }
    
    async fn end(ctx: &mut Context) -> Result {
        assert_eq!(1, ctx.load_scoped::<Scope, Data>("id").unwrap().0);
        Ok(())
    }
    
    let app = App::new().gate(gate).end(end);

Database

Diesel

The roa-diesel crate provides integration of diesel, please refer to docs or integration example for more details.

Tokio-postgres

The roa-pg crate provides integration of tokio-postgres. A simple query service:

use roa::{App, Context, throw};
use roa::http::StatusCode;
use roa_pg::{connect, Client};
use std::sync::Arc;
use std::error::Error;
use roa::query::query_parser;
use roa::preload::*;
use async_std::task::spawn;

#[derive(Clone)]
struct State {
    pg: Arc<Client>
}

impl State {
    pub async fn new(pg_url: &str) -> Result<Self, Box<dyn Error>> {
        let (client, conn) = connect(&pg_url.parse()?).await?;
        spawn(conn);
        Ok(Self {pg: Arc::new(client)})
    }
}

async fn query(ctx: &mut Context<State>) -> roa::Result {
    let id: u32 = ctx.must_query("id")?.parse()?;
    match ctx.pg.query_opt("SELECT * FROM user WHERE id=$1", &[&id]).await? {
        Some(row) => {
            let value: String = row.get(0);
            ctx.write(value);
            Ok(())
        }
        None => throw!(StatusCode::NOT_FOUND),
    }
}

#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let url = "postgres://fred:secret@localhost/test";
    let state = State::new(url).await?;
    App::state(state)
        .gate(query_parser)
        .end(query)
        .listen("127.0.0.1:0", |addr| {
            println!("Server is listening on {}", addr)
        })?.await?;
    Ok(())
}

Multipart

The roa-multipart crate provides support for multipart form. A file uploading service:

use async_std::fs::File;
use async_std::io;
use async_std::path::Path;
use futures::stream::TryStreamExt;
use futures::StreamExt;
use roa::http::StatusCode;
use roa::tcp::Listener;
use roa::router::{Router, post};
use roa::{throw, App, Context};
use roa_multipart::MultipartForm;
use std::error::Error as StdError;

async fn post_file(ctx: &mut Context) -> roa::Result {
    let mut form = ctx.form();
    while let Some(item) = form.next().await {
        let field = item?;
        match field.content_disposition() {
            None => throw!(StatusCode::BAD_REQUEST, "content disposition not set"),
            Some(content_disposition) => match content_disposition.get_filename() {
                None => continue, // ignore non-file field
                Some(filename) => {
                    let path = Path::new("./upload");
                    let mut file = File::create(path.join(filename)).await?;
                    io::copy(&mut field.into_async_read(), &mut file).await?;
                }
            },
        }
    }
    Ok(())
}

#[async_std::main]
async fn main() -> Result<(), Box<dyn StdError>> {
    let router = Router::new().on("/file", post(post_file));
    App::new()
        .end(router.routes("/")?)
        .listen("127.0.0.1:8000", |addr| {
            println!("Server is listening on {}", addr);
        })?
        .await?;
    Ok(())
}

GraphQL

The roa-juniper crate provides integration of juniper. However, you cannot get it from crates.io until juniper v0.14.3 is published.

You can preview this feature in integration-example.

Protocol

Websocket

Roa supports websocket when feature = "websocket" is enabled. An echo service:

use futures::StreamExt;
use roa::http::Method;
use log::{error, info};
use roa::cors::Cors;
use roa::logger::logger;
use roa::preload::*;
use roa::router::{allow, Router};
use roa::websocket::Websocket;
use roa::App;
use std::error::Error as StdError;

#[async_std::main]
async fn main() -> Result<(), Box<dyn StdError>> {
    let router = Router::new().on(
        "/chat",
        allow(
            [Method::GET],
            Websocket::new(|_ctx, stream| async move {
                let (write, read) = stream.split();
                if let Err(err) = read.forward(write).await {
                    error!("forward err: {}", err);
                }
            }),
        ),
    );
    let app = App::new()
        .gate(logger)
        .gate(Cors::new())
        .end(router.routes("/")?);
    app.listen("127.0.0.1:8000", |addr| {
        info!("Server is listening on {}", addr)
    })?
    .await?;
    Ok(())
}

HTTPS

Roa supports tls when feature = "tls" is enabled, you can start a https server like this:

use roa::App;
use roa::tls::{TlsIncoming, ServerConfig, NoClientAuth, TlsListener};
use roa::tls::internal::pemfile::{certs, rsa_private_keys};
use std::fs::File;
use std::io::BufReader;
use std::error::Error;

#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let mut config = ServerConfig::new(NoClientAuth::new());
    let mut cert_file = BufReader::new(File::open("path/to/cert.pem")?);
    let mut key_file = BufReader::new(File::open("path/to/key.pem")?);
    let cert_chain = certs(&mut cert_file).unwrap();
    let mut keys = rsa_private_keys(&mut key_file).unwrap();
    
    config.set_single_cert(cert_chain, keys.remove(0))?;
    
    let (addr, server) = App::new()
        .end("Hello, world")
        .bind_tls("127.0.0.1:0", config)?;
    println!("Server is listening on {}", addr);
    server.await?;
    Ok(())
}

Runtime

  • Async-std

    Roa framework use async runtime and TcpStream of async-std by default. You can disable this feature in Cargo.toml.

    roa = { version = "0.5.0", default-features = false }
  • Tokio

    The roa-tokio crate provides tokio-based async runtime and TcpStream, you can use it like this:

    use roa::http::StatusCode;
    use roa::{App, Context};
    use roa_tokio::{TcpIncoming, Exec};
    use std::error::Error;
    
    async fn end(_ctx: &mut Context) -> roa::Result {
        Ok(())
    }
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        let app = App::with_exec((), Exec).end(end);
        let incoming = TcpIncoming::bind("127.0.0.1:0")?;
        println!("server is listening on {}", incoming.local_addr());
        app.accept(incoming).await?;
        Ok(())
    }

Executor

You can access the global executor by context.

use roa::{Context, Result};

async fn endpoint(ctx: &mut Context) -> Result {
    let id = ctx.exec.spawn(async {1i32}).await;
    let words = ctx.exec.spawn_blocking(|| "Hello, world!").await;
    Ok(())
}

Graceful Shutdown

Roa app runs on hyper::Server, which supports graceful shutdown.

use roa::App;
use roa::preload::*;
use futures::channel::oneshot;
use std::error::Error;

#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Prepare some signal for when the server should start shutting down...
    let (tx, rx) = oneshot::channel::<()>();
    App::new()
        .end("Hello, world")
        .listen("127.0.0.1:8000", |addr| {
            println!("Server is listening on {}", addr)
        })?
        .with_graceful_shutdown(async {
            rx.await.ok();
        })
        .await?;
    // Await the `server` receiving the signal...
    
    // And later, trigger the signal by calling `tx.send(())`.
    let _ = tx.send(());
    Ok(())
}