Craft 特性

Craft 允許開發者透過簡潔的註解方式自動生成處理函數與端點,同時與 OpenAPI 文件生成無縫整合。

使用情境

Craft 特性在以下情境中特別實用:

  • 當你需要從結構體方法快速建立路由處理函數時
  • 當你希望減少手動編寫參數提取與錯誤處理的樣板程式碼時
  • 當你需要為 API 自動生成 OpenAPI 文件時
  • 當你希望將業務邏輯與 Web 框架解耦時

基本用法

使用 Craft 特性需要引入以下模組:

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

建立服務結構體

使用 #[craft] 巨集標註你的 impl 區塊,即可將結構體方法轉換為處理函數或端點。

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

#[craft]
impl Opts {
    // 建構函數
    fn new(state: i64) -> Self {
        Self { state }
    }
    
    // 更多方法...
}

建立處理函數

使用 #[craft(handler)] 將方法轉換為處理函數:

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

此方法將成為一個處理函數,它會:

  • 自動從查詢參數提取 leftright
  • 存取結構體中的 state 狀態
  • 回傳計算結果作為字串回應

建立端點

使用 #[craft(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()
}

端點可利用 Arc 來共享狀態,這在處理並行請求時特別有用。

靜態端點

你也可以建立不依賴實例狀態的靜態端點:

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

在此範例中,還加入了自訂錯誤回應的描述,這將反映在生成的 OpenAPI 文件中。

參數提取器

Salvo 的 oapi::extract 模組提供了多種參數提取器,最常見的包括:

  • QueryParam<T>:從查詢字串提取參數
  • PathParam<T>:從 URL 路徑提取參數
  • FormData<T>:從表單資料提取參數
  • JsonBody<T>:從 JSON 請求主體提取參數

這些提取器會自動完成參數解析與型別轉換,大幅簡化處理函數的編寫。

與 OpenAPI 整合

Craft 特性可自動生成符合 OpenAPI 規範的 API 文件。在範例中:

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

// 生成 OpenAPI 文件
let doc = OpenApi::new("Example API", "0.0.1").merge_router(&router);

// 加入 OpenAPI 文件與 Swagger UI 路由
let router = router
    .push(doc.into_router("/api-doc/openapi.json"))
    .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));

如此配置後,API 文件將在 /api-doc/openapi.json 端點提供,Swagger UI 則可在 /swagger-ui 路徑存取。

完整範例

以下是一個完整範例,展示如何使用 Craft 特性建立三種不同類型的端點:

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