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 機能を使用して3種類の異なるエンドポイントを作成する完全な例です:

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