Rocketは素晴らしいWebアプリケーションフレームワークで、サンプルコードも充実しているけど、WebAPIサーバーを作る包括的なチュートリアルが無かったように思えたので書いてみることにする。なお、このリポジトリコードはこの記事を参考にしている。
この解説に使ったコードは以下。
Table of Contents
Setup
# rustupをインストールする.
$ curl https://sh.rustup.rs -sSf | /bin/bash -s -- -y --default-toolchain nightly
Run
$ cd rocket-webapi
$ cargo run
🔧 Configured for development.
=> address: localhost
=> port: 8000
=> log: normal
=> workers: 8
=> secret key: generated
=> limits: forms = 32KiB
=> tls: disabled
🛰 Mounting '/':
=> GET /
=> GET /todos
=> GET /todos/<todoid>
🚀 Rocket has launched from http://localhost:8000
Usage
-
Hello, world!
http://localhost:8000
-
Get all ToDOs
http://localhost:8000/todos
-
Get ToDO by ID
http://localhost:8000/todos/10
Tutorial
なぜRustなのか?
Rustは型安全でゼロコスト抽象化を実現したシステムプログラミング言語だ。巷ではC言語の代替と言われることもあるが、実際使ってみるとより安全なC++としての趣きが強いと思う。筆者は10年以上C++でMMOゲームや金融系のハイパフォーマンスサーバーを書いてきて、C++17とかModern CMakeとか、Rangeとかテンプレートプログラミングとかを色々追ってきたが、Rustに出会ってからC++の最新を追うのをやめた。Rustは例えるなら(まだない)C++23に安全性とSaneなメタプロシステムと最高のビルドシステムと最高のパッケージマネージャが付いてきた、みたいな感じ。それほどまでにRustは素晴らしい機能と言語としての表現力を持っている(と思う)。
Rocketとは?
RocketはRustで書かれたタイプセーフなマイクロウェブアプリケーションフレームワークで、PythonのFlaskに似ている感じ。それでは、シンプルなTODOアプリのWebAPIサーバーを作ってみて、Rocketの使い方を解説してみる。
Hello Rocket!
まずは、Hello, worldを返すだけのアプリを作ってみる。
-
プロジェクト作成
$ cargo new rocket-webapi $ cd rocket-webapi
-
rust-toolchain
nightly
-
Cargo.toml
[package] name = "rocket-jsonapi" version = "0.1.0" [dependencies] rocket = "0.4" rocket_contrib = { version = "0.4", features = ["json"] }
-
src/main.rs
#![feature(proc_macro_hygiene)] #![feature(decl_macro)] #[macro_use] extern crate rocket; /// GETがきたときに"Hello, world!"というレスポンスを返す #[get("/")] fn index() -> &'static str { "Hello, world!" } fn main() { rocket::ignite() .mount("/", routes![index]) // ここにルーティングをセットする .launch(); }
実行してみる。
$ cargo run
🔧 Configured for development.
=> address: localhost
=> port: 8000
=> log: normal
=> workers: 8
=> secret key: generated
=> limits: forms = 32KiB
=> tls: disabled
🛰 Mounting '/':
=> GET /
🚀 Rocket has launched from http://localhost:8000
$ curl http://localhost:8000
Hello, world!
動いた! \(^o^)/
ここでコンパイルエラーが出た人は、以下を試してみてほしい
# Rust toolchainを更新する
rustup update
ToDOアプリのWebAPIをつくる
次にいきおいでToDoのWebAPIを作ってみる。
-
Cargo.toml
[package] name = "rocket-webapi" version = "0.1.0" [dependencies] rocket = "0.4" rocket_contrib = { version = "0.4", features = ["json"] } # serdeのcrateを追加する serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.0"
-
main.rs
#![feature(proc_macro_hygiene)] #![feature(decl_macro)] #[macro_use] extern crate rocket; mod models; mod routes; // WebAPIのURLルーティングはroutes.rsに移動する use routes::*; fn main() { rocket::ignite() .mount("/", routes![index, todos, new_todo, todo_by_id]) .launch(); }
-
routes.rs
// JSONを返すのに必要 use rocket_contrib::json::Json; use crate::models::ToDo; #[get("/")] pub fn index() -> &'static str { "Hello, world!" } /// TODOリストを返す。 /// Jsonの型がResponderをimplしているので、JSON文字列を返すことができる #[get("/todos")] pub fn todos() -> Json<Vec<ToDo>> { Json(vec![ToDo { id: 1, title: "Read Rocket tutorial".into(), description: "Read https://rocket.rs/guide/quickstart/".into(), done: false, }]) } /// 新しいTODOを作成する /// POSTの時はこうする #[post("/todos", data = "<todo>")] pub fn new_todo(todo: Json<ToDo>) -> String { format!("Accepted post request! {:?}", todo.0) } /// TODOを取得する #[get("/todos/<todoid>")] pub fn todo_by_id(todoid: u32) -> String { let todo = ToDo { id: 1, title: "Read Rocket tutorial".into(), description: "Read https://rocket.rs/guide/quickstart/".into(), done: false, }; format!("{:?}", todo) }
-
models.rs
use serde::{Deserialize, Serialize}; /// TODOのモデルはmodels.rsに定義 #[derive(Debug, Serialize, Deserialize)] pub struct ToDo { pub id: u32, pub title: String, pub description: String, pub done: bool, }
実行してみる。
$ cargo run
🔧 Configured for development.
=> address: localhost
=> port: 8000
=> log: normal
=> workers: 8
=> secret key: generated
=> limits: forms = 32KiB
=> tls: disabled
🛰 Mounting '/':
=> GET /
=> GET /todos
=> POST /todos
=> GET /todos/<todoid>
🚀 Rocket has launched from http://localhost:8000
curlでリクエストしてみる。
$ curl -i http://localhost:8000/todos
HTTP/1.1 200 OK
Content-Type: application/json
Server: Rocket
Content-Length: 111
Date: Wed, 04 Jul 2018 13:44:50 GMT
[{"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}]
OK, いい感じ。
次、POST。
$ curl -i -H "Content-Type: application/json" -X POST -d '{"id": 100, "title":"Read this book", "description": "http://shop.oreilly.com/product/0636920040385.do", "done": false}' http://localhost:20000/todos
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Server: Rocket
Content-Length: 142
Date: Thu, 05 Jul 2018 03:55:22 GMT
Accepted post request! ToDo { id: 100, title: "Read this book", description: "http://shop.oreilly.com/product/0636920040385.do", done: false }
POSTもOK。
Responder
上記の例でJson型の戻り値を返すとJSONの文字列がレスポンスとして返った。この仕組みを実現いているのがResponderトレイトだ。Rocketのルーティングの関数はすべてResponderトレイトをimplしなければならない。
難しそうい聞こえるが、実際にはRocketがいろいろな型のResponderトレイトをあらかじめimplしといてくれるので、自分でimplする場面は意外に少ないかもしれない。以下に主なResponderのimplを示す。
型 | レスポンス |
---|---|
&'static str, &str, String | text/plainの文字列が返る |
NamedFile | ファイルの文字列が返る |
Redirect | 別のURLにリダイレクトする |
Stream | HTTPストリーミングレスポンスが返る |
Json | application/jsonのJSON文字列が返る |
Template | Templateをレンダリングした結果が返る |
rocket::response::statusにある型 | 例えばAcceptedの場合あ203 Acceptedになる |
Option | Some(T)の場合はTのResponder、Noneの場合は404 Not Foundになる |
Result | Ok(T)の場合はT、Err(E)の場合はUのResponderの結果が返る |
最後の3つはWrapping Responderと言われておりWrapした中身のResponderの結果を装飾したり、中身の型によって動きを動的に返る役割を持つ。
以下の例を見てみよう。
-
JSONを一つ返す
fn sample() -> Json<ToDo> { Json(ToDo { id: 1, title: "Read Rocket tutorial".into(), description: "Read https://rocket.rs/guide/quickstart/".into(), done: false, }) }
$ curl http://localhost:8000/todos {"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}
なるほど。
-
Vectorにしてみる
fn sample() -> Vec<Json<ToDo>> { Json(vec![ToDo { id: 1, title: "Read Rocket tutorial".into(), description: "Read https://rocket.rs/guide/quickstart/".into(), done: false, }]) }
$ curl http://localhost:8000/todos [{"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}]
JSONがArrayになった。おぉいいね。
-
rocket::response::status::Acceptedでwrapすると
use rocket::response::status::Accepted; fn sample() -> Accepted<Vec<Json<ToDo>>> { Accepted(Some(Json(vec![ToDo { id: 1, title: "Read Rocket tutorial".into(), description: "Read https://rocket.rs/guide/quickstart/".into(), done: false, }]))) }
$ curl -i http://localhost:8000/todos HTTP/1.1 202 Accepted Content-Type: application/json Server: Rocket Content-Length: 111 Date: Fri, 06 Jul 2018 15:32:19 GMT [{"id":1,"title":"Read Rocket tutorial","description":"Read https://rocket.rs/guide/quickstart/","done":false}]
Status codeが202 Acceptedになる。
スバラシイ。
長くなったのでここまで。次回以降は以下のいずれかを解説したい。
- パラメータのパース、バリデーション
- Configuration
- エラー処理
- データベース
- ロギング