Open3

Rustでtwitter的なWebアプリを作る

oksonghoksongh

雑に書いてあとからきれいにする。
Axum,SQLiteを使っているが、DBは気分で変えるかも

oksonghoksongh

https://github.com/oksongh/InterrupTweet/commit/aea5357fbb695d0890d2fc93226699823faba984

tweet生成のRestAPIを作成。
一旦controllerにDBアクセスまで全部書いてみる。
エンドポイントのテストが簡単にできそうなのでやってみた。
tokio::testだとテストごとにコネクションプール作成とマイグレーションやらないといけないのが少し面倒で、メリットとしてはマイグレーションファイルのパスを環境変数で指定できること。
sqlx::testだとコネクションプール作成とマイグレーションを勝手にやってくれるらしいので試してみようと思うが、マイグレーションファイルはマクロに与えるので環境変数で指定するのは無理そう。constな変数はインライン展開してくれるらしいからマクロにも与えられるかもしれない。
そもそもマイグレーションファイルのパスを抽象化する必要があるのか疑問だが。

今回初めて使って便利だと思ったのがstaticな変数を実行時に初期化できるonce_cellクレート。いつ実行されるんだろうか。Lazyってあるくらいだし最初に呼ばれたときだろうか?

static MIGRATOR: Lazy<String> = Lazy::new(|| {
    dotenvy::dotenv().ok();
    env::var("MIGRATIONS_DIR").expect("MIGRATIONS_PATH must be set")
});

それにしても作るならユーザーを作成するAPIが先だったなあ

oksonghoksongh

https://github.com/oksongh/InterrupTweet/commit/6da83eaa210668ca570256e2f2186d2d4ae0e168

ユーザー作成APIを作った。
いろいろ試行錯誤した。

  1. ハンドラーの返す型は何がいい?具体的な型か抽象的な型か

    1. 具体的な型:Jsonとか、Stringとか(StatusCode,Json)とか
      Json単体、String単体だとメリットは特にない。とりあえずで書きやすいぐらい。
      ステータスコード入りだと毎度ステータスコードを書くことを強制してくれるのがありがたい。
    2. 抽象的な型:impl IntoResponse
      何を返すのかがはっきりしないが、それゆえにリファクタリングがしやすい。
      最初Jsonだけ返してたけど作成系APIだし201CREATED返すか、のように変更するとき、シグニチャーが変わらないのが嬉しい。
      シグニチャーが変わらないとAPIのテストを修正する必要がなくなって良い。
  2. APIのテストはどんな形式がいい?
    最初はAPIごとテストする必要性を感じられなかった。ハンドラー関数を関数としてテストすればよくないか?と。
    tokioなりAxumなりがexampleに出しているテストはこんな感じ。

    雰囲気だけで動かない、chatGPTに任せたツケだが
    #[sqlx::test(migrations = "./migrations")]
    async fn test_create_new_tweet(pool: SqlitePool) {
        let app = Router::new()
            .route("/tweets", post(create_new_tweet))
            .with_state(pool.clone());
    
        let request = Request::builder()
            .method("POST")
            .uri("/tweets")
            .header("Content-Type", "application/json")
            .body(Body::from(json!({
                "user_id": 1,
                "content": "Hello, world!"
            })
            .to_string()))
            .unwrap();
    
        let response = app.clone().oneshot(request).await.unwrap();
        assert_eq!(response.status(), StatusCode::OK);
    
        let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
        let body_text = String::from_utf8(body.to_vec()).unwrap();
        assert_eq!(body_text, "\"Tweet created successfully\"");
    }
    

    長くないか?テスト一つ書くたびにこれがいるの?エンドポイント作る、Content-Type設定する、入力をJsonで用意する、...
    関数の振る舞いだけテストするならここまで必要ないと思う。
    でも入力のJsonをそのまま書くのはcurlから呼び出すときとかに便利層ではあるよね。

知らなかったこと

HTTPのステータスコードで201知らなかった。作成しても200返せばいいと思ってた。
Locationの存在も知らなかった。行儀よくするなら実装するべきらしいね。

次はリファクタリングする。テスト書くと分量が増えるしモジュール分けるか(ファイル分けるか)する。