Rust用Web framework、Rocketを使ってみよう

2023.02.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Introduction

Rust用のWebフレームワークはActix-webやaxumなどいろいろありますが、
それらと同じくらい人気のフレームワークが、Rocketです。

今回はこのRocketを試してみます。

Rocket?

Rocketは、使いやすさを維持しつつ、高速&安全&スケーラブルな
Webアプリを開発できるRust用Webフレームワークです。
シンプルなでAPIで直感的に使いやすく、
ドキュメントもそろっているので安心です。

テンプレート機能やルーティング、ミドルウェアなどの機能も持っていて、
基本的なWebアプリに必要な機能に加えて拡張性があります。

このあと実際にコードをかきつつ、Rocketの機能について紹介していきます。

Environment

今回試した環境は以下のとおりです。

  • MacBook Pro (13-inch, M1, 2020)
  • OS : MacOS 12.4
  • Rust : 1.66.1
  • cargo-shuttle : 0.10.0

cargo shuttleは下記コマンドでインストール可能です。

% cargo install cargo-shuttle

Try Rocket!

Quick start

では、CargoをつかってRustのプロジェクトを作成しましょう。

% cargo new rocket_example --bin
     Created binary (application) `rocket_example` package

% cd rocket_example/

Cargo.tomlにrocketのライブラリを指定します。
rocket_dyn_templatesもあとで使うのでついでに設定しておきます。

[dependencies]
rocket = "0.5.0-rc.2"

[dependencies.rocket_dyn_templates]
version = "0.1.0-rc.2"
features = ["handlebars","tera"]

src/main.rsにHello worldプログラムを記述しましょう。
コードみるとわかるように、ルーティングの書き方もシンプルです。
マクロをつかってGETメソッドで/helloにアクセスすると
index関数が実行されます。
(マクロを使わなくても書ける)

use rocket::{get, launch, routes};

#[get("/hello")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

起動してアクセスしてみます。

% cargo run

・・・

Configured for debug.
   >> address: 127.0.0.1
   >> port: 8000
・・・
? Routes:
   >> (index) GET /hello
? Fairings:
   >> Shield (liftoff, response, singleton)
?️ Shield:
   >> Permissions-Policy: interest-cohort=()
   >> X-Content-Type-Options: nosniff
   >> X-Frame-Options: SAMEORIGIN
? Rocket has launched from http://127.0.0.1:8000

http://localhost:8000/helloにGETでアクセスすると結果がかえってきます。
では、このあとRocketの機能をいくつか実装してみます。

テンプレート

Rocketはテンプレート機能を実装しており、handlebarsかteraを使って
簡単にテンプレートを作成することができます。
ここではteraをつかってみましょう。

まずは適当なteraファイル、
templates/index.html.teraファイルを作成します。

<!DOCTYPE html>
<html>
  <head>
    <title>Templating Example</title>
  </head>
  <body>
     Argument :{{ foo }}
  </body>
</html>

Rocketは設定項目としてはtemplate_dirをもっており、
デフォルトがtemplatesとなっています。
テンプレート用ディレクトリを変更したい場合は自分で
template_dirを設定しましょう。

main.rsを下記のように修正します。

use rocket::{get, launch, routes};
use rocket_dyn_templates::{Template, handlebars, context};

#[get("/templeting/<arg_foo>")]
pub fn templating(arg_foo: &str) -> Template {
    Template::render("index", context! {
        foo: arg_foo,        
    })

}

#[launch]
fn rocket() -> _ {
  rocket::build()
    .mount("/", routes![templating])
    .attach(Template::fairing())
}

attach関数にTemplateを渡して、テンプレート機能を有効化します。
テンプレートは後述するFairingとして実装されているので、
これでテンプレート機能を使えます。

curlで動作確認。

% curl http://localhost:8000/templeting/tera-template

<!DOCTYPE html>
<html>
  <head>
    <title>Templating Example</title>
  </head>
  <body>
     Argument :tera-template
  </body>
</html>%

なお、debugモードで起動している場合、
teraファイル修正後、サーバを再起動しなくても変更が反映されます。

Profiles

debug用、release用などのモードに応じたアプリ設定を簡単に行うことができます。
プロジェクトルートにRocket.tomlファイルを下記内容で作成しましょう。

[default]
host = "localhost"
limits = { form = "64 kB", json = "1 MiB" }
foo="bar"

[debug]
port = 8000
limits = { json = "10MiB" }
foo="buzz"


[release]
host="dev.classmethod.jp"
port = 9999
foo="hoge"

main.rsでConfigをつかってみます。
Rocket起動時、Rocket.tomlの設定が
起動したときのモード(--debugとか--releseとか)
に応じてロードされ、コンソールに表示されます。

use rocket::serde::Deserialize;
use rocket::{get, launch, routes};

・・・

#[launch]
fn rocket() -> _ {
  let rocket = rocket::build()
    .mount("/", routes![index]);

    
    let figment = rocket.figment();
    #[derive(Deserialize,Debug)]
    #[serde(crate = "rocket::serde")]
    struct Config {
        port: u16,
        foo: String,
        //var_env:String,
    }

    let config: Config = figment.extract().expect("config");
    println!("{:?}",config);

    rocket

}

cargo runで起動すると、デフォルトはdebugモードなので
下記内容がコンソールに出力されます。

・・・
Config { port: 8000, foo: "buzz" }
・・・

また、prefixに「ROCKET_」とつけた環境変数があれば、その値も設定されます。
(Rocket.tomlの値より優先される)
つまり、↓のように「ROCKET_VAR_ENV」という環境変数をセットすれば、
var_envという名前のパラメータが使えます。

% export ROCKET_VAR_ENV="hello"

Testing Library

