Rust面接対策

Rustバックエンド開発者面接問題

レベル別にまとめた、選りすぐりの Rust バックエンド面接問題 15問。基礎、実践上のトレードオフ、シニアレベルの本番運用での判断を確認できます。

RustのAI面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

初級向け質問

1Rustの所有権規則について説明し、それがクローン(clone)、借用(borrow)、アプリケーション状態の共有といったバックエンドにおける実践的な選択にどう影響するかを述べてください。

Rustでは、各値に単一の所有者(owner)が存在します。`Copy` トレイトを実装していない値をムーブすると所有権が移動し、所有者がスコープを抜けると値はドロップ(破棄)されます。コードは、共有参照を通じて不変(イミュータブル)にデータを借用するか、排他的な可変参照を通じて可変(ミュータブル)に借用するかのいずれかを選択できます。バックエンドコードでは、この規則によって、関数が所有権を受け取るか、一時的に借用するか、独立した所有値を手に入れるためにクローンするか、あるいは変更の共有が必要な場合に同期機構などの並行性プリミティブを伴って `Arc` などのハンドル経由で長寿命な状態を共有するかといった設計判断が形作られます。

use std::sync::Arc;

struct AppState {
    service_name: String,
}

async fn handler(state: Arc<AppState>, request_id: String) -> String {
    // Borrow: no ownership transfer of the String inside state.
    let name: &str = &state.service_name;

    // Clone only if an owned independent value is needed.
    let owned_name = name.to_owned();

    format!("{owned_name}:{request_id}")
}

fn configure(state: Arc<AppState>) {
    // Cloning Arc increments the reference count; it does not clone AppState.
    let state_for_route = Arc::clone(&state);
    let _ = state_for_route;
}
AI コーチを使ってこの質問に答えてみる

2&T と &mut T による借用の違いは何ですか?また、それらの規則は API (Application Programming Interface) やハンドラのシグネチャにどのように影響しますか?

`&T` は共有参照(shared reference)です。読み取り専用のアクセスを許可し、同じ値に対する複数の共有参照が同時に存在できます。`&mut T` は排他的な可変参照(exclusive mutable reference)です。値の変更を許可しますが、これが有効である間は、同じ値に対する他の共有参照や可変参照を一切使用できません。 これらの規則は、読み取り専用の関数には `&T` や `&str` のようなより狭い借用型を受け取らせ、値を変更する関数には `&mut T` を受け取らせるか、所有権を移動(ムーブ)させるか、あるいは内部可変性(interior mutability)や同期機構を利用させるという形でAPIの設計に影響を与えます。バックエンドのハンドラでは通常、並行するリクエスト間で共有アプリケーション状態への単純な `&mut` アクセスを行うことは避け、共有ハンドルと同期機構の組み合わせやその他の所有権パターンを採用します。

struct User {
    name: String,
    login_count: u64,
}

fn display_name(user: &User) -> &str {
    &user.name
}

fn record_login(user: &mut User) {
    user.login_count += 1;
}

fn main() {
    let mut user = User { name: "Ada".into(), login_count: 0 };

    let name = display_name(&user); // shared borrow for reading
    println!("{name}");

    record_login(&mut user); // exclusive mutable borrow for mutation
}
AI コーチを使ってこの質問に答えてみる

3Box はどのように機能し、Box を介したヒープ割り当てによってどのような問題が解決されますか?

`Box<T>` は所有権を持つスマートポインタであり、`Box` ハンドル自体はポインタと同等の固定サイズの値である一方、実体である `T` はヒープ上に格納されます。単一の所有権を持ち、スコープを抜ける際にヒープ割り当てを破棄(解放)します。`Box` は間接参照を提供するため、サイズの大きな値、コンパイル時に既知のサイズが必要な再帰型、動的サイズ型、および `Box<dyn Trait>` などのトレイトオブジェクトを扱う際に有用です。`Box` をムーブすると、ヒープに格納された値自体をムーブまたはコピーすることなく、メモリ割り当ての所有権のみが移譲されます。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
AI コーチを使ってこの質問に答えてみる

4Result型とOption型について説明し、値の不在や失敗する可能性のある操作を処理するための主要で慣用的な方法を述べてください。

