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

## はじめに

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


## 前提条件

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


## Hello Rocket

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

```shell-session
$ cargo new --bin hello_rocket
$ cd hello_rocket
$ rustup override set nightly
```


```toml:Cargo.toml
[dependencies]
rocket = "0.1.4"
rocket_codegen = "0.1.4"
```

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

```rust: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();
}
```

実行

```shell-session
$ 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`環境変数に値をしていすることで、動作環境に応じた制御が可能になります。

```shell-session
$ 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`で動作を制御することができます。

```toml:Rocket.toml
[staging]
port = 8000

[production]
port = 8000
```

設定項目は[ドキュメントのrocket::configあたりに記載](https://api.rocket.rs/rocket/config/)があります。


## ルーティング

```rust: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](https://github.com/Keats/tera)か、[Handlebars](https://github.com/sunng87/handlebars-rust)を使うことができます。

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


```toml: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();
}
```


```jinja:templates/hello.tera
<p>Hello {{name}}</p>
```

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

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

## JSONを返す

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

```rust: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"}`を取得することができます。

## エラーハンドリング

```rust: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](http://diesel.rs/) SQLiteを使った例があります。
DieselはPostgreSQLおよびSQLiteしかサポートしていませんが、上記のコード例を見る限りは[rust-mysql-simple](https://github.com/blackbeam/rust-mysql-simple)を使ってデータベースアクセスすることはできそうな気がします。

[DieselもそろそろMySQLサポートされるような気配もしますし。。。](https://github.com/diesel-rs/diesel/issues/514)

## 最後に

駆け足でRocketについて説明しました。
[豊富なコード例](https://github.com/SergioBenitez/Rocket/tree/master/examples)や[ドキュメントの充実具合](https://rocket.rs/guide/)、[テストライブラリ完備](https://rocket.rs/guide/testing/)、[将来Websocketのサポートが予定されていたり](https://github.com/SergioBenitez/Rocket/issues/90)して、今後Rustで使われる予感がしています。Rustを学ぶ第一歩としてRocketを使ってみてはいかがでしょうか。