2位じゃダメなんでしょうか?
私好きですよ、2位(*´ω`*)
「ばあちゃんに届け2ゲト」
https://jihu.blog.fc2.com/blog-entry-491.html
2位ってなんか応援したくなりますよね?_(:3」∠)_
というわけでRust界のWebフレームワーク2位をひた走るRocketさんを贔屓にしていきたいわけです。
Rocketももうじき0.5系が出そうです。というか公式のトップがすでにRocket0.5(のrc1)になってたりします。1位のactix webに続いてtokioベース化ということでasync/awaitが使えるはずなので軽く試してみたのですが、あれ?途中までいい感じだったんだけど、作り込んでいるうちに非同期処理が何かとバッティングしたぞ(´・ω:;.:...
Rocketでasync/awaitの話はまた今度に置いておいて、今回はRust0.5系を使ってみたメモでAPI鯖の立てかたあたり書いてみます。まだrc-1なのでリリースまでに結構変わる可能性がありますが……
Rocket0.5でWebサーバを立てる
今回の環境
Rust 1.57
Rocket 0.5.0-rc1
とにもかくにも公式
Getting Started
https://rocket.rs/v0.5-rc/guide/getting-started/#getting-started
プロジェクトを作ってtomlを修正する
cargo initでプロジェクトを作ったら、まずはtomlのdependenciesを修正します。
[dependencies]
rocket = { version="0.5.0-rc.1", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
Rocketのバージョンは0.5系、それでもってこれが大切なのですが今回はAPI鯖の構築を目標とするのでfeaturesにjsonを突っ込んでおきます。0.4系だとjsonはrocket-contrib経由だったみたいですが0.5系で変更されているようです。
あとはみんな大好きserde。Jsonを使うときはコレ。serde自体はrocketが依存して取り込んでいるようなのですが、deriveが使えない_(:3」∠)_
というわけでバージョンに気をつけて同じバージョンのものをdependenciesに入れておきます。もちろんfeaturesにderiveを追加するのを忘れずに。
※なお依存で入ったserdeのバージョン確認方法です
cargo tree -i serde
main.rsを書き換える
Getting Startedの内容をそのままコピペします。が!
私はextern crateよりuse派なのでuseに書き換えちゃいます。
- #[macro_use] extern crate rocket;
+ use rocket::*;
これでcargo runすれば127.0.0.1:8000でWebサーバが立ち上がります。
[オプション]cargo-watchの導入
コードを書き換えるたびにctrl-cしてcargo runするのは流石に面倒なので、サーバを書くときはcargo-watchあたり導入すると楽だと思います。
cargo install cargo-watch
入れておけばcargo runの代わりに
cargo-watch -x run
ってしておけば、コードを保存するたびに勝手に現行プロセス終了して新たにcargo runしてくれます。
RESTっぽくしていく
API鯖と言えばやっぱりREST。別にGraphQLでもProtocolBuffersでもなんでも使えばいいと思いますが、JSONでやり取りするのが一番デバッグがしやすくて楽ちんです(*´ω`*)
Jsonを返すようにする
indexのhello world!をJsonにして返すようにしてみます。
// 応答Json用の構造体を作ります。Jsonに変換するので#[derive(Serialize)]を付けます。
#[derive(Serialize)]
struct IndexResult {
message: String,
}
// 返り値のhttp::StatusをなくしてJsonだけにしてもいい。今回はサンプルなので冗長に書いてます。
// Jsonが返り値に含まれるとレスポンスのcontent-typeは自動でapplication/jsonになります。
#[get("/")]
fn index() -> (http::Status, Json<IndexResult>) {
let data = IndexResult {
message: format!("hello world!"),
};
(http::Status::Ok, Json(data))
}
これが成功の基本形で、postを受け取ったときはhttp::Status::Createdを返すようにしたりできます。
ここまでをcargo runしてブラウザでアクセスすると、hello world!がJSONで返ります。
ヘッダもちゃんとapplication/jsonです。
パラメータを受け取る
公式
https://api.rocket.rs/master/rocket/attr.post.html#typing-requirements
アトリビュートの部分で<>で囲ってパラメータ名を設定するというRESTあるあるな記述です。実際に使用する値は関数の引数で、パラメータ名と同じ名前にして受け取ります。
#[get("/<id>")]
fn index(id: i32) -> (http::Status, Json<IndexResult>) {
エラーもJsonで返すようにする?
あんまり需要ないかな? でもJsonで欲しいーって時もあるよね?
エラーレスポンス周りは0.4系と変わってない感じですね。一応公式貼っておきます。
https://rocket.rs/v0.5-rc/guide/requests/#error-catchers
ではおもむろに404 NotFoundを見てみましょう。適当なページにアクセスしてみます。
http://127.0.0.1:8000/ore_ore_oredayo_omae_daredayo
普通にWebの404のページが表示されました。ではコレをJsonで返すようにしてみましょう。
エラー処理用関数を用意する
まずは返り値となるJson用の構造体を用意します。messageに加え追加情報を返すdebugフィールドを追加しています。
#[derive(Serialize)]
struct NotFound {
message: String,
debug: String,
}
続いて#[catch(エラーコード)]
を付けた関数を用意します。引数は&Request
で、返り値はJson
です。引数でステータスコードも拾えますが、catchのところでステータスコードを書いているのに引数でもStatusコードとな?
たぶんdefault用なんでしょう。普段は&Reauestのみでいいと思います。
#[catch(404)]
fn not_found(req: &Request) -> Json<NotFound> {
Json(NotFound {
message: format!("Not found"),
debug: format!("{}", req.uri()),
})
}
中は単に構造体に値を突っ込んでJsonで返しているだけです。
エラー処理関数を登録する
エラー処理関数は書いただけだと実行されません。Rocketのlaunch時に.register()へcatchers!で登録する必要があります。
fn rocket() -> _ {
rocket::build()
.mount("/", routes![index])
+ .register("/", catchers![not_found])
}
これで404NotFoundのときもJsonが返るようになります。
すべてのエラーをJsonで返す
すべてのやり取りをJsonにしたいーって気持ちありますよね? ありません?
#[catch(404)]
の404のところをdefault
ってすれば全部のエラーを処理できます。引数でStatusを拾うようにすれば、なんのエラーだったかが分かります。
#[catch(default)]
fn all_error(status: http::Status, req: &Request) -> Json<NotFound> {
Json(NotFound {
message: format!("{}", status),
debug: format!("{}", req.uri()),
})
}
POST/PUT/DELETEを用意する
GETとエラーがJsonで応答するようになったので、残りのAPIも返せるようにしていきます。
基本的には#[get]のところが#[post]/#[put]/#[delete]になるだけで、それぞれ専用の関数を作って、rocketのbuild時にroutes!へ登録する形になります。
#[post("/")]
fn post_index() -> (http::Status, Json<IndexResult>) {
let data = IndexResult {
message: format!("hello post!"),
};
(http::Status::Ok, Json(data))
}
rocket::build()
- .mount("/", routes![index])
+ .mount("/", routes![index, post_index])
.register("/", catchers![all_error])
データを受け取る
基本的には構造体を作ってJsonデータを受け取る流れになります。構造体にはderive(Deserialize)を付けてJson文字列からデータに変換できるようにしておきます。
- use rocket::serde::{Serialize, json::Json};
+ use rocket::serde::{Serialize, Deserialize, json::Json};
#[derive(Deserialize)]
struct PostData {
message: String,
}
データはアトリビュート部で名前を付けます。関数の引数でその名前を引数にして受け取ります。
#[post("/", data = "<post_data>")]
fn post_index(post_data: Json<PostData>) -> (http::Status, Json<IndexResult>) {
[オプション]文字列で受け取って内部で後からデシリアライズしたいときもあるよね
単に文字列で受け取るだけなら構造体は必要なく、postアトリビュートで名前をつけて関数の引数でStringで受け取ることができます。
#[post("/", data = "<post_data>")]
fn post_index(post_data: String) -> (http::Status, Json<IndexResult>) {
この場合、どこかで構造体を用意してjsonのfrom_strでデシリアライズすることができます。
- use rocket::serde::{Serialize, Deserialize, json::Json};
+ use rocket::serde::{Serialize, Deserialize, json::Json, json};
if let Ok(val) = json::from_str::<PostData>(&post_data){
}
今回はこのあたりで
( ´Д`)ノ~