`Option<T>` は `Some(T)` または `None` のいずれかを表し、値が存在しないことが想定される場合に使用されます。`Result<T, E>` は `Ok(T)` または `Err(E)` のいずれかを表し、エラー情報が重要となる失敗する可能性のある操作に使用されます。慣用的な処理方法としては、`match`、`if let`/`let else`、`map`、`and_then`、`ok_or`、`unwrap_or`、`unwrap_or_else` などのコンビネータ、および互換性のある `Option` や `Result` 型を返す関数から早期リターンを行うための `?` 演算子があります。`unwrap` や `expect` は、不変条件の表明、テスト、プロトタイプ作成、または回復不能な状況に留めるのが最適であり、バックエンドにおける通常の回復可能なエラー処理には使用すべきではありません。

fn parse_port(s: Option<&str>) -> u16 {
    s.and_then(|v| v.parse::<u16>().ok())
     .unwrap_or(8080)
}

assert_eq!(parse_port(Some("3000")), 3000);
assert_eq!(parse_port(None), 8080);
AI コーチを使ってこの質問に答えてみる

5? 演算子はどのように動作し、異なるエラー型同士の変換は何によって可能になりますか?

`?` 演算子は、エラー伝播を簡潔に記述するための構文です。`Result` 型に対して `expr?` を適用すると、`Ok` の値を取り出して処理を継続するか、あるいは `Err` を呼び出し元関数へ早期リターンします。`Option` 型に対しては、`Some` の値を取り出すか、`None` をリターンします。このとき、外側の関数は互換性のある型を返す必要があります。`Result` では、発生したエラーが `From` / `Into` スタイルの変換を通じて関数の戻り値のエラー型へと変換されるため、異なるエラー型同士を合成できます。この変換は手動で実装されることもあれば、`thiserror` などのヘルパークレートを用いて自動導出されることもよくあります。

use std::{fs, io, num::ParseIntError};

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn read_number(path: &str) -> Result<i32, AppError> {
    let s = fs::read_to_string(path)?; // io::Error -> AppError
    let n = s.trim().parse::<i32>()?;  // ParseIntError -> AppError
    Ok(n)
}
AI コーチを使ってこの質問に答えてみる

6enumに対するパターンマッチングについて説明し、網羅的なマッチング(exhaustive matching)がドメインやAPIの正当性をどのように向上させるかを述べてください。

enumは固定されたバリアント(variant)の集合のうち、いずれか1つの値を取り得るデータ構造をモデル化し、各バリアントはデータを保持できます。`match`式はバリアントごとに分岐し、内部のデータを分配束縛(デストラクト)できます。Rustでは、ワイルドカードや包括パターン(catch-all pattern)が明示的に使用されていない限り、enumのマッチングが網羅的であるかをコンパイラが検証します。これにより、新しい状態やレスポンスのバリアントが追加された際、それをマッチングしているコードに対して暗黙的に無視するのではなく、その新しいケースをどのように処理するかを明示的に決定することをコンパイラが強制するため、ドメインやAPIの正当性が向上します。

enum PaymentStatus {
    Pending,
    Paid { receipt_id: String },
    Failed(String),
}

fn message(status: PaymentStatus) -> String {
    match status {
        PaymentStatus::Pending => "Payment is pending".to_string(),
        PaymentStatus::Paid { receipt_id } => format!("Paid, receipt {receipt_id}"),
        PaymentStatus::Failed(reason) => format!("Payment failed: {reason}"),
    }
}
AI コーチを使ってこの質問に答えてみる

7Newtypeパターン(newtype wrappers)とは何ですか。また、ID、金額、トークン、シークレット情報の型安全性をどのように向上させますか?

Newtypeラッパーとは、既存の内部表現をラップする独立したRustの型であり、多くの場合 `struct UserId(Uuid);` や `struct AccessToken(String);` のように単一フィールドのタプル構造体として定義されます。 これにより、コンパイラが基底のデータ型を共有する意味的に異なる値(例えば `UserId` と `OrderId`、セントとドル、未検証の文字列と検証済みトークンなど)を取り違えないようになるため、型安全性が向上します。また、Newtypeを利用することで、値の生成やバリデーション規則を集約できるほか、`Display`、`Debug`、`Serialize` などのトレイトの実装や、シークレット情報のマスキング(redaction)処理を個別に制御できます。

