OpenAPIドキュメント生成

OpenAPIはRESTful APIのインターフェース設計を記述するためのオープンソース仕様です。JSONまたはYAML形式でAPIのリクエストとレスポンスの構造、パラメータ、戻り値の型、エラーコードなどの詳細を定義し、クライアントとサーバー間の通信を明確かつ標準化します。

OpenAPIはもともとSwagger仕様のオープンソース版でしたが、現在は独立したプロジェクトとなり、多くの大企業や開発者から支持されています。OpenAPI仕様を使用することで、開発チームの協働が改善され、コミュニケーションコストが削減され、開発効率が向上します。また、OpenAPIはAPIドキュメント、モックデータ、テストケースなどを自動生成するツールも提供しており、開発とテスト作業を容易にします。

SalvoはOpenAPIの統合を提供しています(utoipaから改変)。Salvoは自身の特徴に基づき、Handlerから関連するOpenAPIデータ型情報を非常にエレガントに自動取得します。さらに、SalvoはSwaggerUI、scalar、rapidoc、redocなどの人気のあるオープンソースのOpenAPIインターフェースも統合しています。

Rustの型名が長く、OpenAPIでの使用に適さない場合があるため、salvo-oapiNamer型を提供しており、必要に応じてルールをカスタマイズし、OpenAPIでの型名を変更できます。

サンプルコード

main.rs
Cargo.toml
oapi-hello/src/main.rs
use salvo::oapi::extract::*;
use salvo::prelude::*;

