Zero To Production In Rust 読書メモ
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
actix-web の App をテストケースごとに作り直すのはできないらしい。
この issue を立てた人のコメントを見るとできてる気もするが、著者はうまくいった試しがないとのこと。
actix-web でも integration test をする方法は提供されているが、この本ではロックインをなるべく減らしたいというモチベーションから reqwest を使ってテストを書いていく方針になっている。
テストを書ける場所おさらい
- 実装とセットで書く
-
#[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
}
- lib は一つだけ
- bin は複数定義可能
[package]
name = "zero2prod"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
[[bin]]
path = "src/main.rs"
name = "zero2prod"
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.
並列で結合テストを行うときに有用。
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>
を使ってた。
actix_web::web::From<T>
の引数取り出しの実装はこれ
UrlEncoded::new
の内部では serde_urlencoded::from_bytes::<T>(&body).map_err(|_| UrlencodedError::Parse)
を呼び出して T
に変換している。
Serde は Rust におけるシリアライズ・デシリアライズフレームワーク。
Serde はデータモデルだけを提供しており、実際に json や avro といった形式へ変換する仕組みは serde_json
や avro-rs
などの別クレートに委ねられている。
シリアライズ処理は [Serializer]
trait と [Serialize]
trait の2つで実現されている。
Serializer
は Serde の 29 種類の データ型 を json などのフォーマットとの対応付け方が定義されている。
例えば、 serde_json
で seq を json に変換する処理では [
を出力するようになっている。(seq の中身がない場合はいきなり []
を返すように最適化されている)
もう一つの 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 でシリアライズが行えるという仕組み。
デシリアライズも同じ仕組みで実装されている。
Serde のメモリ最適化についての資料
Rust コンパイラは generics を持つ関数を見つけると、関数の中身をコピーし、そこに含まれる型パラメータに具体型を埋め込んでいく。
つまり、自分でコピペして型名だけ変えた関数をたくさん作ったのと同じことになる。
Rust はこのようにコンパイル時に generics による振る舞いの違いを解決し、実行時のオーバーヘッドをなくしている。これがゼロコスト抽象と呼ばれているもの。
Serde も generics を使っているが、 Rust なので実行時におけるパフォーマンスの影響は全く無い (通常の関数呼び出しと同じだけのコストで済む)。
また、他言語のシリアライズライブラリではリフレクションが使われがちだが、 Serde はコンパイル時点でフィールド名などの情報がすべて分かっているため、リフレクションが発生しない。
これも Serde のパフォーマンス上のメリットになっている。
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 を採用している。
sqlx のマイグレーションの流れ
-
DATABASE_URL
に connection string を設定 -
sqlx migrate add {name}
を実行し、migrations/{timestamp}_{name}.sql
を生成 - .sql ファイルにマイグレーション用のクエリを書く
sqlx migrate run