use uuid::Uuid;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct UserId(Uuid);

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct OrderId(Uuid);

struct SecretToken(String);

impl std::fmt::Debug for SecretToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("SecretToken(**redacted**)")
    }
}

fn load_user(id: UserId) {
    // query by user id
}

fn example(user_id: UserId, order_id: OrderId) {
    load_user(user_id);
    // load_user(order_id); // does not compile: expected UserId, found OrderId
}
AI コーチを使ってこの質問に答えてみる

中級向け質問

8RustにおけるDropトレイトの仕組みについて、ドロップ順序、RAII(Resource Acquisition Is Initialization)によるクリーンアップ、非同期クリーンアップにおける制限を含めて説明してください。

`Drop`はRustにおける決定論的なクリーンアップフックです。所有権を持つ値がスコープを抜けるなどして破棄される際、その値が`Drop`を実装していれば、Rustは自動的にその`drop(&mut self)`メソッドを呼び出し、続いてそのフィールドをドロップします。これによりRAII(Resource Acquisition Is Initialization)がサポートされ、ファイル、ソケット、ロック、トランザクション、パーミット、バッファなどのリソースは、それらを所有する値やガードに結び付けられ、それらの値がドロップされた時点で解放されます。 ローカル変数は生成された順序と逆順でドロップされます。`Drop`を実装した構造体の場合、まず構造体の`drop`メソッドが実行され、その後にフィールドが宣言順にドロップされます。 `Drop::drop`は同期的に動作し、`async`にしたり`await`したりすることはできません。そのため、適切な非同期クリーンアップを行うには、通常、明示的な非同期のclose/shutdown/flushメソッドを用意するか、別の設計を採用する必要があります。`Drop`自身は同期的またはベストエフォートなクリーンアップしか行えません。

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Guard,
    second: Guard,
}

impl Drop for Pair {
    fn drop(&mut self) {
        println!("drop Pair");
    }
}

fn main() {
    let _a = Guard("a");
    let _pair = Pair {
        first: Guard("first"),
        second: Guard("second"),
    };
    let _b = Guard("b");
}
AI コーチを使ってこの質問に答えてみる

9Rustにおけるライフタイム(lifetime)について説明し、RustのAPI(Application Programming Interface)で明示的なライフタイム注釈が時として必要になる理由を述べてください。

ライフタイムとは参照が有効である期間を表すものであり、コンパイラがダングリング参照を排除できるようにします。ライフタイム注釈は参照間の関係を表すコンパイル時の制約であり、基底にあるデータの寿命を延ばしたり、実行時の振る舞いを追加したりすることはありません。単純なシグネチャの多くはライフタイムの省略規則によって自動的に処理されますが、入力参照と出力参照の関係をコンパイラが推論できない場合や、型に参照を保持させる場合、あるいはAPIでジェネリックな借用制約を表現する必要がある場合には、明示的な注釈が必要になります。

fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left.len() >= right.len() { left } else { right }
}

struct UserView<'a> {
    name: &'a str,
}

fn main() {
    let a = String::from("short");
    let b = String::from("longer");
    let result = longest(&a, &b);
    let view = UserView { name: result };
    println!("{}", view.name);
}
AI コーチを使ってこの質問に答えてみる

10'static ライフタイムは何を意味し、生成されたタスク(spawned task)やバックエンドの API 設計とどのように相互作用しますか?

参照における `'static` は、文字列リテラルや `static` アイテムのように、参照先データがプログラム全体の実行期間中有効であることを意味します。一方、`T: 'static` のようなトレイト境界は、型 `T` の値が `'static` ではない借用参照を含まないことを意味し、任意に長い期間保持できることを示します。値自体が永久に生存しなければならない、あるいは破棄(drop)できないという意味ではありません。生成されたタスクは、タスクを生成したスタックフレームが終了した後もエグゼキュータが実行を継続する可能性があるため、多くの場合 `'static` な future を要求します。バックエンドコードでは、通常、ローカル変数を借用する代わりに、`async move` を使用して所有権を持つデータをタスク内にムーブするか、所有権を持つ型を使用するか、`Arc` などの共有ハンドルをクローンすることを意味します。API 設計においては、保持されるコールバック、バックグラウンドジョブ、生存期間の長い状態には `'static` 境界を使用すべきですが、短期間の借用操作には不要な `'static` 境界を設けないようにする必要があります。

