[Rust] warpを使ってみる

今までwebフレームワークは [actix/actix-web](https://github.com/actix/actix-web) にお世話になってきたんですが
最近は [seanmonstar/warp](https://github.com/seanmonstar/warp) をよく見かけるようになってきたので使ってみようと思いたってやってみたメモ。

# 環境

- OS: Ubuntu 20.04
- DockerCompose: 1.27.4
- Rust: 1.49.0

```toml:Cargo.toml
[package]
name = "rust_warp_examples"
version = "0.1.0"
authors = ["yagince <xxxx@gmail.com>"]
edition = "2018"

[dependencies]
anyhow = "=1.0.36"
thiserror = "=1.0.23"
warp = "=0.3.0"
tokio = { version = "1", features = ["full"] }
env_logger = "=0.8.2"
serde_derive = "=1.0.118"
serde = "=1.0.118"
serde_json = "=1.0.61"

[dev-dependencies]
assert-json-diff = "1.1.0"
```

# 実行するようのdocker-compose.ymlを用意

```Dockerfile
FROM rust:1.49.0-slim-buster

ENV CARGO_TARGET_DIR=/tmp/target \
    DEBIAN_FRONTEND=noninteractive \
    LC_CTYPE=ja_JP.utf8 \
    LANG=ja_JP.utf8

RUN apt-get update \
  && apt-get install -y -q \
     ca-certificates \
     locales \
     apt-transport-https\
     libssl-dev \
     pkg-config \
     curl \
     build-essential \
  && echo "ja_JP UTF-8" > /etc/locale.gen \
  && locale-gen \
  \
  && echo "install rust tools" \
  && rustup component add rustfmt \
  && cargo install cargo-watch cargo-make

WORKDIR /app

CMD ["cargo", "run"]
```

```yaml:docker-compose.yml
version: "3.7"

services:
  app:
    build:
      context: .
    container_name: app
    working_dir: /app
    command: cargo watch -x run
    tty: true
    volumes:
      - ./:/app
    ports:
      - 3000:3000
```

今回はまだDBとか使わないのでシンプル

# まずはシンプルにhello worldしてみる

まずはREADMEに書いてあるExampleを書いてみる

```rust:main.rs
use warp::Filter;

#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String).map(|name| format!("Hello, {}!", name));

    warp::serve(hello).run(([0, 0, 0, 0], 3000)).await;
}
```

```shell
docker-compose up -d
```

で起動

```shell
❯ curl -v http://localhost:3000/hello/warp
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /hello/warp HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.73.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain; charset=utf-8
< content-length: 12
< date: Thu, 07 Jan 2021 13:12:23 GMT
<
* Connection #0 to host localhost left intact
Hello, warp!%
```

うん、とりあえず動いた

# ログを出したい

アクセスログが出たほうが動作確認しやすいので、ログを出したいですね

https://github.com/seanmonstar/warp/blob/48cf25c629/examples/todos.rs#L15-L31

この辺を真似てみます。

```rust:main.rs
use warp::Filter;

#[tokio::main]
async fn main() {
    std::env::set_var("RUST_LOG", "example");
    env_logger::init();

    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String).map(|name| format!("Hello, {}!", name));

    let routes = hello.with(warp::log("example"));

    warp::serve(routes).run(([0, 0, 0, 0], 3000)).await;
}
```

```
[2021-01-07T13:22:19Z INFO  warp::filters::log] 192.168.16.1:58474 "GET /hello/warp HTTP/1.1" 200 "-" "curl/7.73.0" 84.539µs
[2021-01-07T13:22:20Z INFO  warp::filters::log] 192.168.16.1:58478 "GET /hello/warp HTTP/1.1" 200 "-" "curl/7.73.0" 81.013µs
```

こんな感じで出るようになりました。

※ `warp::log` で指定した名前が `RUST_LOG` で設定されていないと出ません。

# filterとhandlerを真似てみる

https://github.com/seanmonstar/warp/blob/48cf25c629/examples/todos.rs

このexampleが一番実際なにかアプリケーションを作った場合に近い気がするので、真似てみます。

```rust:main.rs
use warp::Filter;

pub mod filters;
pub mod handlers;

#[tokio::main]
async fn main() {
    std::env::set_var("RUST_LOG", "example");
    env_logger::init();

    let hello = filters::hello();

    let routes = hello.with(warp::log("example"));

    warp::serve(routes).run(([0, 0, 0, 0], 3000)).await;
}
```

```rust:handlers.rs
use std::convert::Infallible;

pub async fn hello(name: String) -> Result<impl warp::Reply, Infallible> {
    Ok(format!("Hello, {}!", name))
}
```

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}
```

```shell
❯ curl -v http://localhost:3000/hello/warp
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /hello/warp HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.73.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain; charset=utf-8
< content-length: 12
< date: Thu, 07 Jan 2021 13:42:14 GMT
<
* Connection #0 to host localhost left intact
Hello, warp!%
```

うん、動きました。

ちょっとハマったポイントとしては
`use warp::Filter;`
が必要だったってことですね。

```rust:filters.rs
use super::handlers;

pub fn hello() -> impl warp::Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}
```

こんな風に書くと

```
app    | error[E0599]: no method named `and` found for struct `warp::filter::and::And<warp::filter::and::And<warp::filter::and::And<impl warp::Filter+std::marker::Copy, Exact<warp::path::internal::Opaque<__StaticPath>>>, impl warp::Filter+std::marker::Copy>, impl warp::Filter+std::marker::Copy>` in the current scope
app    |  --> src/filters.rs:5:10
app    |   |
app    | 5 |         .and(warp::get())
app    |   |          ^^^ method not found in `warp::filter::and::And<warp::filter::and::And<warp::filter::and::And<impl warp::Filter+std::marker::Copy, Exact<warp::path::internal::Opaque<__StaticPath>>>, impl warp::Filter+std::marker::Copy>, impl warp::Filter+std::marker::Copy>`
app    |   |
app    |   = help: items from traits can only be used if the trait is in scope
app    |   = note: the following trait is implemented but not in scope; perhaps add a `use` for it:
app    |           `use warp::Filter;`
```

こんな風に怒られます。
ちゃんと `use warp::Filter;` ってエラーに書いてあるのですぐ気づくっちゃ気づくんですが。
おそらくwarp::Filterにtraitがいろいろあるんですね。
なるほど。

# テストを書いてみる

https://docs.rs/warp/0.2.5/warp/test/index.html

なるほど、filter毎に単体テストを書けるようなしくみがサポートされてるんですね。
良い( ・∀・)ｲｲ!!

```shell
docker-compose run --rm app bash
```

```shell
cargo watch -x test
```

先程のページにかかれてるサンプル通り書いてみる

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hello() {
        let filter = hello();

        let res = warp::test::request().path("/hello/warp").reply(&filter);

        assert_eq!(res.status(), 200);
    }
}
```

