Rust Rocket v0.4.0 の入門

この記事はRustその2 Advent Calendar 2018の23日目の記事です。

Rust の Web フレームワークである Rocket に入門していきます。

この記事では、公式のサンプルを参照しながら、それを解説していきます。

執筆環境

$ rustc --version
rustc 1.33.0-nightly (2d3e909e4 2018-12-22)

インストール

Rust のインストール

公式のインストール手順に従って下さい。

執筆環境と同等の環境を用意するには、以下のようにします。

$ rustup toolchain install nightly-2018-12-23
$ rustup default nightly-2018-12-23

リポジトリのダウンロード

公式のサンプルは、リポジトリに含まれています。 clone してきましょう。

$ git clone -b v0.4.0 https://github.com/SergioBenitez/Rocket.git
$ cd Rocket/examples

準備ができました。以下でサンプルを見ていきます。 以下では、各章開始時点では examples ディレクトリ直下に居る前提で話を進めます。

hello_world

概要

何はともあれhello worldからやっていきましょう。

$ cd hello_world
$ cargo run

難なく起動してくれるはずです。 この状態で、 localhost:8000 にアクセスすれば、 Hello, world! とだけ表示されたページが出てきます。

ソース

src/main.rs を見て下さい。 書いてある重要な内容は、大きく2つです。

ハンドラ

1つは、ハンドラの定義です。

hello_world では、ルートパスにアクセスした際に、 "Hello, world" が返されました。 この処理を定義する記述が以下になります。

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

Rocketでは、カスタム属性(Custom Attribute) を使います。 #[get("/")] で、この関数が パス / へのGETリクエストに対するハンドラであると示しています。 get 以外にも、 put, post, delete, head, patch, options が利用でき、それぞれ HTTP のリクエストメソッドと対応しています。

ハンドラ自体は、引数なし、文字列を返すだけの関数となっています。 この戻り値がそのままレンダリングされていたわけです。 ハンドラの引数が何の役目を果たすのか、戻り値の型は単純な文字列以外には何が指定できるのか、などは、別のサンプルで解説されています。

マウント、起動

さて、やっていることのもうひとつは、マウントとアプリケーションの起動です。

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

rocket::ignite() で、アプリケーションを作成します。 ここに、なんやかんや設定をして、最後に launch() すると、アプリケーションが開始され、アクセスできるようになります。

hello_world で設定しているのは、ルーティングがひとつだけです。 mount("/", routes![hello]) が、hello関数をルート直下にマウントしています。

hello_person

概要

この例では、パスから動的にパラメータを取得します。

$ cd hello_person
$ cargo run

以下のページにアクセスできます。

  • localhost:8000/hello/foo
    • foo とだけ描画されます
    • foo を別の文字に変えると、描画される内容も変わります
    • foo に何も入れないと Not Found になります
  • localhost:8000/hello/foo/100
    • Hello, 100 year old named foo! と描画されます
    • 100 を別の数字に変えると、描画される内容の 100 の部分が変化します
    • 100 に数字以外(abc, 1+1 など)を入れると Not Found になります
  • それ以外は Not Found です

ソース

src/main.rs に全て書かれています。

hello_world からは、以下の点が変更されています。

  • ハンドラが2つになり、それぞれの内容が変更されています
  • ルーティングにハンドラが2つ指定されています

ルーティング

まずは、ルーティングに関して確認していきます。

    rocket::ignite().mount("/", routes![hello, hi]).launch();

routes! マクロの中にハンドラを与えていますが、これは複数並べることができます。

また、 "/" を "/yo"に変更し、起動してみましょう。

localhost:8000/hello/foo/100 へはアクセスできなくなり、 localhost:8000/yo/hello/foo/100 へアクセスできるようになりました。 同時に、 localhost:8000/hello/foo へはアクセスできなくなり、 localhost:8000/yo/hello/foo へアクセスできるようになっています。

このように、 mount() の第一引数でルーティングをディレクトリ的単位でまとめて制御することができます。

ハンドラの引数とパラメータ

ハンドラが2つありますが、 hello() だけ確認します。

#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

カスタム属性のパスに、 <name><age> が現れました。 対応するように、 hello() の引数に、 nameage が現れました。 これらが、パスからパラメータを受け取るための記述になります。

<name> の位置に対応したものが、引数 name に与えられ、 <age> の位置に対応したものが、引数 age に与えられ、関数が呼ばれます。

特に、 age は自動的に数値型にパースされます。 では、数値として読み込めなかった時はどうなるかというと、概要で示したように Not Found になります。

パースのできる/できないはどのようにして決定するのでしょうか?そもそも、パースできる型はどのように決まっているのでしょうか? 鍵は、 FromParam トレイトにあります。

FromParamトレイトは、プリミティブの数値型、 String&RawStrOption<T>Result<T, T::Error> などに実装されています。 パス文字列から各型への変換は、 from_param() によって行われます。

この変換が成功した場合、ハンドラが実行されます。

補足としては、以下のとおりです。

  • &RawStr : 生のパス文字列そのものを表す型で、変換等しないのでマッチは必ず成功します
  • String : 生のパス文字列から文字列に変換します。失敗する場合があります
  • Option<T> : 変換が成功した時は Some(T) 、失敗したときは None を返します。マッチは必ず成功します
  • Result<T, T::Error> : 変換が成功した時は Ok(T) 、失敗した時は Err(T::Error) が返ります。マッチは必ず成功します

tera_templates

概要

hello_world、hello_personでは、レスポンスは全て単純な文字列で返してきましたが、 Rocket ではテンプレートエンジンを利用してレスポンスを作成できます。

Rocketで利用できるのは、 TeraHandlebars です。ここでは Tera を紹介します。

$ cd tera_templates
$ cargo run

起動して、 localhost:8000/hello/foo にアクセスしてみましょう。HTML文章が表示されると思います。

ソース

src/main.rs からみていきます。

        .attach(Template::fairing())

テンプレートを利用するには、 Template::fairing() をアタッチしておく必要があります。

#[get("/hello/<name>")]
fn get(name: String) -> Template {
    let context = TemplateContext { name, items: vec!["One", "Two", "Three"] };
    Template::render("index", &context)
}

ハンドラの戻り値が Template となっています。 Template::render() を返すと、テンプレートの内容をレンダリングしてくれます。

render() の引数には、テンプレートの名前と、テンプレートに渡すコンテキストを指定します。 テンプレートの名前は、 "index" を指定しているので、 templates/index.tera が解釈されます。

コンテキストには、Serde でシリアライズ可能なものを指定できます。 この例では、自前で以下のように定義しています。

#[derive(Serialize)]
struct TemplateContext {
    name: String,
    items: Vec<&'static str>
}

さて、 templates/index.tera の内容を確認していきます。

    <h1>Hi {{name}}</h1>

コンテキストとして渡した、 name が使用されています。

        {% for s in items %}
            <li>{{ s }}</li>
        {% endfor %}

ここでは items の内容をループで表示しています。


さいごに

今回は、公式のexamplesの中から基礎の基礎である hello_world 、 動的パスを扱った hello_person 、 レスポンスをリッチにする tera_template の3つを紹介しました。

計画的に執筆していなかったため、あまり紹介できませんでした。 examples は30個位上あるので、余裕のある際に追加していければと思います。