use std::sync::Arc;

struct AppState {
    name: String,
}

fn spawn_background(state: Arc<AppState>, user_id: String) {
    tokio::spawn(async move {
        // The task owns an Arc handle and a String, so it does not borrow this function's stack.
        println!("{}:{user_id}", state.name);
    });
}
AI コーチを使ってこの質問に答えてみる

11Rustがポリモーフィズムを実現するためにトレイトをどのように使用しているかを説明し、impl Trait、ジェネリクス、dyn Traitの違いを比較してください。

Rustは共通の振る舞いを表現するためにトレイトを使用します。`fn f<T: Trait>(x: T)` のようなジェネリクス境界は、通常、単相化(monomorphization)による静的ディスパッチを用います。引数位置の `impl Trait` は、実質的に匿名ジェネリクスパラメータの糖衣構文(シンタックスシュガー)です。戻り値位置の `impl Trait` は、関数側で決定された単一の具象戻り値型を隠蔽します。`dyn Trait` は `&dyn Trait` や `Box<dyn Trait>` などのポインタ経由で使用されるトレイトオブジェクトであり、vtable(仮想関数テーブル)を用いた動的ディスパッチを行い、オブジェクト安全性の制約を満たす範囲で実行時の型消去や異種要素の混在をサポートします。

trait Render { fn render(&self) -> String; }

struct Html;
struct Json;

impl Render for Html { fn render(&self) -> String { "html".into() } }
impl Render for Json { fn render(&self) -> String { "json".into() } }

fn render_generic<T: Render>(x: &T) -> String { x.render() } // static dispatch
fn render_impl_arg(x: &impl Render) -> String { x.render() } // anonymous generic
fn make_renderer() -> impl Render { Html } // one hidden concrete type
fn render_dyn(x: &dyn Render) -> String { x.render() } // dynamic dispatch

fn main() {
    let items: Vec<Box<dyn Render>> = vec![Box::new(Html), Box::new(Json)];
    for item in items { println!("{}", render_dyn(item.as_ref())); }
}
AI コーチを使ってこの質問に答えてみる

12トレイトにおける関連型(associated types)とは何ですか?また、ジェネリック型パラメータよりも関連型を選択すべきなのはどのような場合ですか?

関連型とは、トレイト内部で宣言され、各実装によって具体的に指定される名前付きの型のプレースホルダです(例: `Iterator::Item`)。その型がトレイトの仕様(コントラクト)の一部であり、各実装型に対して自然な選択肢が1つに定まる場合に適しています。一方、ジェネリック型パラメータは、呼び出し側が型を選択すべき場合や、異なる型の選択肢に対して同一の実装型が複数の実装を持つ必要がある場合に適しています。

trait Repository {
    type Entity;
    fn get(&self, id: u64) -> Option<Self::Entity>;
}

struct User;
struct UserRepo;

impl Repository for UserRepo {
    type Entity = User;
    fn get(&self, _id: u64) -> Option<Self::Entity> { Some(User) }
}

fn load<R: Repository>(repo: &R) -> Option<R::Entity> {
    repo.get(1)
}
AI コーチを使ってこの質問に答えてみる

上級向け質問

13Rustサービスでトランザクショナルアウトボックス(Transactional Outbox)パターンをどのように実装しますか?

