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でも使えますし、今後も期待です。