OpenAPI

OpenAPI 是一個開源的規範,用於描述 RESTful APIs 的接口設計.它以 JSON 或 YAML 格式定義了 API 的請求和響應的結構、參數、返回類型、錯誤碼等細節,使得客戶端和服務端之間的通信更加明確和規範化.

OpenAPI 最初是 Swagger 規範的開源版本,現在已經成爲了一個獨立的項目,並得到了許多大型企業和開發者的支持.使用 OpenAPI 規範可以幫助開發團隊更好地協作,減少溝通成本,提高開發效率.同時,OpenAPI 還爲開發者提供了自動生成 API 文檔、Mock 數據和測試用例等工具,方便開發和測試工作.

Salvo 提供了 OpenAPI 的集成 (修改自 utoipa在新窗口打開).

示例代碼

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
        .push(doc.into_router("/api-doc/openapi.json"))
        .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));

    let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}
[package]
name = "example-oapi-hello"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
salvo = { workspace = true, features = ["oapi"] }
tokio = { workspace = true, features = ["macros"] }
tracing.workspace = true
tracing-subscriber.workspace = true

在瀏覽器裏面輸入 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 並且創建對應的 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.

/// 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,
    }
    
  • rename_all = ...: 支持於 serde 類似的語法定義重命名字段的規則. 如果同時定義了 #[serde(rename_all = "...")]#[salvo(schema(rename_all = "..."))], 則優先使用 #[serde(rename_all = "...")].

  • symbol = ...: 一個字符串字面量, 用於定義結構在 OpenAPI 中線上的名字路徑. 比如 #[salvo(schema(symbol = "path.to.Pet"))].

  • default: Can be used to populate default values on all fields using the struct’s Default implementation.