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()
}
このメソッドは以下の特徴を持つハンドラ関数になります:
- クエリパラメータから
left と right の値を自動抽出
- 構造体内の
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種類の異なるエンドポイントを作成する完全な例です:
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;
}
[package]
name = "example-craft"
version.workspace = true
edition.workspace = true
publish.workspace = true
rust-version.workspace = true
[dependencies]
salvo = { workspace = true, features = ["craft", "oapi"] }
tokio = { workspace = true, features = ["macros"] }
tracing.workspace = true
tracing-subscriber.workspace = true