Rust+Rocket-0.5系でAPI鯖を立てるよメモ

#２位じゃダメなんでしょうか？
私好きですよ、２位(\*´ω｀\*)

「ばあちゃんに届け２ゲト」
https://jihu.blog.fc2.com/blog-entry-491.html

２位ってなんか応援したくなりますよね？\_(:3」∠)\_
というわけでRust界のWebフレームワーク２位をひた走るRocketさんを贔屓にしていきたいわけです。

Rocketももうじき0.5系が出そうです。というか公式のトップがすでにRocket0.5(のrc1)になってたりします。１位の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を修正します。

```toml
[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のバージョン確認方法です

```shell
cargo tree -i serde
```

## main.rsを書き換える
Getting Startedの内容をそのままコピペします。が！
私はextern crateよりuse派なのでuseに書き換えちゃいます。

```diff
- #[macro_use] extern crate rocket;
+ use rocket::*;
```
これでcargo runすれば127.0.0.1:8000でWebサーバが立ち上がります。

## [オプション]cargo-watchの導入
コードを書き換えるたびにctrl-cしてcargo runするのは流石に面倒なので、サーバを書くときはcargo-watchあたり導入すると楽だと思います。

```shell
cargo install cargo-watch
```

入れておけばcargo runの代わりに

```shell
cargo-watch -x run
```

ってしておけば、コードを保存するたびに勝手に現行プロセス終了して新たにcargo runしてくれます。

# RESTっぽくしていく

API鯖と言えばやっぱりREST。別にGraphQLでもProtocolBuffersでもなんでも使えばいいと思いますが、JSONでやり取りするのが一番デバッグがしやすくて楽ちんです(\*´ω｀\*)

## Jsonを返すようにする

indexのhello world!をJsonにして返すようにしてみます。

```rust
// 応答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で返ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/64e5ee51-addb-3170-9fc2-5c6739f84f61.png)

ヘッダもちゃんとapplication/jsonです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/d891289f-562f-37a0-ef01-e7dbdbe1f052.png)

## パラメータを受け取る

公式
https://api.rocket.rs/master/rocket/attr.post.html#typing-requirements

アトリビュートの部分で<>で囲ってパラメータ名を設定するというRESTあるあるな記述です。実際に使用する値は関数の引数で、パラメータ名と同じ名前にして受け取ります。

```rust
#[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
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/21b91793-a830-7a1f-df56-ca5f50404846.png)

普通にWebの404のページが表示されました。ではコレをJsonで返すようにしてみましょう。

### エラー処理用関数を用意する

まずは返り値となるJson用の構造体を用意します。messageに加え追加情報を返すdebugフィールドを追加しています。

```rust
#[derive(Serialize)]
struct NotFound {
    message: String,
    debug: String,
}
```

続いて`#[catch(エラーコード)]`を付けた関数を用意します。引数は`&Request`で、返り値は`Json`です。引数でステータスコードも拾えますが、catchのところでステータスコードを書いているのに引数でもStatusコードとな？

たぶんdefault用なんでしょう。普段は&Reauestのみでいいと思います。

```rust
#[catch(404)]
fn not_found(req: &Request) -> Json<NotFound> {
    Json(NotFound {
        message: format!("Not found"),
        debug: format!("{}", req.uri()),
    })
}
```

中は単に構造体に値を突っ込んでJsonで返しているだけです。

### エラー処理関数を登録する

エラー処理関数は書いただけだと実行されません。Rocketのlaunch時に.register()へcatchers!で登録する必要があります。

```diff
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![index])
+       .register("/", catchers![not_found])
}
```

これで404NotFoundのときもJsonが返るようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140718/ed84fed3-ee36-58f4-2785-43ef0d4a4659.png)

### すべてのエラーをJsonで返す

すべてのやり取りをJsonにしたいーって気持ちありますよね？　ありません？

`#[catch(404)]`の404のところを`default`ってすれば全部のエラーを処理できます。引数でStatusを拾うようにすれば、なんのエラーだったかが分かります。

```rust
#[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!へ登録する形になります。

```rust
#[post("/")]
fn post_index() -> (http::Status, Json<IndexResult>) {
    let data = IndexResult {
        message: format!("hello post!"),
    };
    (http::Status::Ok, Json(data))
}
```

```diff
    rocket::build()
-        .mount("/", routes![index])
+        .mount("/", routes![index, post_index])
         .register("/", catchers![all_error])
```

## データを受け取る

基本的には構造体を作ってJsonデータを受け取る流れになります。構造体にはderive(Deserialize)を付けてJson文字列からデータに変換できるようにしておきます。

```diff
- use rocket::serde::{Serialize, json::Json};
+ use rocket::serde::{Serialize, Deserialize, json::Json};
```

```rust
#[derive(Deserialize)]
struct PostData {
    message: String,
}
```

データはアトリビュート部で名前を付けます。関数の引数でその名前を引数にして受け取ります。

```rust
#[post("/", data = "<post_data>")]
fn post_index(post_data: Json<PostData>) -> (http::Status, Json<IndexResult>) {
```

###[オプション]文字列で受け取って内部で後からデシリアライズしたいときもあるよね

単に文字列で受け取るだけなら構造体は必要なく、postアトリビュートで名前をつけて関数の引数でStringで受け取ることができます。

```rust
#[post("/", data = "<post_data>")]
fn post_index(post_data: String) -> (http::Status, Json<IndexResult>) {
```

この場合、どこかで構造体を用意してjsonのfrom_strでデシリアライズすることができます。

```diff
- use rocket::serde::{Serialize, Deserialize, json::Json};
+ use rocket::serde::{Serialize, Deserialize, json::Json, json};
```

```rust
    if let Ok(val) = json::from_str::<PostData>(&post_data){
    }
```

#今回はこのあたりで
( ´Д｀)ﾉ~