Rocketのユニットテスト&結合テストは、標準のテスト用ライブラリを使って簡単にかけます。
↓のindex関数をテストしたい場合、

//main.rs

#[get("/hello")]
fn index() -> &'static str {
    "Hello, world!"
}

下記のようにClientオブジェクトを使って、
簡単に検証可能です。

//main.rs
#[cfg(test)]
mod test {
    use super::rocket;
    use rocket::uri;
    use rocket::http::{ContentType, Status};
    use rocket::local::blocking::Client;

    #[test]
    fn index() {
        let client = Client::tracked(rocket()).expect("valid rocket instance");
        let mut response = client.get(uri!(super::index)).dispatch();

        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.content_type(), Some(ContentType::Plain));
        assert!(response.headers().get_one("X-Content-Type-Options").is_some());
        assert_eq!(response.into_string().unwrap(), "Hello, world!");
    }
}

cargo testでテストが実行されます。

% cargo test

running 1 tests
test test::index ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s

Fairings

さきほどteraテンプレートをつかった機能は、
FairingsというRocketの機能を使って実現しています。
これは、Expressのミドルウェアみたいなもので、
通常のリクエストーレスポンス間に任意の処理を実装する仕組みです。

ここにあるサンプルをつかってみましょう。
src/fairings.rsファイルを↓のように実装します。
(fairing関数以外はサンプルそのまま)

use std::future::Future;
use std::io::Cursor;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};

use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Method, ContentType, Status};

#[derive(Default)]
pub struct Counter {
    get: AtomicUsize,
    post: AtomicUsize,
}

impl Counter {
    pub fn fairing() -> impl Fairing {
        Counter{
            get:AtomicUsize::new(0),
            post:AtomicUsize::new(0)
        }
    }
}

#[rocket::async_trait]
impl Fairing for Counter {

    fn info(&self) -> Info {
        Info {
            name: "GET/POST Counter",
            kind: Kind::Request | Kind::Response
        }
    }

    async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
        if req.method() == Method::Get {
            self.get.fetch_add(1, Ordering::Relaxed);
        } else if req.method() == Method::Post {
            self.post.fetch_add(1, Ordering::Relaxed);
        }
    }

    async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
        // Don't change a successful user's response, ever.
        if res.status() != Status::NotFound {
            return
        }

        if req.method() == Method::Get && req.uri().path() == "/counts" {
            let get_count = self.get.load(Ordering::Relaxed);
            let post_count = self.post.load(Ordering::Relaxed);

            let body = format!("Get: {}\nPost: {}", get_count, post_count);
            res.set_status(Status::Ok);
            res.set_header(ContentType::Plain);
            res.set_sized_body(body.len(), Cursor::new(body));
        }
    }
}

Fairingを作るには、rocket::fairing::Fairingトレイトを実装します。
Fairingはリクエスト/レスポンスなどのイベントのコールバックを受け取り、
リクエスト/レスポンスの書き換えやロギングなど、
自由に処理させることができます。

次に、src/main.rsを修正します。
build時に↑のFairingsをattachします。

・・・

mod fairings;
use fairings::*;
use rocket::fairing::AdHoc;

#[launch]
fn rocket() -> _ {

  rocket::build()
    .mount("/", routes![index])
    .attach(Counter::fairing()) //Counter fairingsを追加
    .attach(Template::fairing())
}

アプリを起動後、何度かindexをアクセスします。
その後/countsにアクセスすると、Counter fairingによって計測された
Get/Postリクエスト実行回数を表示します。

% curl http://localhost:8000/counts
Get: 12
Post: 0

また、Fairingトレイトを実装するのが面倒な場合は
AdHoc Fairingを使う方法もあります。
これは、rocket::fairing::AdHocの関数とクロージャをつかって
簡単に実装できます。

↓では、3つのAdHoc Fairing設定しています。
on_liftoffは起動時、on_requestはすべてのリクエスト時、
on_shutdownはアプリのシャットダウン前にそれぞれ実行されます。

use rocket::{get, launch, routes};
use rocket::fairing::AdHoc;

・・・

#[launch]
fn rocket() -> _ {

    rocket::build()
    .mount("/", routes![index])
    .attach(AdHoc::on_liftoff("Startup", |_| Box::pin(async move {
        println!("=== start up! ===");
    })))
    .attach(AdHoc::on_request("All Request", |req, _| Box::pin(async move {
        println!("{:?}",req);
     })))
    .attach(AdHoc::on_shutdown("Shutdow", |_| Box::pin(async move {
        println!("=== shutdown! ===");
    })))
}

以上、Rocketの機能をいくつか簡単に解説しました。
まだまだ便利な機能がありますので、ガイドをご確認ください。

Rocket with Shuttle

以前紹介した、
RustのサーバレスプラットフォームであるShuttle.rsでは、Rocketを選択して
アプリを実装することができます。

手順は簡単で、cargo-shuttleをつかって
init時にRocketを選択すればOKです。

% mkdir your_shuttle_project && cd  your_shuttle_project
% cargo shuttle login #APIキーを入力

% cargo shuttle init
How do you want to name your project? It will be hosted at ${project_name}.shuttleapp.rs.
✔ Project name · <your_shuttle_project>

Where should we create this project?
✔ Directory · .

Shuttle works with a range of web frameworks. Which one do you want to use?
›
  actix-web
  axum
❯ rocket
  tide
  tower
  poem
  salvo
  serenity
  poise
  warp
  thruster
  none

runでローカル起動、deployでShuttleにデプロイします。

% cargo shuttle run

% cargo shuttle deploy

Summary

今回はRustのWebフレームワーク、Rocketを試してみました。
とてもシンプルで使いやすいですね。
Shuttle.rsでも使えますし、今後も期待です。

References