ZERO TO PRODUCTION IN RUST
official github repo
code checker
coverage
Newletterサービスを作る
ユーザストーリー
訪問者として
- newsletterをsubscribeできる
- subscribeしているnewsletterがアップデートされたらメール通知を受け取る
編集者として
- 新しいコンテンツを公開したときにsubscriberにメールを送信できる
実装しないこと
- unsubscribe機能
- 複数のnewsletterの管理
- subscriberのセグメント分け
- クリック率などのトラッキング機能
訪問者として
-
newsletterをsubscribeできる
-
subscribeしているnewsletterがアップデートされたらメール通知を受け取る
-
メールアドレスの登録フォームが必要
-
登録用のAPIエンドポイントが必要
- webフレームワークの選択
- テストの戦略を決める
- DBと連携するクレートを作る
- スキーマの変更をどう管理するか決める
- クエリを書く
actix-webを選択する
hello world!
use actix_web::{web, App, HttpRequest, HttpServer, Responder};
async fn greet(req: HttpRequest) -> impl Responder {
let name = req.match_info().get("name").unwrap_or("World");
format!("Hello {}!", &name)
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
HttpServer::new(|| {
App::new()
// web::get() is a shortcut for Route::new().guard(guard::Get())
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Rustには標準で非同期ランタイムが無い。
なので、Tokioのような非同期ランタイムを依存関係として 用意する。
#[tokio::main]はプロシージャマクロで非同期ランタイムのコードを生成するためにつけられている(?)
cargo +nightly expandするとマクロを展開した結果が確認できる
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use actix_web::{web, App, HttpRequest, HttpServer, Responder};
async fn greet(req: HttpRequest) -> impl Responder {
let name = req.match_info().get("name").unwrap_or("World");
{
let res = ::alloc::fmt::format(format_args!("Hello {0}!", & name));
res
}
}
fn main() -> Result<(), std::io::Error> {
let body = async {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
})
.bind("127.0.0.1:8000")?
.run()
.await
};
#[allow(clippy::expect_used, clippy::diverging_sub_expression)]
{
return tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
}
使用可能なポートを指定したい場合には0番ポートを指定する
0番のポートはエニーポート(any port)と呼ばれ、アプリケーションに対して、動的に別番号の空きポートを割り当てるために用意された特殊なポート番号である。
actix-webのForm
To extract typed data from a request body, the inner type T must implement the DeserializeOwned trait.
use actix_web::{post, web};
use serde::Deserialize;
#[derive(Deserialize)]
struct Info {
name: String,
}
// This handler is only called if:
// - request headers declare the content type as `application/x-www-form-urlencoded`
// - request payload deserializes into an `Info` struct from the URL encoded format
#[post("/")]
async fn index(web::Form(form): web::Form<Info>) -> String {
format!("Welcome {}!", form.name)
}
serialize/deserializeのライブラリことserde
Serde is a framework for serializing and deserializing Rust data structures efficiently and generically.
A type that implements FromRequest is called an extractor and can extract data from the request. Some types that implement this trait are: Json, Header, and Path.
pub trait FromRequest: Sized {
type Error: Into<Error>;
type Future: Future<Output = Result<Self, Self::Error>>;
// Required method
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future;
// Provided method
fn extract(req: &HttpRequest) -> Self::Future { ... }
}
FromRequestトレイトを実装した型をextractorと呼んでいる。extractorのfrom_requestは与えた型をSelfとして返す。
ルートハンドラーがfrom_requestを呼んでリクエストが型を満たしているときだけ、ハンドラーに値が渡り処理が行われる(?)
- handlerが呼ばれる前にactix-webがfrom_requestを呼んで入力値をパース(?)する
- Form::from_requestはbodyをFormDataにデシリアライズする
- デシリアライズに失敗すると400 BAD REQUESTを返して、成功すれば 200 OKを返す
DBの選択
著者はPostgreSQLが好きらしい
クライアントの選択
- tokio-postgress
- sqlx
- diesel
選択のポイント - compile-time safety
- SQL-first vs a DSL for query building
- async vs sync interface
compile-time safety | SQL or DSL | async or sync | |
---|---|---|---|
tokio-postgress | × | SQL | async |
sqlx | 〇 | SQL | async |
diesel | 〇 | DSL | sync |
sqlxを選択する
DBのマイグレーションにはsqlx-cliを使う
西のViper、東のConfig
Config lets you set a set of default parameters and then extend them via merging in configuration from a variety of sources:
Environment variables
String literals in well-known formats
Another Config instance
Files: TOML, JSON, YAML, INI, RON, JSON5 and custom ones defined with Format trait
Manual, programmatic override (via a .set method on the Config instance)
#[derive(serde::Deserialize)]
pub struct Settings {
pub database: DatabaseSettings,
pub application_port: u16,
}
#[derive(serde::Deserialize)]
pub struct DatabaseSettings {
pub username: String
pub password: String
pub port: u16
pub host: String,
pub database_name: String,
}
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
let settings = config::Config::builder() => Config, ConfigBuilder<DefaultState>
.add_source(config::File::new("configuration.yaml", config::FileFormat::Yaml))
.build()?;
// try to convert the config into a Settings struct
settings.try_deserialize::<Settings>()
}
アプリケーションレベルの状態管理
use std::cell::Cell;
use actix_web::{web, App, HttpRequest, HttpResponse, Responder};
struct MyData {
count: std::cell::Cell<usize>,
}
async fn handler(req: HttpRequest, counter: web::Data<MyData>) -> impl Responder {
// note this cannot use the Data<T> extractor because it was not added with it
let incr = *req.app_data::<usize>().unwrap();
assert_eq!(incr, 3);
// update counter using other value from app data
counter.count.set(counter.count.get() + incr);
HttpResponse::Ok().body(counter.count.get().to_string())
}
let app = App::new().service(
web::resource("/")
.app_data(3usize)
.app_data(web::Data::new(MyData { count: Default::default() }))
.route(web::get().to(handler))
);
Observability is about being able to ask arbitrary questions about your environment without - and this is the key part - having to know ahead of time what you wanted to ask.
log crate
logging facadeとあるようにset_loggerを呼んでロギングライブラリーに出力を流す(?)
actix-webにはloggerのmiddlewareがある
A scoped, structured logging and diagnostics system.
tracing is a framework for instrumenting Rust programs to collect structured, event-based diagnostic information.
実装をガリガリ進める前にロギングを準備しておくとデバックが簡単になってhappy
rustでproperty-base testを書く時にオプションになるのはquickcheckとproptest
use std::convert::TryFrom;
#[derive(Debug, PartialEq)]
struct EvenNumber(i32);
impl TryFrom<i32> for EvenNumber {
type Error = ();
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(EvenNumber(value))
} else {
Err(())
}
}
}
便利バリデーター
subscriberのemailに確認用リンクを送信する
- /subscribeにPOSTがあったらテーブルにデータをインサートしステータスをpendingにする
- トークンを生成してDBに格納する
- /subscription/confirm?token=<token here!!>のリンクの付いたメールを送信する
- 200を返す
- ユーザがリンクを踏んだらDBのトークンとクエリパラメータのトークンを照合する
- 一致したらステータスをpendingからconfirmedに変える
- 200を返す
とても便利なtodo!マクロ
シンプルなhttpクライアント
httpリクエストのためのモックサーバー
テキストからリンク文字列を見つける
エラーを返す前にログに出す
...
.map_err(|e| {
tracing::error!("error: {:?}", e);
e
})
...
エラーハンドリング
Control Flow ... 次に何をするべきか
Reporting ... 何が間違っているか
Internal | At the edge | |
---|---|---|
Control Flow | Types, methods, fields | Status code |
Reporting | Logs/traces | Response body |
エラーはそのエラーがハンドリングされる階層でログ出力する
ハンドリングせずに伝搬させる場合はログ出力しない(複数階層で同じエラーをログに出さない)
chap9まででnewsletterの登録までできるようになった
残りのセクションで以下を追加していく
- /newslettersのエンドポイントをprotectする
- review機能をつけてnewsletterを編集できるようにする
- subscriberへのメール送信を並列化する
- 送信に失敗したメールをretryできるようにする
認証に使えるもの
- ユーザーが知っているもの(passwords, PINs, security questions, etc.)
- ユーザーが持っているもの(devices, authenticator app, etc.)
- ユーザー自身(fingerprints, Face ID, etc.)
PHC string formatはパスワードハッシュ、ソルト、アルゴリズムなどの情報を格納するためのフォーマット
ex. ${algorithm}${algorithm version}${,-separated algorithm parameters}${hash}${salt}
毎回わかりにくい名前だなって思うやつ