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 中的型別名稱。
範例程式碼
在瀏覽器中輸入 http://localhost:8698/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並建立對應的Router。OpenApi::new("test api", "0.0.1").merge_router(&router)中的merge_router表示此OpenAPI透過解析某個路由及其子路由來取得必要的文件資訊。某些路由的Handler可能未提供生成文件所需的資訊,這些路由將被忽略,例如使用#[handler]巨集而非#[endpoint]巨集定義的Handler。也就是說,在實際專案中,基於開發進度等原因,你可以選擇不生成 OpenAPI 文件,或僅部分生成 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 文件時,需使用 #[endpoint] 巨集代替常規的 #[handler] 巨集,它實際上是增強版的 #[handler] 巨集。
- 可透過函式簽章取得生成 OpenAPI 所需的資訊;
- 對於不便透過簽章提供的資訊,可直接在
#[endpoint]巨集中以添加屬性的方式提供,透過此方式提供的資訊會與透過函式簽章取得的資訊合併,若存在衝突,則會覆蓋函式簽章提供的資訊。
你可以使用 Rust 內建的 #[deprecated] 屬性標註某個 Handler 已過時並廢棄。雖然 #[deprecated] 屬性支援添加如廢棄原因、版本等資訊,但 OpenAPI 並不支援,因此這些資訊在生成 OpenAPI 時將被忽略。
程式碼中的文件註解部分會自動被提取用於生成 OpenAPI,第一行用於生成 summary,整個註解部分用於生成 description。
ToSchema
可使用 #[derive(ToSchema)] 定義資料結構:
可使用 #[salvo(schema(...))] 定義可選設定:
-
example = ...可以是json!(...)。json!(...)會被serde_json::json!解析為serde_json::Value。 -
xml(...)可用於定義 Xml 物件屬性:
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 並不支援。OpenAPI 僅支援一個布林值來確定是否棄用。雖然完全可以宣告一個帶原因的棄用,如 #[deprecated = "There is better way to do this"],但此原因不會在 OpenAPI 規範中呈現。
結構體欄位上的註解文件會用作生成 OpenAPI 規範中的參數描述。
ToParameters 容器屬性 #[salvo(parameters(...))]
以下屬性可用於那些派生自 ToParameters 的結構體的容器屬性 #[salvo(parameters(…))]:
names(...)為作為路徑參數使用的結構體的未命名字段定義逗號分隔的名稱清單。僅支援在未命名結構體上使用。style = ...可定義所有參數的序列化方式,由 [ParameterStyle][style] 指定。預設值基於parameter_in屬性。default_parameter_in = ...定義此欄位的參數使用的預設位置,該位置的值來自於 [parameter::ParameterIn][in_enum]。若未提供此屬性,則預設來自query。rename_all = ...可作為serde的rename_all的替代方案。實際上提供了相同的功能。
使用 names 為單個未命名的參數定義名稱。
使用 names 為多個未命名的參數定義名稱。
ToParameters 欄位屬性 #[salvo(parameter(...))]
以下屬性可在結構體欄位上使用 #[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] 或 [primitivetypes][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 = ...可作為serde的rename的替代方案。實際上提供了相同的功能。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定義自由形式型別,例如HashMap和BTreeMap。自由形式型別允許在對映值中使用任意型別。支援的格式有additional_properties和additional_properties = true。
欄位可空性與必需規則
一些應用於 ToParameters 欄位屬性的可為空和是否必需的規則同樣可用於 ToSchema 欄位屬性。參見規則。
部分 #[serde(...)] 屬性支援
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] 的用法,用於路徑參數,並內嵌一個查詢欄位:
使用 value_type 將 String 型別覆蓋為 i64 型別。
使用 value_type 將 String 型別覆蓋為 Object 型別。在 OpenAPI 規範中,Object 型別會顯示為 type:object。