Preparazione al colloquio Rust

Domande per Colloqui Rust Backend Developer

15 domande selezionate per colloquio Rust backend, raggruppate per livello di seniority. Usale per ripassare i fondamenti, i compromessi pratici e il ragionamento di produzione a livello senior.

Avvia un Colloquio Rust AINessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Domande Junior

1Spiega le regole di *ownership* (proprietà) di Rust e come esse influenzano le scelte pratiche di backend come la clonazione, il *borrowing* (prestito) e la condivisione dello stato dell'applicazione.

Rust assegna a ogni valore un singolo proprietario. Spostare un valore non `Copy` trasferisce la proprietà, e il valore viene rilasciato quando il suo proprietario esce dall'ambito (*scope*). Il codice può prendere in prestito dati in modo immutabile tramite riferimenti condivisi o in modo mutabile tramite un riferimento mutabile esclusivo. Nel codice di backend, questo influenza se una funzione assume la proprietà, prende in prestito temporaneamente, clona per ottenere un valore indipendente di proprietà, o condivide lo stato a lungo termine tramite *handle* come `Arc`, con sincronizzazione o altre primitive di concorrenza quando è necessaria la mutazione condivisa.

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;
}
Prova a rispondere a questa domanda con un coach IA

2Qual è la differenza tra il prestito con `&T` e `&mut T`, e come queste regole influenzano le firme delle API e dei gestori?

`&T` è un riferimento condiviso: consente l'accesso in sola lettura e molti riferimenti condivisi allo stesso valore possono essere attivi contemporaneamente. `&mut T` è un riferimento mutabile esclusivo: consente la mutazione, ma mentre è attivo nessun altro riferimento condiviso o mutabile allo stesso valore può essere utilizzato. Queste regole modellano le API facendo in modo che le funzioni di sola lettura accettino `&T` o tipi presi in prestito più specifici come `&str`, mentre le funzioni di mutazione accettano `&mut T`, prendono la proprietà (ownership) o usano la mutabilità interna (interior mutability)/sincronizzazione. I gestori di backend di solito evitano l'accesso `&mut` diretto allo stato condiviso dell'applicazione attraverso richieste concorrenti e invece usano handle condivisi più sincronizzazione o altri pattern di proprietà.

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
}
Prova a rispondere a questa domanda con un coach IA

3Come funziona `Box` e quali problemi risolve l'allocazione sullo heap tramite `Box`?

`Box<T>` è un puntatore intelligente proprietario il cui `T` è memorizzato sullo heap, mentre il riferimento `Box` è un valore a dimensione fissa simile a un puntatore. Ha una singola proprietà e dealloca/libera l'allocazione sullo heap quando esce dallo scope (ambito). `Box` fornisce indirezione, il che aiuta con valori grandi, tipi ricorsivi che necessitano di una dimensione nota, valori a dimensione dinamica e oggetti trait come `Box<dyn Trait>`. Spostare un `Box` trasferisce la proprietà dell'allocazione senza spostare o copiare il valore stesso memorizzato sullo heap.

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

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Prova a rispondere a questa domanda con un coach IA

4Descrivi `Option` e `Result` e i principali modi idiomatici per gestire l'assenza di un valore e le operazioni fallibili.

Un `Option<T>` rappresenta `Some(T)` o `None` ed è usato per l'assenza attesa di un valore. Un `Result<T, E>` rappresenta `Ok(T)` o `Err(E)` ed è usato per operazioni fallibili dove le informazioni sull'errore sono importanti. La gestione idiomatica include `match`, `if let`/`let else`, combinatori come `map`, `and_then`, `ok_or`, `unwrap_or`, e `unwrap_or_else`, e l'operatore `?` per la propagazione anticipata da funzioni che restituiscono tipi `Option` o `Result` compatibili. `unwrap` ed `expect` sono meglio riservati per invarianti, test, prototipi o situazioni irrecuperabili, non per normali errori di backend recuperabili.

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);
Prova a rispondere a questa domanda con un coach IA

