Craft Features

Craft allows developers to automatically generate handlers and endpoints using 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 handlers from struct methods.
  • When you want to reduce the boilerplate code for manually writing 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

Using the Craft feature requires importing the following modules:

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

Creating a Service Struct

Using the #[craft] macro to annotate your impl block allows you to convert struct methods into handlers or endpoints.

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

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

Creating a Handler

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

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

This method becomes a handler that:

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

Creating an Endpoint

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 Endpoint

You can also create static endpoints that do not 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 description for a custom error response has also been added, which will be reflected in the generated OpenAPI documentation.

Parameter Extractors

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

  • QueryParam<T>: Extracts parameters from the query string.
  • PathParam<T>: Extracts parameters from the URL path.
  • FormData<T>: Extracts parameters from form data.
  • JsonBody<T>: Extracts parameters from the JSON request body.

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

OpenAPI Integration

The Craft feature can automatically generate API documentation that conforms to 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 routes for OpenAPI documentation and Swagger UI
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 available 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 the 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 routes for OpenAPI documentation and Swagger UI
    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;
}