Característica Craft

Craft permite a los desarrolladores generar automáticamente funciones manejadoras y endpoints mediante anotaciones simples, integrando además de forma transparente con la generación de documentación OpenAPI.

Casos de Uso

La característica Craft es especialmente útil en los siguientes escenarios:

  • Cuando necesitas crear rápidamente funciones manejadoras de rutas a partir de métodos de estructuras
  • Cuando deseas reducir el código repetitivo para la extracción manual de parámetros y manejo de errores
  • Cuando necesitas generar automáticamente documentación OpenAPI para tu API
  • Cuando quieres desacoplar la lógica de negocio del framework web

Uso Básico

Para utilizar la característica Craft, necesitas importar los siguientes módulos:

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

Creación de una Estructura de Servicio

Anota tu bloque impl con la macro #[craft] para convertir los métodos de la estructura en funciones manejadoras o endpoints.

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

#[craft]
impl Opts {
    // Constructor
    fn new(state: i64) -> Self {
        Self { state }
    }
    
    // Más métodos...
}

Creación de Funciones Manejadoras

Usa #[craft(handler)] para convertir un método en una función manejadora:

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

Este método se convertirá en una función manejadora que:

  • Extrae automáticamente los valores left y right de los parámetros de consulta
  • Accede al state de la estructura
  • Devuelve el resultado del cálculo como una respuesta de cadena

Creación de Endpoints

Usa #[craft(endpoint)] para convertir un método en 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()
}

Los endpoints pueden utilizar Arc para compartir estado, lo cual es especialmente útil al manejar solicitudes concurrentes.

Endpoints Estáticos

También puedes crear endpoints estáticos que no dependen del estado de la instancia:

#[craft(endpoint(responses((status_code = 400, description = "Parámetros de solicitud incorrectos."))))]
pub fn add3(left: QueryParam<i64>, right: QueryParam<i64>) -> String {
    (*left + *right).to_string()
}

En este ejemplo, se añade una descripción personalizada de respuesta de error, que se reflejará en la documentación OpenAPI generada.

Extractores de Parámetros

El módulo oapi::extract de Salvo proporciona varios extractores de parámetros, siendo los más comunes:

  • QueryParam<T>: Extrae parámetros de cadenas de consulta
  • PathParam<T>: Extrae parámetros de rutas URL
  • FormData<T>: Extrae parámetros de datos de formulario
  • JsonBody<T>: Extrae parámetros de cuerpos de solicitud JSON

Estos extractores manejan automáticamente el análisis de parámetros y la conversión de tipos, simplificando enormemente la escritura de funciones manejadoras.

Integración con OpenAPI

La característica Craft puede generar automáticamente documentación de API conforme a la especificación OpenAPI. En el ejemplo:

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

// Generar documentación OpenAPI
let doc = OpenApi::new("API de Ejemplo", "0.0.1").merge_router(&router);

// Añadir rutas de documentación OpenAPI y 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 esta configuración, la documentación de la API estará disponible en el endpoint /api-doc/openapi.json, y Swagger UI será accesible en la ruta /swagger-ui.

Ejemplo Completo

A continuación se muestra un ejemplo completo que demuestra cómo usar la característica Craft para crear tres tipos diferentes de endpoints:

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