5Come funziona l'operatore `?` e cosa permette la conversione tra diversi tipi di errore?

L'operatore `?` è una scorciatoia per propagare gli errori. Per un `Result`, `expr?` restituisce il valore `Ok` e continua, oppure restituisce anticipatamente dalla funzione corrente con un `Err`; per un `Option`, restituisce il valore `Some` oppure restituisce `None`. La funzione che la contiene deve restituire un tipo compatibile. Per `Result`, diversi tipi di errore possono essere composti perché l'errore sorgente viene convertito nel tipo di errore della funzione utilizzando la conversione in stile `From`/`Into`, spesso implementata manualmente o derivata con crate di aiuto come `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)
}
Prova a rispondere a questa domanda con un coach IA

6Spiega il pattern matching sugli `enum` e come il matching esaustivo migliora la correttezza del dominio e dell'API.

Gli `enum` modellano un valore che può essere una di un set fisso di varianti, e le varianti possono contenere dati. L'espressione `match` si ramifica in base alla variante e può destrutturare i dati al suo interno. Rust verifica che i match sugli `enum` siano esaustivi, a meno che non venga utilizzato un wildcard o un pattern catch-all. Questo migliora la correttezza del dominio e dell'API perché quando viene aggiunta una nuova variante di stato o di risposta, il compilatore può forzare il codice che effettua il matching su di essa a decidere come gestire il nuovo caso, invece di ignorarlo silenziosamente.

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}"),
    }
}
Prova a rispondere a questa domanda con un coach IA

7Cosa sono i wrapper newtype e come migliorano la sicurezza dei tipi per ID, denaro, token e segreti?

Un wrapper newtype è un tipo Rust distinto, spesso una `struct` a tupla a campo singolo, attorno a una rappresentazione esistente, come `struct UserId(Uuid);` o `struct AccessToken(String);`. Migliora la sicurezza dei tipi perché il compilatore non confonderà valori semanticamente diversi che condividono lo stesso tipo sottostante, come `UserId` rispetto a `OrderId`, centesimi rispetto a dollari, o stringhe grezze rispetto a token validati. I newtype possono anche centralizzare le regole di costruzione e validazione e controllare i trait come `Display`, `Debug`, `Serialize` o il comportamento di redazione per i segreti.

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
}
Prova a rispondere a questa domanda con un coach IA

Domande Middle

8Come funziona il `trait Drop`, inclusi l'ordine di rilascio, la pulizia RAII e le limitazioni relative alla pulizia asincrona?

`Drop` è l'hook di pulizia deterministico di Rust. Quando un valore posseduto viene distrutto, ad esempio quando esce dal suo ambito, Rust chiama automaticamente il suo metodo `drop(&mut self)` se implementa `Drop`, quindi rilascia i suoi campi. Questo supporta il RAII (Resource Acquisition Is Initialization): risorse come file, socket, lock, transazioni, permessi o buffer sono legate a valori/guardie che le possiedono e vengono rilasciate quando tali valori vengono rilasciati. Le variabili locali vengono rilasciate in ordine inverso di creazione; per una `struct` con `Drop`, il metodo `drop` della `struct` viene eseguito prima che i suoi campi siano rilasciati, e i campi sono rilasciati nell'ordine di dichiarazione. `Drop::drop` è sincrono e non può essere `async` o `awaited`, quindi una pulizia asincrona elegante richiede solitamente un metodo esplicito asincrono di chiusura/spegnimento/flush o un'altra progettazione; `Drop` può eseguire solo pulizie sincrone o 'best-effort'.

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");
}
Prova a rispondere a questa domanda con un coach IA

9Spiega le *lifetime* e perché le annotazioni di *lifetime* esplicite sono a volte richieste nelle API Rust.

Una *lifetime* descrive per quanto tempo un riferimento è valido, consentendo al compilatore di rifiutare i riferimenti pendenti (*dangling references*). Le annotazioni di *lifetime* sono vincoli in fase di compilazione che esprimono relazioni tra riferimenti; non estendono la *lifetime* dei dati sottostanti né aggiungono comportamento a runtime. Molte firme semplici sono gestite dall'elisione delle *lifetime* (lifetime elision), ma le annotazioni esplicite sono necessarie quando il compilatore non può inferire come i riferimenti di input e output si relazionano, quando i tipi memorizzano riferimenti, o quando un'API deve esprimere vincoli di prestito generici.

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);
}
Prova a rispondere a questa domanda con un coach IA

10Cosa significa la lifetime `'static` e come interagisce con i task spawnati e la progettazione delle API di backend?

