Padroneggiare Quest'Arte

Perché Creare Questo Framework

Da principiante, ho faticato a comprendere framework esistenti come actix-web e Rocket. Quando ho tentato di riscrivere un precedente servizio web Go in Rust, ogni framework sembrava più complesso di quelli disponibili in Go. Considerando la già ripida curva di apprendimento di Rust, perché complicare ulteriormente i framework web?

Quando Tokio ha introdotto il framework Axum, sono stato entusiasta, pensando di poter finalmente smettere di mantenere il mio framework web. Tuttavia, ho presto realizzato che, sebbene Axum sembrasse semplice, richiedeva acrobazie di tipi e definizioni generiche estese. Creare anche un middleware basilare richiedeva una profonda competenza in Rust e la scrittura di grandi quantità di codice boilerplate oscuro.

Così, ho deciso di continuare a mantenere il mio framework web unico—uno che sia intuitivo, ricco di funzionalità e amichevole per i principianti.

Salvo è Adatto a Te?

Salvo è semplice ma completo e potente, probabilmente il più forte nell'ecosistema Rust. Nonostante le sue capacità, rimane facile da imparare e usare, risparmiandoti qualsiasi dolore di "auto-castrazione".

  • Ideale per i principianti di Rust: le operazioni CRUD sono comuni e, con Salvo, tali compiti sembrano semplici come con framework in altri linguaggi (ad esempio, Express, Koa, Gin, Flask). In alcuni aspetti, Salvo è persino più astratto e conciso.

  • Adatto alla produzione: se intendi utilizzare Rust in produzione per server robusti e ad alta velocità, Salvo fa al caso tuo. Sebbene non sia ancora alla versione 1.0, le sue funzionalità principali hanno subito anni di iterazione, garantendo stabilità e risoluzione tempestiva dei problemi.

  • Perfetto per te, specialmente se i tuoi capelli si stanno diradando e cadendo ogni giorno.

Raggiungere la Semplicità

Hyper gestisce molte implementazioni di basso livello, rendendolo una base affidabile per le esigenze generali. Salvo segue questo approccio, offrendo un sistema di routing potente e flessibile insieme a funzionalità essenziali come Acme, OpenAPI e autenticazione JWT.

Salvo unifica Handlers e Middleware: il Middleware è un Handler. Entrambi sono collegati al Router tramite hoop. In sostanza, elaborano Request e possono scrivere dati in Response. Un Handler riceve tre parametri: Request, Depot (per dati temporanei durante l'elaborazione della richiesta) e Response.

Per comodità, puoi omettere parametri non necessari o ignorare il loro ordine.

use salvo::prelude::*;

#[handler]
async fn hello_world(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.render("Hello world");
}
#[handler]
async fn hello_world(res: &mut Response) {
    res.render("Hello world");
}

L'API di routing è eccezionalmente semplice ma potente. Per casi d'uso tipici, devi concentrarti solo sul tipo Router.

Inoltre, se una struct implementa tratti rilevanti, Salvo può generare automaticamente documentazione OpenAPI, estrarre parametri, gestire errori con garbo e restituire messaggi user-friendly. Questo rende la scrittura di handler intuitiva come scrivere funzioni ordinarie. Esploreremo queste funzionalità in dettaglio più avanti. Ecco un esempio:

#[endpoint(tags("message_logs"))]
pub async fn create_message_log_handler(
    input: JsonBody<CreateOrUpdateMessageLog>,
    depot: &mut Depot,
) -> APPResult<Json<MessageLog>> {
    let db = utils::get_db(depot)?;
    let log = create_message_log(&input, db).await?;
    Ok(Json(log))
}

In questo esempio, JsonBody<CreateOrUpdateMessageLog> analizza automaticamente il JSON dal corpo della richiesta nel tipo CreateOrUpdateMessageLog (supportando più fonti dati e tipi annidati). La macro #[endpoint] genera documentazione OpenAPI per questo endpoint, semplificando l'estrazione dei parametri e la gestione degli errori.

Sistema di Routing

Credo che il sistema di routing di Salvo si distingua. I router possono essere piatti o ad albero, distinguendo tra alberi di logica di business e alberi di directory di accesso. L'albero di logica di business organizza i router in base alle esigenze aziendali, che potrebbero non allinearsi con l'albero di directory di accesso.

Tipicamente, le route sono scritte così:

Router::new().path("articles").get(list_articles).post(create_article);
Router::new()
    .path("articles/{id}")
    .get(show_article)
    .patch(edit_article)
    .delete(delete_article);

Spesso, visualizzare articoli ed elencarli non richiede il login dell'utente, ma creare, modificare o eliminare articoli sì. Il sistema di routing annidato di Salvo gestisce questo elegantemente. Possiamo raggruppare le route pubbliche insieme:

Router::new()
    .path("articles")
    .get(list_articles)
    .push(Router::new().path("{id}").get(show_article));

Quindi, raggruppare le route protette con middleware per l'autenticazione:

Router::new()
    .path("articles")
    .hoop(auth_check)
    .post(create_article)
    .push(Router::new().path("{id}").patch(edit_article).delete(delete_article));

Anche se entrambi i router condividono lo stesso path("articles"), possono essere aggiunti allo stesso router genitore, risultando in:

Router::new()
    .push(
        Router::new()
            .path("articles")
            .get(list_articles)
            .push(Router::new().path("{id}").get(show_article)),
    )
    .push(
        Router::new()
            .path("articles")
            .hoop(auth_check)
            .post(create_article)
            .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
    );

{id} corrisponde a un segmento di percorso. Tipicamente, un id di articolo è numerico, quindi possiamo limitare la corrispondenza con una regex: r"{id:/\d+/}".