OpenAPI 文件生成

OpenAPI 是一套開源的規範,用於描述 RESTful APIs 的介面設計。它以 JSON 或 YAML 格式定義了 API 的請求與回應結構、參數、返回型別、錯誤碼等細節,使客戶端與伺服器端的通訊更加明確且規範化。

OpenAPI 最初是 Swagger 規範的開源版本,現已成為獨立專案,並獲得許多大型企業與開發者的支持。使用 OpenAPI 規範可幫助開發團隊更有效率地協作,降低溝通成本,提升開發效率。同時,OpenAPI 還提供自動生成 API 文件、Mock 資料與測試案例等工具,方便開發與測試工作。

Salvo 提供了 OpenAPI 的整合(修改自 utoipa)。salvo 根據自身特性,能優雅地從 Handler 自動獲取相關的 OpenAPI 資料型別資訊。salvo 還整合了 SwaggerUI、scalar、rapidoc、redoc 等幾個開源流行的 OpenAPI 介面。

針對 Rust 型別名稱較長,可能不適合直接用於 OpenAPI 的情況,salvo-oapi 提供了 Namer 型別,可根據需求自訂規則,改變 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.toml 中啟用 oapi 功能: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 並建立對應的 RouterOpenApi::new("test api", "0.0.1").merge_router(&router) 中的 merge_router 表示這個 OpenAPI 通過解析某個路由及其子路由來獲取必要的文件資訊。某些路由的 Handler 若未提供生成文件的資訊,這些路由將被忽略,例如使用 #[handler] 宏而非 #[endpoint] 宏定義的 Handler。也就是說,在實際專案中,可根據開發進度選擇不生成或部分生成 OpenAPI 文件,後續只需將 #[handler] 改為 #[endpoint] 並修改函數簽名即可逐步增加 OpenAPI 介面數量。

資料提取器

通過 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>:從請求的 Cookie 中提取資料的提取器。CookieParam<T, false> 表示此參數非必填,可省略;CookieParam<T, true> 表示此參數必填,不可省略,否則返回錯誤;

  • PathParam<T>:從請求 URL 中提取路徑參數的提取器。此參數若不存在,路由匹配即不成功,因此不存在可省略的情況;

  • FormBody<T>:從請求提交的表單中提取資訊;

  • JsonBody<T>:從請求提交的 JSON 格式負載中提取資訊;

#[endpoint]

在生成 OpenAPI 文件時,需使用 #[endpoint] 宏代替常規的 #[handler] 宏,它實際上是增強版的 #[handler] 宏。

  • 可通過函數簽名獲取生成 OpenAPI 所需的資訊;

  • 對於不便通過簽名提供的資訊,可直接在 #[endpoint] 宏中添加屬性提供,此方式提供的資訊會與函數簽名獲取的資訊合併,若存在衝突則會覆蓋函數簽名提供的資訊。

可使用 Rust 自帶的 #[deprecated] 屬性標記某個 Handler 已過時廢棄。雖然 #[deprecated] 屬性支持添加廢棄原因、版本等資訊,但 OpenAPI 並不支持,這些資訊在生成 OpenAPI 時將被忽略。

程式碼中的文件註釋部分會自動被提取用於生成 OpenAPI,第一行用於生成 summary,整個註釋部分用於生成 description

/// This is a summary of the operation
///
/// All lines of the doc comment will be included to operation 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] trait 的 #[derive] 實現。

通常情況下,路徑參數需要在 endpoint 的 [#[salvo_oapi::endpoint(...parameters(...))]][path_parameters] 中定義。但當使用 [struct][struct] 定義參數時,可省略此步驟。不過,若要提供描述或更改預設配置,[primitive types][primitive] 和 [String][std_string] 路徑參數或 [tuple] 風格的路徑參數仍需在 parameters(...) 中定義。

可使用 Rust 內建的 #[deprecated] 屬性標記欄位為已棄用,這會反映到生成的 OpenAPI 規範中。

#[deprecated] 屬性支持添加棄用原因或版本等額外資訊,但 OpenAPI 僅支持布林值來確定是否棄用。雖然可聲明帶原因的棄用,如 #[deprecated = "There is better way to do this"],但原因不會呈現在 OpenAPI 規範中。

結構體欄位上的註釋文件會用作生成 OpenAPI 規範中的參數描述。

#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
    /// Query todo items by name.
    name: String
}

ToParameters Container Attributes for#[salvo(parameters(...))]

以下屬性可用於派生自 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);

ToParameters Field Attributes for#[salvo(parameter(...))]

以下屬性可用於結構體欄位的 #[salvo(parameter(...))]

  • style = ... 定義參數如何被 [ParameterStyle][style] 序列化。預設值基於 parameter_in 屬性。
  • parameter_in = ... 使用來自 [parameter::ParameterIn][in_enum] 的值定義此欄位參數的位置。若未提供此值,則預設來自 query
  • explode 定義是否為 objectarray 中的每個參數建立新的 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 = ... 用於為 map 定義自由形式型別,如 HashMapBTreeMap。自由形式型別允許在映射值中使用任意型別。支持的格式有 additional_propertiesadditional_properties = true

Field nullability and required rules

一些應用於 ToParameters 欄位屬性的是否可為空和是否必需的規則同樣可用於 ToSchema 欄位屬性。參見規則

Partial#[serde(...)] attributes support

ToParameters 派生目前支持部分 [serde 屬性][serde attributes]。這些支持的屬性將反映到生成的 OpenAPI 文件中。目前支持以下屬性:

  • rename_all = "..." 在容器級支持。
  • rename = "..." 在欄位級支持。
  • default 根據 [serde 屬性][serde attributes] 在容器級和欄位級支持。
  • skip_serializing_if = "..." 在欄位級支持。
  • with = ... 在欄位級支持。
  • skip_serializing = "..." 在欄位級或變體級支持。
  • skip_deserializing = "..." 在欄位級或變體級支持。
  • skip = "..." 在欄位級支持。

其他 serde 屬性會影響序列化,但不會反映在生成的 OpenAPI 文件上。

範例

演示使用 #[salvo(parameters(...))] 容器屬性結合 [ToParameters][to_parameters] 的用法,用在路徑參數上,並內聯一個查詢欄位:

use serde::Deserialize;
use salvo_core::prelude::*;
use salvo_oapi::{ToParameters, ToSchema};

#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
enum PetKind {
    Dog,
    Cat,
}

#[derive(Deserialize, ToParameters)]
struct PetQuery {
    /// Name of pet
    name: Option<String>,
    /// Age of pet
    age: Option<i32>,
    /// Kind of pet
    #[salvo(parameter(inline))]
    kind: PetKind
}

#[salvo_oapi::endpoint(
    parameters(PetQuery),
    responses(
        (status_code = 200, description = "success response")
    )
)]
async fn get_pet(query: PetQuery) {
    // ...
}

使用 value_typeString 型別覆蓋為 i64 型別。

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
    #[salvo(parameter(value_type = i64))]
    id: String,
}

使用 value_typeString 型別覆蓋為 Object 型別。在 OpenAPI 規範中,Object 型別會顯示為 type:object

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
    #[salvo(parameter(value_type = Object))]
    id: String,
}

你也可以用一個泛型來覆蓋欄位的預設型別。

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
    #[salvo(parameter
{/* 本行由工具自动生成,原文哈希值:66e3964ffc9d02e6953bdf48a6d2ed87 */}