Craft Feature

Craft allows developers to automatically generate handler functions and endpoints through simple annotations, while seamlessly integrating with OpenAPI documentation generation.

Use Cases

The Craft feature is particularly useful in the following scenarios:

  • When you need to quickly create route handler functions from struct methods
  • When you want to reduce boilerplate code for manual parameter extraction and error handling
  • When you need to automatically generate OpenAPI documentation for your API
  • When you want to decouple business logic from the web framework

Basic Usage

To use the Craft feature, you need to import the following modules:

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

Creating a Service Struct

Annotate your impl block with the #[craft] macro to convert struct methods into handler functions or endpoints.

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

#[craft]
impl Opts {
    // Constructor
    fn new(state: i64) -> Self {
        Self { state }
    }
    
    // More methods...
}

Creating Handler Functions

Use #[craft(handler)] to convert a method into a handler function:

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

This method will become a handler function that:

  • Automatically extracts left and right values from query parameters
  • Accesses the state from the struct
  • Returns the calculation result as a string response

Creating Endpoints

Use #[craft(endpoint)] to convert a method into an 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()
}

Endpoints can utilize Arc to share state, which is particularly useful when handling concurrent requests.

Static Endpoints

You can also create static endpoints that don't depend on instance state:

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

In this example, a custom error response description is added, which will be reflected in the generated OpenAPI documentation.

Parameter Extractors

Salvo's oapi::extract module provides various parameter extractors, with the most common including:

  • QueryParam<T>: Extracts parameters from query strings
  • PathParam<T>: Extracts parameters from URL paths
  • FormData<T>: Extracts parameters from form data
  • JsonBody<T>: Extracts parameters from JSON request bodies

These extractors automatically handle parameter parsing and type conversion, greatly simplifying the writing of handler functions.

Integration with OpenAPI

The Craft feature can automatically generate API documentation compliant with the OpenAPI specification. In the example:

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

With this configuration, the API documentation will be available at the /api-doc/openapi.json endpoint, and Swagger UI will be accessible at the /swagger-ui path.

Complete Example

Below is a complete example demonstrating how to use the Craft feature to create three different types of endpoints:

use salvo::oapi::extract::*;
use salvo::prelude::*;
use std::sync::Arc;

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

#[craft]
impl Opts {
    fn new(state: i64) -> Self {
        Self { state }
    }

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

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

    #[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 an initial value of 1
    let opts = Arc::new(Opts::new(1));

    // Configure routes for three endpoints
    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 the server on localhost:5800
    let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}