Rustサービスでトランザクショナルアウトボックスパターンを実装するには、単一のデータベーストランザクション内(例: `sqlx::Transaction` を使用)でドメインデータの更新と送信イベントペイロードのアトミックな書き込みを行います。`outbox` テーブルには送信先トピック、ペイロード、およびステータスまたは作成タイムスタンプを保存します。 バックグラウンドの非同期ワーカー(またはDebeziumなどの変更データキャプチャ(CDC)プロセス)が保留中のイベントをポーリングまたはストリーミングし、メッセージブローカー(Kafka、RabbitMQ、NATSなど)へ発行します。Rustでポーリングワーカーを用いる場合(例: `tokio::spawn` ループ内)、`SELECT ... FOR UPDATE SKIP LOCKED` を使用することで、複数のサービスインスタンスがロック競合を起こさずに並行してアウトボックス行を取得・ディスパッチできます。ブローカーへの配信確認を受け取ったワーカーは、アウトボックスレコードのステータスを更新するか行を削除します。ネットワークの再試行によって少なくとも1回(at-least-once)の配信になり得るため、下流のコンシューマはメッセージを冪等に処理できるように設計する必要があります。

use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

pub async fn create_user_with_outbox(
    mut tx: Transaction<'_, Postgres>,
    username: &str,
    email: &str,
) -> Result<(), sqlx::Error> {
    let user_id = Uuid::new_v4();
    sqlx::query!(
        "INSERT INTO users (id, username, email) VALUES ($1, $2, $3)",
        user_id, username, email
    )
    .execute(&mut *tx)
    .await?;

    let payload = serde_json::json!({"event": "UserCreated", "user_id": user_id, "email": email});
    sqlx::query!(
        "INSERT INTO outbox_events (id, topic, payload, status) VALUES ($1, $2, $3, 'PENDING')",
        Uuid::new_v4(),
        "user_events",
        payload
    )
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;
    Ok(())
}

pub async fn run_outbox_relay(pool: PgPool) {
    loop {
        let mut tx = match pool.begin().await { Ok(t) => t, Err(_) => continue };
        let rows = sqlx::query!(
            "SELECT id, topic, payload FROM outbox_events WHERE status = 'PENDING' LIMIT 50 FOR UPDATE SKIP LOCKED"
        )
        .fetch_all(&mut *tx)
        .await;

        if let Ok(events) = rows {
            for event in events {
                // publish_to_broker(&event.topic, &event.payload).await;
                let _ = sqlx::query!("UPDATE outbox_events SET status = 'PROCESSED' WHERE id = $1", event.id)
                    .execute(&mut *tx)
                    .await;
            }
            let _ = tx.commit().await;
        }
        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
    }
}
AI コーチを使ってこの質問に答えてみる

14データベースの更新と外部APIの呼び出しを伴う複数ステップの処理をどのように設計しますか?

データベースへの書き込みと外部API呼び出しにまたがる複数ステップの処理を設計する場合、2相コミット(2PC: Two-Phase Commit)に依存することなく、部分的な障害やネットワーク分断に対処できる設計にする必要があります。 1. **状態・意図の先行永続化**: ローカルデータベースのトランザクション内で、初期状態(`PENDING` や `INITIATED` など)と必要なペイロードを記録します。遅延の生じやすいネットワークI/Oの実行中にデータベースロックやコネクションプールのスロットを保持し続けないよう、外部へのネットワーク呼び出しを開始する前にこのトランザクションをコミットします。 2. **冪等な外部リクエスト**: 決定論的な冪等性キー(処理のUUIDなどから生成)を用いて外部APIを呼び出します。これにより、リトライが行われても実世界での副作用(二重請求など)が重複して発生することを防ぎます。 3. **状態遷移とタイムアウト処理**: API呼び出しが成功レスポンスを返した場合は、データベースレコードを `COMPLETED` に更新します。恒久的に失敗した場合は `FAILED` とマークし、補償トランザクションを実行します。呼び出しがタイムアウトしたかネットワークエラーが返された場合は、レコードを `INDETERMINATE` または `REQUIRES_RECONCILIATION` としてマークし、バックグラウンドワーカーがプロバイダーのステータス取得エンドポイントに問い合わせるか、同じ冪等性キーで再試行して状態の整合性(リコンシリエーション)を取るようにします。

use uuid::Uuid;