La keyword `'static` su un riferimento significa che i dati referenziati sono validi per l'intera durata del programma, come con i literals di stringa o gli elementi `static`. Un bound come `T: 'static` significa che i valori di tipo `T` non contengono riferimenti presi in prestito (borrowed references) non-`'static`, quindi possono essere mantenuti per una lifetime arbitraria; non significa che il valore stesso debba vivere per sempre o non possa essere eliminato (dropped). I task spawnati spesso richiedono future `'static` perché l'executor potrebbe continuare a eseguirli dopo che il frame dello stack di spawn (spawning stack frame) è tornato. Nel codice di backend, questo di solito significa spostare i dati di proprietà (owned data) nel task con `async move`, usando tipi di proprietà (owned types), o clonare handle condivisi come `Arc`, piuttosto che prendere in prestito variabili locali (borrowing local variables). Le API dovrebbero usare bound `'static` per callback memorizzati, job in background e stato a lunga durata, ma evitare bound `'static` non necessari per operazioni prese in prestito di breve durata (short-lived borrowed operations).

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);
    });
}
Prova a rispondere a questa domanda con un coach IA

11Descrivi come Rust utilizza i `trait` per il polimorfismo e confronta `impl Trait`, i generici e `dyn Trait`.

Rust utilizza i `trait` per esprimere comportamenti condivisi. I vincoli generici come `fn f<T: Trait>(x: T)` utilizzano solitamente il *dispatch* statico tramite monomorfizzazione. `impl Trait` in posizione di argomento è principalmente una scorciatoia per un parametro generico anonimo. `impl Trait` in posizione di ritorno nasconde un tipo di ritorno concreto scelto dalla funzione. `dyn Trait` è un *trait object* utilizzato dietro un puntatore come `&dyn Trait` o `Box<dyn Trait>`; utilizza il *dispatch* dinamico tramite una *vtable* e supporta l'erasure dei tipi a runtime/valori eterogenei soggetti a restrizioni di *object safety*.

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())); }
}
Prova a rispondere a questa domanda con un coach IA

12Cosa sono i tipi associati (associated types) nei trait e quando sono preferibili ai parametri di tipo generici?

I tipi associati (associated types) sono segnaposto di tipo nominati dichiarati all'interno di un trait e specificati da ogni implementazione, ad esempio `Iterator::Item`. Sono preferibili quando il tipo fa parte del contratto del trait e ogni implementatore ha una scelta naturale per esso. I parametri di tipo generici sono preferibili quando il chiamante dovrebbe scegliere il tipo o quando lo stesso implementatore potrebbe aver bisogno di implementazioni multiple per diverse scelte di tipo.

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)
}
Prova a rispondere a questa domanda con un coach IA

Domande Senior

13Come implementeresti il pattern transactional outbox in un servizio Rust?

Per implementare il pattern Transactional Outbox in un servizio Rust, scrivi gli aggiornamenti dei dati di dominio e i payload degli eventi in uscita atomicamente all'interno di una singola transazione di database (ad esempio, utilizzando `sqlx::Transaction`). Una tabella `outbox` memorizza l'argomento di destinazione, il payload e lo stato o il timestamp di creazione. Un worker asincrono in background (o un processo di Change Data Capture come Debezium) quindi effettua il polling o lo streaming degli eventi in sospeso e li pubblica sul message broker (come Kafka, RabbitMQ o NATS). Quando si utilizza un worker di polling in Rust (ad esempio, in un ciclo `tokio::spawn`), l'uso di `SELECT ... FOR UPDATE SKIP LOCKED` consente a più istanze del servizio di recuperare e inviare righe dell'outbox contemporaneamente senza contesa di blocco. Una volta che la consegna del broker è stata riconosciuta, il worker aggiorna lo stato del record outbox o elimina la riga. Poiché i tentativi di rete possono comportare una consegna at-least-once, i consumer a valle devono essere progettati per gestire i messaggi in modo idempotente.

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;
    }
}
Prova a rispondere a questa domanda con un coach IA

14Come progetteresti un'operazione a più fasi che aggiorna un database e chiama un'API esterna?

Progettare un'operazione a più fasi che comprende una scrittura su database e una chiamata ad un'API esterna richiede di gestire fallimenti parziali e partizioni di rete senza affidarsi al `two-phase commit` (2PC). 1. **Persisti lo Stato / Intento per Primo**: Registra uno stato iniziale (come `PENDING` o `INITIATED`) e il payload richiesto all'interno della transazione del database locale. Effettua il commit di questa transazione prima di iniziare la chiamata di rete esterna per evitare di mantenere blocchi del database e slot del pool di connessioni durante I/O di rete lenti. 2. **Richiesta Esterna Idempotente**: Chiama l'API esterna utilizzando una chiave di idempotenza deterministica (ad es., derivata dall'UUID dell'operazione). Questo garantisce che tentativi ripetuti non causino effetti collaterali duplicati nel mondo reale (come un doppio addebito). 3. **Transizione di Stato e Gestione dei Timeout**: Se la chiamata API restituisce una risposta di successo, aggiorna il record del database a `COMPLETED`. Se fallisce permanentemente, contrassegnalo come `FAILED` ed esegui azioni compensative. Se la chiamata va in timeout o restituisce un errore di rete, contrassegna il record come `INDETERMINATE` / `REQUIRES_RECONCILIATION` e lascia che un worker in background riconcili lo stato interrogando l'endpoint di stato del provider o riprovando con la stessa chiave di idempotenza.

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(())
}
Prova a rispondere a questa domanda con un coach IA

15Come progetteresti un backend Rust multi-tenant con isolamento stretto dei dati e routing dinamico?

La progettazione di un backend Rust multi-tenant coinvolge tre aree chiave: identificazione del tenant, strategia di isolamento dei dati e routing dinamico. 1. **Estrazione del tenant e propagazione del contesto**: Un middleware HTTP (ad esempio, in Axum o Actix-web) risolve l'identità del tenant da claim JWT (JSON Web Token), sottodomini o header. Costruisce un `TenantContext` fortemente tipizzato memorizzato nelle estensioni della richiesta. Gli handler utilizzano estrattori di tipo Rust per garantire che la logica di business non possa essere eseguita senza un contesto tenant valido. 2. **Strategia di isolamento dei dati**: * ***Database condiviso con sicurezza a livello di colonna/riga (RLS - Row-Level Security)***: Tutti i tenant condividono le tabelle con una colonna `tenant_id`. L'isolamento è applicato tramite RLS di PostgreSQL utilizzando variabili di sessione a livello di connessione (ad esempio, `SET LOCAL app.tenant_id = $1`) o wrapper del query builder. * ***Schema per tenant***: I tenant condividono un'istanza di database ma hanno schemi isolati; il routing regola il percorso di ricerca per ogni connessione. * ***Database per tenant***: I tenant hanno database separati. Il servizio mantiene un registro di pool (ad esempio, `Arc<DashMap<TenantId, PgPool>>` o una cache di pool LRU (Least Recently Used)) per risolvere dinamicamente i pool di connessioni. 3. **Prevenzione delle fughe**: Per evitare la contaminazione tra tenant nelle applicazioni Tokio asincrone, il contesto del tenant deve essere passato esplicitamente attraverso i confini dei task piuttosto che essere memorizzato in stato globale o in memoria thread-local (`thread_local!`), che si interrompe durante le migrazioni dei thread worker asincroni.

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 })
    }
}
Prova a rispondere a questa domanda con un coach IA