```shell
error[E0599]: no method named `status` found for opaque type `impl warp::Future` in the current scope
  --> src/filters.rs:20:24
   |
20 |         assert_eq!(res.status(), 200);
   |                        ^^^^^^ method not found in `impl warp::Future`
   |
help: consider `await`ing on the `Future` and calling the method on its `Output`
   |
20 |         assert_eq!(res.await.status(), 200);
   |                        ^^^^^^
```

おぉ、なるほど。Futureが返ってくるからawaitしないとイケナイらしい。

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_hello() {
        let filter = hello();

        let res = warp::test::request()
            .path("/hello/warp")
            .reply(&filter)
            .await;

        assert_eq!(res.status(), 200);
    }
}
```

```shell
running 1 test
test filters::tests::test_hello ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

うん、通りましたね。

`res.body` も見てみます。

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_hello() {
        let filter = hello();

        let res = warp::test::request()
            .path("/hello/warp")
            .reply(&filter)
            .await;

        assert_eq!(res.status(), 200);
        assert_eq!(res.body(), "Hello, warp");
    }
}
```

```shell
running 1 test
test filters::tests::test_hello ... FAILED

failures:

---- filters::tests::test_hello stdout ----
thread 'filters::tests::test_hello' panicked at 'assertion failed: `(left == right)`
  left: `b"Hello, warp!"`,
 right: `"Hello, warp"`', src/filters.rs:24:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    filters::tests::test_hello

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
```

あぁ、なるほど、バイナリか。

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_hello() {
        let filter = hello();

        let res = warp::test::request()
            .path("/hello/warp")
            .reply(&filter)
            .await;

        assert_eq!(res.status(), 200);
        assert_eq!(res.body(), "Hello, warp!".as_bytes());
    }
}
```