pub async fn process_external_charge(pool: &sqlx::PgPool, user_id: Uuid, amount: i64) -> Result<(), String> {
    let payment_id = Uuid::new_v4();
    let idempotency_key = format!("pay_{}", payment_id);

    // 1. Commit intent locally before making external network calls
    sqlx::query!(
        "INSERT INTO payments (id, user_id, amount, status, idempotency_key) VALUES ($1, $2, $3, 'PENDING', $4)",
        payment_id, user_id, amount, idempotency_key
    )
    .execute(pool)
    .await
    .map_err(|e| e.to_string())?;

    // 2. Call external API with timeout and idempotency key
    let client = reqwest::Client::new();
    let api_result = client.post("https://api.payment.com/v1/charges")
        .header("Idempotency-Key", &idempotency_key)
        .json(&serde_json::json!({ "amount": amount }))
        .timeout(std::time::Duration::from_secs(5))
        .send()
        .await;

    // 3. Update state based on outcome; handle timeouts via reconciliation
    match api_result {
        Ok(resp) if resp.status().is_success() => {
            sqlx::query!("UPDATE payments SET status = 'SUCCESS' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
        Ok(_) => {
            sqlx::query!("UPDATE payments SET status = 'FAILED' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
        Err(_) => {
            sqlx::query!("UPDATE payments SET status = 'REQUIRES_RECONCILIATION' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}
AI コーチを使ってこの質問に答えてみる

15厳格なデータ分離と動的ルーティングを備えたマルチテナントのRustバックエンドをどのように設計しますか?

マルチテナントなRustバックエンドの設計は、主にテナント識別、データ分離戦略、動的ルーティングの3つの領域で構成されます。1. **テナント抽出とコンテキスト伝播**: HTTPミドルウェア(AxumやActix-webなど)がJWT(JSON Web Token)クレーム、サブドメイン、またはヘッダーからテナント識別子を解決します。型安全な`TenantContext`を構築してリクエスト拡張(extensions)に格納します。ハンドラ側ではRustの型エクストラクタを活用し、有効なテナントコンテキストが存在しない限りビジネスロジックが実行されないよう強制します。2. **データ分離戦略**: - *カラム/行レベルセキュリティ(RLS: Row-Level Security)を用いた共有データベース*: すべてのテナントが`tenant_id`カラムを持つテーブルを共有します。接続レベルのセッション変数(例: `SET LOCAL app.tenant_id = $1`)を用いたPostgreSQLのRLSや、クエリビルダーのラッパーによって分離を強制します。 - *テナントごとのスキーマ分離(Schema-per-Tenant)*: テナント間で単一のデータベースインスタンスを共有しつつスキーマを独立させます。ルーティング時に接続ごとの検索パス(search path)を切り替えます。 - *テナントごとのデータベース分離(Database-per-Tenant)*: テナントごとに個別のデータベースを持ちます。サービス側で接続プールを動的に解決するため、プールレジストリ(例: `Arc<DashMap<TenantId, PgPool>>`やLRUプールキャッシュ)を保持します。3. **情報漏洩の防止**: 非同期のTokioアプリケーションにおいてテナント間のデータ混入を防ぐため、テナントコンテキストはタスク境界を越えて明示的に引き渡す必要があります。非同期ワーカースレッド間でのタスク移行時に整合性が崩れるため、グローバルな状態やスレッドローカルストレージ(`thread_local!`)への保存は避けるべきです。

use axum::{extract::{FromRequestParts, State}, http::request::Parts, async_trait};
use std::sync::Arc;
use dashmap::DashMap;
use sqlx::PgPool;

#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TenantId(pub String);

#[derive(Clone)]
pub struct AppState {
    pub pool_registry: Arc<DashMap<TenantId, PgPool>>,
}

#[derive(Clone)]
pub struct TenantContext {
    pub tenant_id: TenantId,
    pub pool: PgPool,
}

#[async_trait]
impl FromRequestParts<AppState> for TenantContext {
    type Rejection = (axum::http::StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
        let tenant_header = parts.headers.get("X-Tenant-ID")
            .and_then(|v| v.to_str().ok())
            .ok_or((axum::http::StatusCode::BAD_REQUEST, "Missing tenant ID"))?;
        
        let tenant_id = TenantId(tenant_header.to_string());
        
        let pool = match state.pool_registry.get(&tenant_id) {
            Some(p) => p.value().clone(),
            None => return Err((axum::http::StatusCode::NOT_FOUND, "Tenant database pool not found")),
        };

        Ok(TenantContext { tenant_id, pool })
    }
}
AI コーチを使ってこの質問に答えてみる