#[endpoint]
async fn hello(name: QueryParam<String, false>) -> String {
    format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();

    let router = Router::new().push(Router::with_path("hello").get(hello));

    let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);

    let router = router
        .unshift(doc.into_router("/api-doc/openapi.json"))
        .unshift(SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui"));

    let acceptor = TcpListener::new("0.0.0.0:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}

ブラウザでhttp://localhost:5800/swagger-uiを入力すると、Swagger UIのページが表示されます。

SalvoのOpenAPI統合は非常にエレガントです。上記のサンプルでは、通常のSalvoプロジェクトと比べて、以下の手順を実行しただけです:

  • Cargo.tomloapi機能を有効化:salvo = { workspace = true, features = ["oapi"] };

  • #[handler]#[endpoint]に置換;

  • name: QueryParam<String, false>を使用してクエリ文字列の値を取得。http://localhost/hello?name=chrisにアクセスすると、このnameクエリ文字列が解析されます。QueryParam<String, false>falseはこのパラメータが省略可能であることを意味します。http://localhost/helloにアクセスしてもエラーになりません。逆に、QueryParam<String, true>はこのパラメータが必須であることを意味し、提供されない場合はエラーが返されます。

  • OpenAPIを作成し、対応するRouterを作成。OpenApi::new("test api", "0.0.1").merge_router(&router)merge_routerは、このOpenAPIが特定のルートを解析して、それとその子ルートから必要なドキュメント情報を取得することを意味します。一部のルートのHandlerはドキュメント生成情報を提供していない可能性があり、これらのルートは無視されます。例えば、#[endpoint]マクロではなく#[handler]マクロで定義されたHandlerです。つまり、実際のプロジェクトでは、開発の進捗などの理由で、OpenAPIドキュメントを生成しないか、部分的に生成するかを選択できます。後でOpenAPIインターフェースの数を徐々に増やすことができ、必要な作業は#[handler]#[endpoint]に変更し、関数シグネチャを修正するだけです。

データ抽出器

use salvo::oapi::extract::*;を使用して、事前定義された一般的なデータ抽出器をインポートできます。抽出器はSalvoに必要な情報を提供し、SalvoがOpenAPIドキュメントを生成できるようにします。

  • QueryParam<T, const REQUIRED: bool>: クエリ文字列からデータを抽出する抽出器。QueryParam<T, false>はこのパラメータが必須でないことを意味します。QueryParam<T, true>はこのパラメータが必須であることを意味し、提供されない場合はエラーが返されます。

  • HeaderParam<T, const REQUIRED: bool>: リクエストヘッダーからデータを抽出する抽出器。HeaderParam<T, false>はこのパラメータが必須でないことを意味します。HeaderParam<T, true>はこのパラメータが必須であることを意味し、提供されない場合はエラーが返されます。

  • CookieParam<T, const REQUIRED: bool>: リクエストのクッキーからデータを抽出する抽出器。CookieParam<T, false>はこのパラメータが必須でないことを意味します。CookieParam<T, true>はこのパラメータが必須であることを意味し、提供されない場合はエラーが返されます。

  • PathParam<T>: リクエストURLからパスパラメータを抽出する抽出器。このパラメータが存在しない場合、ルートマッチングは成功しないため、省略可能な状況はありません。

  • FormBody<T>: リクエストで送信されたフォームから情報を抽出します。

  • JsonBody<T>: リクエストで送信されたJSON形式のペイロードから情報を抽出します。

#[endpoint]

OpenAPIドキュメントを生成する場合、通常の#[handler]マクロの代わりに#[endpoint]マクロを使用する必要があります。これは実際には強化版の#[handler]マクロです。

  • 関数シグネチャからOpenAPI生成に必要な情報を取得できます。

  • シグネチャを通じて提供が不便な情報については、直接#[endpoint]マクロに属性を追加することで提供できます。この方法で提供された情報は、関数シグネチャから取得した情報とマージされ、競合がある場合は関数シグネチャの情報が上書きされます。

Rustの組み込み#[deprecated]属性を使用して、特定のHandlerが廃止されたことをマークできます。#[deprecated]属性は廃止理由やバージョンなどの情報をサポートしていますが、OpenAPIはこれらの情報をサポートしていないため、OpenAPI生成時には無視されます。

コード内のドキュメントコメントは自動的にOpenAPI生成に使用され、最初の行は_summary_の生成に、コメント全体は_description_の生成に使用されます。

/// これは操作の要約です
///
/// ドキュメントコメントのすべての行が操作説明に含まれます。
#[endpoint]
fn endpoint() {}

ToSchema

#[derive(ToSchema)]を使用してデータ構造を定義できます:

#[derive(ToSchema)]
struct Pet {
    id: u64,
    name: String,
}

#[salvo(schema(...))]を使用してオプション設定を定義できます:

  • example = ...json!(...)にすることができます。json!(...)serde_json::json!によってserde_json::Valueとして解析されます。

    #[derive(ToSchema)]
    #[salvo(schema(example = json!({"name": "bob the cat", "id": 0})))]
    struct Pet {
        id: u64,
        name: String,
    }
  • xml(...)はXmlオブジェクト属性を定義するために使用できます:

    #[derive(ToSchema)]
    struct Pet {
        id: u64,
        #[salvo(schema(xml(name = "pet_name", prefix = "u")))]
        name: String,
    }

ToParameters

構造体のフィールドから[パスパラメータ][path_parameters]を生成します。

これは[ToParameters][to_parameters]トレイトの#[derive]実装です。

通常、パスパラメータはendpointの[#[salvo_oapi::endpoint(...parameters(...))]][path_parameters]で定義する必要があります。ただし、[struct][struct]を使用してパラメータを定義する場合は、この手順を省略できます。それでも、説明を提供したりデフォルト設定を変更する必要がある場合、[primitive types][primitive]や[String][std_string]のパスパラメータ、または[tuple]スタイルのパスパラメータはparameters(...)で定義する必要があります。

Rustの組み込み#[deprecated]属性を使用してフィールドを非推奨としてマークでき、生成されるOpenAPI仕様に反映されます。

#[deprecated]属性は非推奨理由やバージョンなどの追加情報をサポートしていますが、OpenAPIはサポートしていません。OpenAPIは非推奨かどうかを判断するためのブール値のみをサポートしています。#[deprecated = "There is better way to do this"]のように理由付きで非推奨を宣言することは完全に可能ですが、この理由はOpenAPI仕様には表示されません。

構造体フィールドのドキュメントコメントは、生成されるOpenAPI仕様のパラメータ説明として使用されます。

#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
    /// 名前でtodoアイテムをクエリします。
    name: String
}

#[salvo(parameters(...))]のToParametersコンテナ属性

派生ToParameters構造体のコンテナ属性#[salvo(parameters(...))]に以下の属性を使用できます:

  • names(...) 無名構造体の未命名フィールドに対して、カンマ区切りの名前リストを定義します。無名構造体でのみサポートされます。
  • style = ... すべてのパラメータのシリアライズ方法を[ParameterStyle][style]で定義します。デフォルト値は_parameter_in_属性に基づきます。
  • default_parameter_in = ... このフィールドのパラメータが使用するデフォルトの位置を[parameter::ParameterIn][in_enum]から定義します。この属性が提供されていない場合、デフォルトはqueryになります。
  • rename_all = ... serderename_allの代替として使用できます。実際には同じ機能を提供します。

単一の無名パラメータに名前を定義するnamesの使用例:

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id")))]
struct Id(u64);

複数の無名パラメータに名前を定義するnamesの使用例:

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id", "name")))]
struct IdAndName(u64, String);

#[salvo(parameter(...))]のToParametersフィールド属性

構造体フィールドに#[salvo(parameter(...))]を使用して以下の属性を設定できます:

  • style = ... パラメータが[ParameterStyle][style]によってどのようにシリアライズされるかを定義します。デフォルト値は_parameter_in_属性に基づきます。

  • parameter_in = ... このフィールドパラメータの位置を[parameter::ParameterIn][in_enum]から定義します。この値が提供されていない場合、デフォルトはqueryになります。

  • explode _objectまたはarray内の各パラメータに対して新しいparameter=value_ペアを作成するかどうかを定義します。

  • allow_reserved パラメータ値に予約文字_:/?#[]@!$&'()*+,;=_を含めることを許可するかどうかを定義します。

  • example = ... メソッド参照または_json!(...)_を指定できます。指定された例は基本パラメータタイプの例を上書きします。

  • value_type = ... OpenAPI仕様でフィールドに使用されるデフォルトタイプを上書きするために使用できます。デフォルトタイプが実際のタイプと一致しない場合、例えば[ToSchema][to_schema]または[primitive types][primitive]で定義されていないサードパーティタイプを使用する場合に便利です。値は通常JSONにシリアライズ可能な任意のRustタイプ、または_Object_のようなカスタムタイプ(汎用OpenAPIオブジェクトとしてレンダリングされます)にすることができます。

  • inline 有効にすると、このフィールドタイプの定義は[ToSchema][to_schema]から取得する必要があり、この定義はインライン化されます。

  • default = ... メソッド参照または_json!(...)_を指定できます。

  • format = ... [KnownFormat][known_format]列挙型のバリアント、またはオープン値の文字列形式を指定できます。デフォルトでは、フォーマットはOpenApi仕様に従ってプロパティのタイプから推測されます。

  • write_only プロパティが書き込み操作_POST,PUT,PATCH_でのみ使用され、_GET_では使用されないことを定義します。

  • read_only プロパティが読み取り操作_GET_でのみ使用され、_POST,PUT,PATCH_では使用されないことを定義します。

  • nullable プロパティがnull可能かどうかを定義します(必須でないこととは異なります)。

  • required = ... パラメータが必須であることを強制するために使用します。ルール参照

  • rename = ... serderenameの代替として使用できます。実際には同じ機能を提供します。

  • multiple_of = ... 値の倍数を定義するために使用します。このキーワードの値でパラメータ値を除算した結果が整数の場合にのみ、パラメータ値は有効と見なされます。倍数値は_0_より厳密に大きくなければなりません。

  • maximum = ... 値を含む上限値を定義するために使用します。

  • minimum = ... 値を含む下限値を定義するために使用します。

  • exclusive_maximum = ... 値を含まない上限値を定義するために使用します。

  • exclusive_minimum = ... 値を含まない下限値を定義するために使用します。

  • max_length = ... stringタイプの値の最大長を定義するために使用します。

  • min_length = ... stringタイプの値の最小長を定義するために使用します。

  • pattern = ... フィールド値が一致する必要がある有効な正規表現を定義するために使用します。正規表現は_ECMA-262_バージョンを使用します。

  • max_items = ... arrayタイプのフィールドに許可される最大項目数を定義するために使用できます。値は非負の整数でなければなりません。

  • min_items = ... arrayタイプのフィールドに許可される最小項目数を定義するために使用できます。値は非負の整数でなければなりません。

  • with_schema = ... デフォルトの_schemaの代わりに関数参照によって作成されたschema_を使用します。関数はfn() -> Into<RefOr<Schema>>を満たす必要があります。引数を受け取らず、RefOr<Schema>に変換可能な値を返す必要があります。

  • additional_properties = ... HashMapBTreeMapなどのマップに対して自由形式のタイプを定義するために使用します。自由形式のタイプでは、マップ値で任意のタイプを使用できます。サポートされる形式は_additional_propertiesadditional_properties = true_です。

フィールドのnull許容性と必須ルール

_ToParametersフィールド属性に適用されるnull許容性と必須に関するいくつかのルールは、ToSchema_フィールド属性にも適用されます。ルール参照

部分的な#[serde(...)]属性のサポート

ToParametersの派生は現在、一部の[serde属性][serde attributes]をサポートしています。これらのサポートされる属性は、生成されるOpenAPIドキュメントに反映されます。現在、以下の属性がサポートされています:

  • rename_all = "..." コンテナレベルでサポート。
  • rename = "..." のみフィールドレベルでサポート。
  • default [