rust

RustのRocketでWebアプリケーションを作ってみる

More than 1 year has passed since last update.

はじめに

RustのWebアプリケーションフレームワークとしてRocketが使えそうだなということで、今回は駆け足でRocketを紹介してみます。

前提条件

rustc 1.16.0-nightly (468227129 2017-01-03)

Hello Rocket

まずはHello, world!を返すシンプルなアプリケーションを動かしてみます。Rocketでは全てnightlyビルドを使って実行します。

$ cargo new --bin hello_rocket
$ cd hello_rocket
$ rustup override set nightly
Cargo.toml
[dependencies]
rocket = "0.1.4"
rocket_codegen = "0.1.4"

rocket_codegenはgetrouteを定義するためのカスタムderiveを提供します。

src/main.rs
#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

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

fn main() {
    rocket::ignite().mount("/", routes![index]).launch();
}

実行

$ cargo run
   Compiling hello_rocket v0.1.0 (file:///Users/hoge/hello_rocket)
    Finished debug [unoptimized + debuginfo] target(s) in 1.16 secs
     Running `target/debug/hello_rocket`
🔧  Configured for development.
    => listening: localhost:8000
    => logging: Normal
🛰  Mounting '/':
    => GET /
🚀  Rocket has launched from http://localhost:8000...
GET /:
    => Matched: GET /
    => Outcome: Success
    => Response succeeded.
GET /favicon.ico:
    => Error: No matching routes for GET /favicon.ico.
    => Warning: Responding with 404 Not Found catcher.
    => Response succeeded.

ブラウザでhttp://localhost:8000にアクセスするとHello world!が表示されていることが確認できます。

ROCKET_ENV

ROCKET_ENV環境変数に値をしていすることで、動作環境に応じた制御が可能になります。

$ ROCKET_ENV=stage cargo run
🔧  Configured for staging.
    => listening: 0.0.0.0:80
    => logging: Normal
🛰  Mounting '/':
    => GET /
Error: Failed to start server.
thread 'main' panicked at 'Permission denied (os error 13)', /Users/hoge/.cargo/registry/src/github.com-1ecc6299db9ec823/rocket-0.1.4/src/rocket.rs:474
note: Run with `RUST_BACKTRACE=1` for a backtrace.

ROCKET_ENVにはdevelopment, staging, production が使えます。
上記ではstaging環境でのデフォルトListenポートが80のため権限なしでエラー終了してしまいます。
Rocket.tomlで動作を制御することができます。

Rocket.toml
[staging]
port = 8000

[production]
port = 8000

設定項目はドキュメントのrocket::configあたりに記載があります。

ルーティング

src/main.rs
#[get("/hello/<name>")]
fn hello(name: &str) -> String {
    format!("Hello, {}", name)
}

fn main() {
    rocket::ignite().mount("/", routes![index, hello]).launch();
}

mount()の第一引数に指定したパスにroutes!マクロで指定したメソッドをマウントする形でルーティングを実装します。上記の場合/hello/hogeにルーティングしますが、mount()の第一引数に"/super"を指定すると /hello/hogeは404となり、/super/hello/hoge等でルーティングされることになります。

テンプレート

Jinja2およびDjangoテンプレートベースのTeraか、Handlebarsを使うことができます。

私が慣れていることもあるので、Teraを使ってみます。

Cargo.toml
[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["tera_templates"]
#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_contrib;

use std::collections::HashMap;
use rocket_contrib::Template;

#[get("/hello/<name>")]
fn hello(name: &str) -> Template {
    let mut context = HashMap::new();
    context.insert("name", name);
    Template::render("hello", &context)
}

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

fn main() {
    rocket::ignite().mount("/", routes![index, hello]).launch();
}
templates/hello.tera
<p>Hello {{name}}</p>

/hello/<name>の際にテンプレート使った結果を返したいので、templates/hello.teraを作成します。

テンプレートファイルを格納するディレクトリはデフォルトでtemplates/ですが、Rocket.tomlから変更することが可能です。
ファイル名はルーティングのメソッド名と一致させておく必要があります。Teraを使いたい場合は拡張子を.teraに、Handlebarsを使いたい場合は拡張子を.hbsにします。詳しい説明はドキュメントのテンプレートに記載があります。

JSONを返す

serdeを使ってJSONを扱います。rocket_contrib::JSONで一段階ラップされています。

src/main.rs
#![feature(plugin, proc_macro)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_contrib;
extern crate serde_json;
#[macro_use]
extern crate serde_derive;

use rocket_contrib::JSON;

#[derive(Serialize, Deserialize)]
struct Todo {
    id: i32,
    title: String,
    body: String,
}

#[get("/todo/<id>")]
fn get_todo(id: i32) -> JSON<Todo> {
    JSON(Todo {
        id: id,
        title: "todo title 1".to_string(),
        body: "todo body 1".to_string(),
    })
}

fn main() {
    rocket::ignite().mount("/", routes![index, hello, get_todo]).catch(errors![not_found]).launch();
}

/todo/1にアクセスすると{"id":1,"title":"todo title 1","body":"todo body 1"}を取得することができます。

エラーハンドリング

src/main.rs
use rocket::Request;

#[error(404)]
fn not_found(req: &Request) -> String {
    format!("not found {}", req)
}

fn main() {
    rocket::ignite().mount("/", routes![index, hello]).catch(errors![not_found]).launch();
}

http://localhost:8000/hellにアクセスするとnot found GET /hellが表示されます。

データベースアクセス

https://github.com/SergioBenitez/Rocket/tree/master/examples/todo

上記にDiesel SQLiteを使った例があります。
DieselはPostgreSQLおよびSQLiteしかサポートしていませんが、上記のコード例を見る限りはrust-mysql-simpleを使ってデータベースアクセスすることはできそうな気がします。

DieselもそろそろMySQLサポートされるような気配もしますし。。。

最後に

駆け足でRocketについて説明しました。
豊富なコード例ドキュメントの充実具合テストライブラリ完備将来Websocketのサポートが予定されていたりして、今後Rustで使われる予感がしています。Rustを学ぶ第一歩としてRocketを使ってみてはいかがでしょうか。