Open72

ZERO TO PRODUCTION IN RUST

pqppqpqppq

Newletterサービスを作る
ユーザストーリー

訪問者として

  • newsletterをsubscribeできる
  • subscribeしているnewsletterがアップデートされたらメール通知を受け取る

編集者として

  • 新しいコンテンツを公開したときにsubscriberにメールを送信できる

実装しないこと

  • unsubscribe機能
  • 複数のnewsletterの管理
  • subscriberのセグメント分け
  • クリック率などのトラッキング機能
pqppqpqppq

訪問者として

  • newsletterをsubscribeできる

  • subscribeしているnewsletterがアップデートされたらメール通知を受け取る

  • メールアドレスの登録フォームが必要

  • 登録用のAPIエンドポイントが必要

pqppqpqppq
  • webフレームワークの選択
  • テストの戦略を決める
  • DBと連携するクレートを作る
  • スキーマの変更をどう管理するか決める
  • クエリを書く
pqppqpqppq

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
}  
pqppqpqppq

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);
    }
}
pqppqpqppq

actix-webのForm
https://docs.rs/actix-web/latest/actix_web/web/struct.Form.html

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)
}
pqppqpqppq

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を返す
pqppqpqppq

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を選択する
https://github.com/launchbadge/sqlx

pqppqpqppq

西のViper、東のConfig
https://docs.rs/config/latest/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>()
}   
pqppqpqppq

アプリケーションレベルの状態管理
https://docs.rs/actix-web/latest/actix_web/struct.App.html#method.app_data

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))
);
pqppqpqppq

https://www.honeycomb.io/guide-observability-for-developers

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を呼んでロギングライブラリーに出力を流す(?)
https://docs.rs/log/latest/log/#
https://docs.rs/log/latest/log/fn.set_logger.html
actix-webにはloggerのmiddlewareがある
https://docs.rs/actix-web/latest/actix_web/middleware/struct.Logger.html

pqppqpqppq

実装をガリガリ進める前にロギングを準備しておくとデバックが簡単になってhappy

pqppqpqppq

subscriberのemailに確認用リンクを送信する

  • /subscribeにPOSTがあったらテーブルにデータをインサートしステータスをpendingにする
  • トークンを生成してDBに格納する
  • /subscription/confirm?token=<token here!!>のリンクの付いたメールを送信する
  • 200を返す
  • ユーザがリンクを踏んだらDBのトークンとクエリパラメータのトークンを照合する
  • 一致したらステータスをpendingからconfirmedに変える
  • 200を返す
pqppqpqppq

エラーを返す前にログに出す

...
.map_err(|e| {
  tracing::error!("error: {:?}", e);
  e
})
...
pqppqpqppq

エラーハンドリング
Control Flow ... 次に何をするべきか
Reporting ... 何が間違っているか

Internal At the edge
Control Flow Types, methods, fields Status code
Reporting Logs/traces Response body
pqppqpqppq

エラーはそのエラーがハンドリングされる階層でログ出力する
ハンドリングせずに伝搬させる場合はログ出力しない(複数階層で同じエラーをログに出さない)

pqppqpqppq

chap9まででnewsletterの登録までできるようになった
残りのセクションで以下を追加していく

  • /newslettersのエンドポイントをprotectする
  • review機能をつけてnewsletterを編集できるようにする
  • subscriberへのメール送信を並列化する
  • 送信に失敗したメールをretryできるようにする
pqppqpqppq

認証に使えるもの

  • ユーザーが知っているもの(passwords, PINs, security questions, etc.)
  • ユーザーが持っているもの(devices, authenticator app, etc.)
  • ユーザー自身(fingerprints, Face ID, etc.)