```shell
running 1 test
test filters::tests::test_hello ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

パスしました。

# JSONを返してみる

## Handler

```rust:handlers.rs
use serde_derive::Serialize;
use std::convert::Infallible;

pub async fn hello(name: String) -> Result<impl warp::Reply, Infallible> {
    Ok(format!("Hello, {}!", name))
}

#[derive(Debug, Clone, PartialEq, Serialize)]
struct HelloJson {
    data: String,
}

pub async fn hello_json(name: String) -> Result<impl warp::Reply, Infallible> {
    Ok(warp::reply::json(&HelloJson {
        data: format!("Hello, {}!", name),
    }))
}
```

## Filter

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn api() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    hello().or(hello_json())
}

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}

pub fn hello_json() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / "json" / String)
        .and(warp::get())
        .and_then(handlers::hello_json)
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert_json_diff::assert_json_eq;
    use serde_json::json;

    #[tokio::test]
    async fn test_hello() {
        let filter = api();

        let res = warp::test::request()
            .path("/hello/warp")
            .reply(&filter)
            .await;

        assert_eq!(res.status(), 200);
        assert_eq!(res.body(), "Hello, warp!".as_bytes());
    }

    #[tokio::test]
    async fn test_hello_json() -> anyhow::Result<()> {
        let filter = api();

        let res = warp::test::request()
            .path("/hello/json/warp")
            .reply(&filter)
            .await;

        assert_eq!(res.status(), 200);

        assert_json_eq!(
            serde_json::from_slice::<serde_json::Value>(res.body())?,
            json!({ "data": "Hello, warp!"})
        );

        Ok(())
    }
}
```

- `hello_json` を追加
  - `warp::reply::json` で Serializeを実装した構造体を渡す
- `hello_json` のテストを追加
  - `hello` と `hello_json` を両方ハンドリングする `api` を追加
  - これをテスト対象にする
  - jsonの期待値は文字列で書くと辛いので、serde_json::jsonマクロにおまかせして、[assert-json-diff](https://crates.io/crates/assert-json-diff)を使ってassert

## Test

```
running 2 tests
test filters::tests::test_hello ... ok
test filters::tests::test_hello_json ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

通りましたね。
なるほど。

# `Infalliable` ってどんな型？

https://doc.rust-lang.org/std/convert/enum.Infallible.html

```rust
impl<T, U> TryFrom<U> for T where U: Into<T> {
    type Error = Infallible;

    fn try_from(value: U) -> Result<Self, Infallible> {
        Ok(U::into(value))  // Never returns `Err`
    }
}
```

なるほど
`Result` を返さなきゃいけないけど、絶対失敗しないので必ず `Ok` を返すような時の ResultのErrに使う型、ですかね？
将来的には `!` という `never` 型になると？
ふむ、なるほど。

# POSTでJSONデータを受け取ってみる

そろそろテスト駆動で行けそうなので、テストを先に書いてみる

## Test

```rust
...
    #[tokio::test]
    async fn test_receive_json() -> anyhow::Result<()> {
        let body = json!({ "data": "Hello, warp!"});
        let res = warp::test::request()
            .method("POST")
            .path("/receive/json")
            .body(body.to_string())
            .reply(&api())
            .await;

        assert_eq!(res.status(), 200);

        assert_json_eq!(
            serde_json::from_slice::<serde_json::Value>(res.body())?,
            body,
        );

        Ok(())
    }
...
```

- `{ "data": "..." }` っていうJSONを投げる
- 投げたJSONがそのまま返ってくる

```shell
...
running 3 tests
test filters::tests::test_hello ... ok
test filters::tests::test_receive_json ... FAILED
test filters::tests::test_hello_json ... ok

failures:

---- filters::tests::test_receive_json stdout ----
thread 'filters::tests::test_receive_json' panicked at 'assertion failed: `(left == right)`
  left: `404`,
 right: `200`', src/filters.rs:75:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    filters::tests::test_receive_json

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
...
```

当然まだ404ですね

## Filter

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn api() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    hello().or(hello_json()).or(receive_json()) // 追加
}

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / String)
        .and(warp::get())
        .and_then(handlers::hello)
}

pub fn hello_json() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("hello" / "json" / String)
        .and(warp::get())
        .and_then(handlers::hello_json)
}

// 追加
pub fn receive_json() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("receive" / "json")
        .and(warp::post())
        .and(warp::body::json())
        .and_then(handlers::receive_json)
}
...
```

- `receive_json` を追加
  - methodは `POST`
  - bodyは `JSON`

## Handler

```rust:handlers.rs
use serde_derive::{Deserialize, Serialize};
use std::convert::Infallible;

pub async fn hello(name: String) -> Result<impl warp::Reply, Infallible> {
    Ok(format!("Hello, {}!", name))
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HelloJson {
    data: String,
}

pub async fn hello_json(name: String) -> Result<impl warp::Reply, Infallible> {
    Ok(warp::reply::json(&HelloJson {
        data: format!("Hello, {}!", name),
    }))
}

pub async fn receive_json(body: HelloJson) -> Result<impl warp::Reply, Infallible> {
    Ok(warp::reply::json(&body))
}
```

- `HelloJson` をそのまま使う
  - 受け取ったbodyをマッピングする為に `Deserialize` を deriveに追加
  - 引数で受け取るのでpubに変更

## Test

```shell
running 3 tests
test filters::tests::test_hello ... ok
test filters::tests::test_hello_json ... ok
test filters::tests::test_receive_json ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

OK

## helloをDRYにしたい

- `hello/{param}`
- `hello/json/{param}`

は `hello` で始まるルーティングとしてまとめたい

```rust:filters.rs
use super::handlers;
use warp::Filter;

pub fn api() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    hello().or(receive_json())
}

