Open13

Zero To Production In Rust 読書メモ

keiskeis

1 ~ 3 章

使ったことなかった cargo のサブコマンド

cargo watch

他の言語でもよくある、コードの変更を検知してビルドしてくれるやつ。
タスクをいくつか指定しておくと、変更があるたびにそれらすべてを順に実行してくれる。
ファイルを保存したら fmt -> clippy -> build -> test -> run を全部まとめてやってもらう、みたいなこともできちゃう。

cargo watch -x check -x run

cargo tarpaulin

カバレッジの計測

cargo tarpaulin --ignore-tests

cargo-audit

依存クレートの脆弱性スキャンをしてくれる。

cargo audit

cargo-expand

コード内のマクロを展開して出力してくれる。マクロの挙動を調べるときとかデバッグに使えそう。
これを書いてる段階では nightly の toolchain がないと動かなかったので追加でインストールが必要。

# nightly toolchain のインストール
rustup toolchain install nightly --allow-downgrade
# 実行
cargo +nightly expand
keiskeis

actix-web の App をテストケースごとに作り直すのはできないらしい。
この issue を立てた人のコメントを見るとできてる気もするが、著者はうまくいった試しがないとのこと。

https://github.com/actix/actix-web/issues/1147

actix-web でも integration test をする方法は提供されているが、この本ではロックインをなるべく減らしたいというモチベーションから reqwest を使ってテストを書いていく方針になっている。

keiskeis

テストを書ける場所おさらい

  • 実装とセットで書く
    • #[cfg(test)] をつける
    • テストのときだけコンパイルされる
    • コードに直接記述するので、 private なコードやプロジェクトの依存パッケージにもアクセスできる
    • public インターフェースに対するテストだけでは安心できないときに使うと便利
  • tests ディレクトリに書く
    • src と同じ階層
    • ここに書いたテストは別のバイナリとしてコンパイルされる
    • public なものにしかアクセスできない
    • integration test を置く場所として良い
  • doc test
    • これも別バイナリとしてコンパイルされて実行される

doc test だけ書き方書いておく

/// Check if a number is even.
/// ```rust
/// use zero2prod::is_even;
/// 
/// assert!(is_even(2));
/// assert!(!is_even(1));
/// ```
pub fn is_even(x: u64) -> bool {
    x % 2 == 0
}
keiskeis

TcpListener::bind() にわたすアドレスのポートを 0 にしておくと、空いているポートを自動で割り当ててくれる。便利。

Binding with a port number of 0 will request that the OS assigns a port to this listener. The port allocated can be queried via the TcpListener::local_addr method.

https://doc.rust-lang.org/std/net/struct.TcpListener.html#method.bind

並列で結合テストを行うときに有用。

keiskeis

actix-web における form のパラメータのパース処理は FromRequest trait の実装に任せられている。

pub trait FromRequest: Sized {
  type Error = Into<actix_web::Error>;

  async fn from_request(
    req: &HttpRequest,
    payload: &mut Payload
  ) -> Result<Self, Self::Error>;

from_request が HTTP request とペイロードのバイトデータを受け取ってパースを行う。
route handler の引数にこの FromRequest trait を実装させることでパースされたデータを取得することが可能。

actix-web は各引数の from_request() を順に呼び出し、すべての呼び出しに成功したら handler の呼び出しを開始する。

本のサンプルコードでは引数にトレイトを実装した actix_web::web::From<T> を使ってた。

keiskeis

Serde は Rust におけるシリアライズ・デシリアライズフレームワーク。
Serde はデータモデルだけを提供しており、実際に json や avro といった形式へ変換する仕組みは serde_jsonavro-rs などの別クレートに委ねられている。

シリアライズ処理は [Serializer] trait と [Serialize] trait の2つで実現されている。

Serializer は Serde の 29 種類の データ型 を json などのフォーマットとの対応付け方が定義されている。

例えば、 serde_json で seq を json に変換する処理では [ を出力するようになっている。(seq の中身がない場合はいきなり [] を返すように最適化されている)

https://github.com/serde-rs/json/blob/master/src/ser.rs#L283-L303

もう一つの Serialize trait は Rust のデータを Serde のデータ型へマッピングする方法を定義している。

Vec におけるSerialize trait の実装は以下のようになっている。
Vec は serialize_seq -> serialize_element -> end という順で Serializer の処理を呼び出し、自身のデータ構造を Serde に宣言している。

use serde::ser::{Serialize, Serializer, SerializeSeq, SerializeMap};

impl<T> Serialize for Vec<T>
where
    T: Serialize,
{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut seq = serializer.serialize_seq(Some(self.len()))?;
        for e in self {
            seq.serialize_element(e)?;
        }
        seq.end()
    }
}

独自に定義したデータ型に Serialize を実装すれば、好きな Serializer でシリアライズが行えるという仕組み。

デシリアライズも同じ仕組みで実装されている。

keiskeis

Rust コンパイラは generics を持つ関数を見つけると、関数の中身をコピーし、そこに含まれる型パラメータに具体型を埋め込んでいく。
つまり、自分でコピペして型名だけ変えた関数をたくさん作ったのと同じことになる。

Rust はこのようにコンパイル時に generics による振る舞いの違いを解決し、実行時のオーバーヘッドをなくしている。これがゼロコスト抽象と呼ばれているもの。

Serde も generics を使っているが、 Rust なので実行時におけるパフォーマンスの影響は全く無い (通常の関数呼び出しと同じだけのコストで済む)。

keiskeis

また、他言語のシリアライズライブラリではリフレクションが使われがちだが、 Serde はコンパイル時点でフィールド名などの情報がすべて分かっているため、リフレクションが発生しない。
これも Serde のパフォーマンス上のメリットになっている。

keiskeis

RDB 使うためのライブラリは何を使うか?

有名どころは以下の3つ。
ORM っぽいものばかり触っていたので、生 SQL を書くものが2つも入っているのはちょっと意外。

Crate コンパイル時検証 インターフェース Async
tokio-postgres No SQL Yes (pipelining にも対応)
sqlx Yes (コンパイル時に DB へ接続してクエリが正しいか確かめる) SQL Yes
diesel Yes DSL No

本では actix-web を使うことと、 SQL をかけたほうが習得カーブが早い点を考慮して sqlx を採用している。

keiskeis

sqlx のマイグレーションの流れ

  1. DATABASE_URL に connection string を設定
  2. sqlx migrate add {name} を実行し、 migrations/{timestamp}_{name}.sql を生成
  3. .sql ファイルにマイグレーション用のクエリを書く
  4. sqlx migrate run