Funzionalità Craft

Craft consente agli sviluppatori di generare automaticamente funzioni handler ed endpoint tramite semplici annotazioni, integrandosi perfettamente con la generazione di documentazione OpenAPI.

Casi d'uso

La funzionalità Craft risulta particolarmente utile nei seguenti scenari:

  • Quando è necessario creare rapidamente funzioni handler per le rotte a partire da metodi di struct
  • Quando si desidera ridurre il codice boilerplate per l'estrazione manuale dei parametri e la gestione degli errori
  • Quando occorre generare automaticamente documentazione OpenAPI per le proprie API
  • Quando si vuole disaccoppiare la logica di business dal framework web

Utilizzo di base

Per utilizzare la funzionalità Craft, è necessario importare i seguenti moduli:

use salvo::oapi::extract::*;
use salvo::prelude::*;

Creazione di una Struct di Servizio

Annotare il blocco impl con la macro #[craft] per convertire i metodi della struct in funzioni handler o endpoint.

#[derive(Clone)]
pub struct Opts {
    state: i64,
}

#[craft]
impl Opts {
    // Costruttore
    fn new(state: i64) -> Self {
        Self { state }
    }
    
    // Altri metodi...
}

Creazione di Funzioni Handler

Utilizzare #[craft(handler)] per convertire un metodo in una funzione handler:

#[craft(handler)]
fn add1(&self, left: QueryParam<i64>, right: QueryParam<i64>) -> String {
    (self.state + *left + *right).to_string()
}

Questo metodo diventerà una funzione handler che:

  • Estrae automaticamente i valori left e right dai parametri di query
  • Accede allo state dalla struct
  • Restituisce il risultato del calcolo come risposta stringa

Creazione di Endpoint

Utilizzare #[craft(endpoint)] per convertire un metodo in un endpoint:

#[craft(endpoint)]
pub(crate) fn add2(
    self: ::std::sync::Arc<Self>,
    left: QueryParam<i64>,
    right: QueryParam<i64>,
) -> String {
    (self.state + *left + *right).to_string()
}

Gli endpoint possono utilizzare Arc per condividere lo stato, particolarmente utile nella gestione di richieste concorrenti.

Endpoint Statici

È possibile creare anche endpoint statici che non dipendono dallo stato dell'istanza:

#[craft(endpoint(responses((status_code = 400, description = "Parametri della richiesta errati."))))]
pub fn add3(left: QueryParam<i64>, right: QueryParam<i64>) -> String {
    (*left + *right).to_string()
}

In questo esempio viene aggiunta una descrizione personalizzata per la risposta di errore, che verrà riflessa nella documentazione OpenAPI generata.

Estrattori di Parametri

Il modulo oapi::extract di Salvo fornisce vari estrattori di parametri, tra i più comuni:

  • QueryParam<T>: Estrae parametri dalle query string
  • PathParam<T>: Estrae parametri dai percorsi URL
  • FormData<T>: Estrae parametri dai dati di form
  • JsonBody<T>: Estrae parametri dal corpo JSON delle richieste

Questi estrattori gestiscono automaticamente il parsing dei parametri e la conversione dei tipi, semplificando notevolmente la scrittura delle funzioni handler.

Integrazione con OpenAPI

La funzionalità Craft può generare automaticamente documentazione API conforme alla specifica OpenAPI. Nell'esempio:

let router = Router::new()
    .push(Router::with_path("add1").get(opts.add1()))
    .push(Router::with_path("add2").get(opts.add2()))
    .push(Router::with_path("add3").get(Opts::add3()));

// Generazione documentazione OpenAPI
let doc = OpenApi::new("API di Esempio", "0.0.1").merge_router(&router);

// Aggiunta rotte per documentazione OpenAPI e Swagger UI
let router = router
    .push(doc.into_router("/api-doc/openapi.json"))
    .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));

Con questa configurazione, la documentazione API sarà disponibile all'endpoint /api-doc/openapi.json, mentre Swagger UI sarà accessibile al percorso /swagger-ui.

Esempio Completo

Di seguito un esempio completo che dimostra come utilizzare la funzionalità Craft per creare tre tipi diversi di endpoint:

main.rs
Cargo.toml
use salvo::oapi::extract::*;
use salvo::prelude::*;
use std::sync::Arc;

// Options struct holding a state value for calculations
#[derive(Clone)]
pub struct Opts {
    state: i64,
}

// Implement methods for Opts using the craft macro for API generation
#[craft]
impl Opts {
    // Constructor for Opts
    fn new(state: i64) -> Self {
        Self { state }
    }

    // Handler method that adds state value to two query parameters
    #[craft(handler)]
    fn add1(&self, left: QueryParam<i64>, right: QueryParam<i64>) -> String {
        (self.state + *left + *right).to_string()
    }

    // Endpoint method using Arc for shared state
    #[craft(endpoint)]
    pub(crate) fn add2(
        self: ::std::sync::Arc<Self>,
        left: QueryParam<i64>,
        right: QueryParam<i64>,
    ) -> String {
        (self.state + *left + *right).to_string()
    }

    // Static endpoint method with custom error response
    #[craft(endpoint(responses((status_code = 400, description = "Wrong request parameters."))))]
    pub fn add3(left: QueryParam<i64>, right: QueryParam<i64>) -> String {
        (*left + *right).to_string()
    }
}

#[tokio::main]
async fn main() {
    // Create shared state with initial value 1
    let opts = Arc::new(Opts::new(1));

    // Configure router with three endpoints:
    // - /add1: Uses instance method with state
    // - /add2: Uses Arc-wrapped instance method
    // - /add3: Uses static method without state
    let router = Router::new()
        .push(Router::with_path("add1").get(opts.add1()))
        .push(Router::with_path("add2").get(opts.add2()))
        .push(Router::with_path("add3").get(Opts::add3()));

    // Generate OpenAPI documentation
    let doc = OpenApi::new("Example API", "0.0.1").merge_router(&router);

    // Add OpenAPI documentation and Swagger UI routes
    let router = router
        .push(doc.into_router("/api-doc/openapi.json"))
        .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));

    // Start server on localhost:8698
    let acceptor = TcpListener::new("127.0.0.1:8698").bind().await;
    Server::new(acceptor).serve(router).await;
}