pub fn hello() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path("hello").and(hello_string().or(hello_json()))
}

pub fn hello_string() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!(String)
        .and(warp::get())
        .and_then(handlers::hello)
}

pub fn hello_json() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    warp::path!("json" / String)
        .and(warp::get())
        .and_then(handlers::hello_json)
}
```

- `warp::path("hello")` の後につなげればいい
  - マクロではなく関数の方を使う

## bodyのDeserializeができない場合のレスポンスはどうなるのか

```rust
    #[tokio::test]
    async fn test_receive_json_invalid() -> anyhow::Result<()> {
        let body = json!({ "hoge": "foo"});

        let res = warp::test::request()
            .method("POST")
            .path("/receive/json")
            .body(body.to_string())
            .reply(&api())
            .await;

        assert_eq!(res.status(), 400);
        assert_eq!(
            res.body(),
            "Request body deserialize error: missing field `data` at line 1 column 14".as_bytes()
        );

        Ok(())
    }
```

こんな感じになるようです
serdeのエラーメッセージがBodyに入って返ってくるんですね。


# 認証Filter入れてみたい

Authorizationヘッダを見て認証してみたい

## Test

```rust
    #[tokio::test]
    async fn test_with_auth() -> anyhow::Result<()> {
        let body = json!({ "data": "foo"});

        let res = warp::test::request()
            .method("POST")
            .header("Authorization", "Bearer hogehoge")
            .path("/with_auth/json")
            .body(body.to_string())
            .reply(&api())
            .await;

        dbg!(res.body());
        assert_eq!(res.status(), 200);
        assert_eq!(
            serde_json::from_slice::<serde_json::Value>(res.body())?,
            json!({
                "user": { "id": 0, "name": "hoge" },
                "body": body,
            }),
        );

        Ok(())
    }
```

こんな感じを目指します。
`Authorization` ヘッダに Bearerトークンを入れてみる。

https://github.com/seanmonstar/warp/blob/6cc14a44ba/examples/rejections.rs#L80-L122
Rejectionをハンドリングしてレスポンスをカスタマイズできそう。
（今回はやらない）

## Filter

https://blog.logrocket.com/jwt-authentication-in-rust/
これを参考にやってみます。
ログインは一旦すっとばして、トークンを受け取って認証するFilterを入れてみたい。

```rust:filters.rs
pub fn api() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
    hello().or(receive_json()).or(with_auth_json())
}

...

pub fn with_auth_json() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone
{
    warp::path!("with_auth" / "json")
        .and(with_authenticate())
        .and(warp::post())
        .and(warp::body::json())
        .and_then(handlers::with_auth)
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct User {
    pub id: u64,
    pub name: String,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("no auth header")]
    NoAuthHeaderError,
    #[error("invalid auth header")]
    InvalidAuthHeaderError,
}
impl warp::reject::Reject for Error {}

pub fn with_authenticate() -> impl Filter<Extract = (User,), Error = warp::Rejection> + Clone {
    warp::header::headers_cloned().and_then(authorize)
}

async fn authorize(
    headers: warp::http::HeaderMap<warp::http::HeaderValue>,
) -> Result<User, warp::Rejection> {
    match token_from_header(&headers) {
        Ok(token) => {
            if token != "hogehoge" {
                return Err(warp::reject::custom(Error::InvalidAuthHeaderError));
            }
            Ok(User {
                id: 0,
                name: "hoge".to_owned(),
            })
        }
        Err(e) => Err(warp::reject::custom(e)),
    }
}

fn token_from_header(
    headers: &warp::http::HeaderMap<warp::http::HeaderValue>,
) -> Result<String, Error> {
    let header = headers
        .get(warp::http::header::AUTHORIZATION)
        .ok_or_else(|| Error::NoAuthHeaderError)?;

    let value = std::str::from_utf8(header.as_bytes()).map_err(|_| Error::NoAuthHeaderError)?;

    if !value.starts_with("Bearer ") {
        return Err(Error::InvalidAuthHeaderError);
    }

    Ok(value.trim_start_matches("Bearer ").to_owned())
}
```

- thiserrorでError型を定義
  - ヘッダーの処理で失敗した時に使う
  - ここはとりあえずanyhow::Errorで雑にやってもよかったかも
- `Authorization` ヘッダーからトークンを取得
  - ヘッダーが無い場合や、Bearerトークンじゃない場合はエラー
- トークンが `hogehoge` だったら `User` を返す
  - トークンが違ったらエラー
- 後のhandlerにUserを渡す

## Handler

```rust:handlers.rs
...
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WithAuthJson {
    user: User,
    body: HelloJson,
}
pub async fn with_auth(user: User, body: HelloJson) -> Result<impl warp::Reply, Infallible> {
    Ok(warp::reply::json(&WithAuthJson { user, body }))
}
```

- filtersで `with_authenticate` がセットした `User` を第一引数で受け取る
  - filtersで定義したandの順番になるっぽい
  - なので、user -> bodyの順番
- 受け取ったuserとbodyをそのままJSONとして返す

```shell
running 5 tests
test filters::tests::test_hello ... ok
test filters::tests::test_hello_json ... ok
test filters::tests::test_receive_json_invalid ... ok
test filters::tests::test_with_auth ... ok
test filters::tests::test_receive_json ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

テストは通りましたね。
なるほど。

# まとめ

- 一旦今回はWebAPIを実装する前提で最低限試してみたい事をちょろっとやってみました
- Warpは基本的にFilterを実装して、つなげていく
  - わかりやすいといえばわかりやすい
- こういうのどうやってやるの？があまり探しやすくはない
  - warpがpub useしている外部ライブラリ(例えばwarp::http)はそれがwarp外のcrateである事を知っていないとドキュメントまでたどり着くのが大変
  - exampleの種類は多いがあくまでexample
      - なんというかもう少し実際のWebアプリケーションなexampleがあると嬉しい
      - 例えばdieselと組み合わせてるサンプルみたいなのとか
- テストは書きやすい
- warpくらいシンプルなフレームワークもアリだなと思った
  - actix-webがRailsだとしたらwarpはSinatraみたいな雰囲気？
    - actix-webはRailsほどフルスタックではないけど…
- どうでもいいことだけど、どうしても `wrap` ってtypoする。。。
- actix-webとパフォーマンスを比較してみたい
- 次回は `sqlx` と組み合わせて使ってみたい