API-Documentation-with-Utoipa[20261004051048]

0 阅读1分钟

使用 utoipa 生成 API 文档

项目代码:github.com/hyperlane-d…

API 文档是任何 Web 服务的关键组件。文档完善的 API 更易于使用、更易于维护,也更可能被开发者采用。在 Hyperlane 生态系统中,utoipa 是推荐的 OpenAPI 合规 API 文档生成工具,可直接从 Rust 代码生成文档。

utoipa 是什么?

Utoipa 是一个 Rust crate,使用 derive 宏从 Rust 代码生成 OpenAPI 3.0 规范。它支持请求和响应类型的自动模式生成、路径参数提取和元数据注释。生成的规范可以作为 JSON 提供,并与 Swagger UI 配合使用以生成交互式文档。

使用 utoipa 与 Hyperlane 的关键优势是文档与代码保持同步。由于 OpenAPI 规范是从实际路由处理程序和数据类型生成的,因此不存在文档与代码脱节的风险。

设置 utoipa

要开始使用 utoipa,需要将其与 Web 框架适配器一起添加到项目中。对于 Hyperlane,你将使用 utoipa 的 derive 宏来注释路由处理程序和数据类型:

use hyperlane::*;
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
    paths(
        get_user,
        create_user,
        list_users,
    ),
    components(
        schemas(User, CreateUserRequest, UserListResponse),
    ),
    info(
        title = "User Management API",
        version = "1.0.0",
        description = "API for managing users in the system"
    )
)]
struct ApiDoc;

#[openapi] 属性宏告诉 utoipa 哪些路由要包含在规范中,哪些类型要记录为模式,以及 API 信息部分要包含哪些元数据。

记录路由处理程序

Hyperlane 应用中的每个路由处理程序都可以使用 utoipa 的 #[utoipa::path] 宏进行注释,以记录其参数、请求体和响应:

use hyperlane::*;
use utoipa::path;

#[utoipa::path(
    get,
    path = "/api/users/{id}",
    params(
        ("id" = String, Path, description = "User ID")
    ),
    responses(
        (status = 200, description = "User found", body = User),
        (status = 404, description = "User not found"),
        (status = 500, description = "Internal server error"),
    ),
    tag = "users"
)]
#[route("/api/users/{id}")]
async fn get_user(ctx: &mut Context) -> Result<(), RequestError> {
    let user_id = ctx.get_route_param("id");
    let user = fetch_user(user_id).await?;

    ctx.get_mut_response()
        .set_status_code(200)
        .add_header("Content-Type", "application/json")
        .set_body(serde_json::to_string(&user).unwrap());

    Ok(())
}

params 部分记录路径参数、查询参数和头部参数。responses 部分记录每个可能的 HTTP 状态码及其关联的响应体类型。

记录请求和响应类型

Utoipa 可以使用 #[derive(ToSchema)] 宏为你的 Rust 类型自动生成 JSON Schema 定义:

use utoipa::ToSchema;

#[derive(ToSchema, serde::Serialize, serde::Deserialize)]
struct User {
    id: String,
    username: String,
    email: String,
    created_at: String,
}

#[derive(ToSchema, serde::Serialize, serde::Deserialize)]
struct CreateUserRequest {
    username: String,
    email: String,
    password: String,
}

#[derive(ToSchema, serde::Serialize, serde::Deserialize)]
struct UserListResponse {
    users: Vec<User>,
    total: usize,
    page: usize,
    per_page: usize,
}

这些模式定义会自动包含在 OpenAPI 规范的 components/schemas 部分中,使它们可以在多个端点之间复用。

记录 POST 和 PUT 处理程序

对于接受请求体的端点,utoipa 的 #[utoipa::path] 宏支持 request_body 属性:

use hyperlane::*;
use utoipa::path;

#[utoipa::path(
    post,
    path = "/api/users",
    request_body = CreateUserRequest,
    responses(
        (status = 201, description = "User created successfully", body = User),
        (status = 400, description = "Invalid request body"),
        (status = 409, description = "Username already exists"),
    ),
    tag = "users"
)]
#[route("/api/users")]
async fn create_user(ctx: &mut Context) -> Result<(), RequestError> {
    let body = ctx.get_request().get_body_json::<CreateUserRequest>()?;
    let user = create_user_in_db(body).await?;

    ctx.get_mut_response()
        .set_status_code(201)
        .add_header("Content-Type", "application/json")
        .set_body(serde_json::to_string(&user).unwrap());

    Ok(())
}

request_body = CreateUserRequest 属性告诉 utoipa 该端点接受与 CreateUserRequest 模式匹配的 JSON 请求体。

记录带分页的列表端点

对于返回分页列表的端点,记录查询参数:

use hyperlane::*;
use utoipa::path;

#[utoipa::path(
    get,
    path = "/api/users",
    params(
        ("page" = Option<usize>, Query, description = "Page number (starting from 1)"),
        ("per_page" = Option<usize>, Query, description = "Items per page (max 100)"),
        ("sort" = Option<String>, Query, description = "Sort field"),
    ),
    responses(
        (status = 200, description = "List of users", body = UserListResponse),
    ),
    tag = "users"
)]
#[route("/api/users")]
async fn list_users(ctx: &mut Context) -> Result<(), RequestError> {
    let page = ctx.try_get_route_param("page").unwrap_or(1);
    let per_page = ctx.try_get_route_param("per_page").unwrap_or(20);

    let result = fetch_users(page, per_page).await?;

    ctx.get_mut_response()
        .set_status_code(200)
        .add_header("Content-Type", "application/json")
        .set_body(serde_json::to_string(&result).unwrap());

    Ok(())
}

提供 OpenAPI 规范

记录完 API 后,需要提供生成的 OpenAPI 规范。创建一个以 JSON 格式返回规范的路由:

use hyperlane::*;
use utoipa::OpenApi;

#[route("/api-docs/openapi.json")]
async fn serve_openapi_spec(ctx: &mut Context) -> Result<(), RequestError> {
    let doc = ApiDoc::openapi();
    let json = serde_json::to_string_pretty(&doc).unwrap();

    ctx.get_mut_response()
        .set_status_code(200)
        .add_header("Content-Type", "application/json")
        .set_body(json);

    Ok(())
}

你还可以提供 Swagger UI 用于交互式文档:

#[route("/api-docs")]
async fn serve_swagger_ui(ctx: &mut Context) -> Result<(), RequestError> {
    let html = r#"
    <!DOCTYPE html>
    <html>
    <head>
        <title>API Documentation</title>
        <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
    </head>
    <body>
        <div id="swagger-ui"></div>
        <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
        <script>
            SwaggerUIBundle({
                url: '/api-docs/openapi.json',
                dom_id: '#swagger-ui',
            });
        </script>
    </body>
    </html>
    "#;

    ctx.get_mut_response()
        .set_status_code(200)
        .add_header("Content-Type", "text/html")
        .set_body(html);

    Ok(())
}

使用标签组织端点

标签帮助将 API 文档组织到逻辑组中。你可以为每个端点分配标签并添加描述:

use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
    paths(
        get_user,
        create_user,
        list_users,
    ),
    components(
        schemas(User, CreateUserRequest, UserListResponse),
    ),
    tags(
        (name = "users", description = "User management operations"),
        (name = "auth", description = "Authentication and authorization"),
        (name = "health", description = "Health check and monitoring"),
    ),
    info(
        title = "My API",
        version = "1.0.0",
        description = "A comprehensive API built with Hyperlane and utoipa"
    )
)]
struct ApiDoc;

添加认证文档

如果你的 API 使用认证,请在 OpenAPI 规范中记录安全需求:

use utoipa::security::{ApiKey, ApiKeyValue};

#[derive(OpenApi)]
#[openapi(
    paths(
        get_user,
        create_user,
    ),
    components(
        schemas(User),
        securitySchemes(
            ("api_key" = ApiKey(ApiKeyValue::Header("X-API-Key"))),
            ("bearer_auth" = ApiKey(ApiKeyValue::Header("Authorization"))),
        ),
    ),
    info(
        title = "My API",
        version = "1.0.0",
    )
)]
struct ApiDoc;

将 Utoipa 与 Hyperlane 中间件结合

Utoipa 文档与 Hyperlane 的中间件系统无缝配合。你可以记录中间件行为并将其应用到路由:

use hyperlane::*;

#[request_middleware(1)]
async fn api_key_auth(ctx: &mut Context) -> MiddlewareResult {
    let request = ctx.get_request();
    let path = request.get_path();

    // 跳过公开端点的认证
    if path == "/api-docs/openapi.json" || path == "/health" {
        return ctx.next().await;
    }

    // 验证受保护端点的 API 密钥
    // ... 认证逻辑

    ctx.next().await
}

API 文档最佳实践

  1. 记录每个端点:API 中的每个路由都应该有 utoipa 注释。不完整的文档比没有文档更糟糕。

  2. 使用描述性摘要:每个端点都应该有清晰、简洁的功能摘要。

  3. 记录所有响应码:包括成功响应、客户端错误(4xx)和服务器错误(5xx)。

  4. 添加示例:尽可能在模式定义中提供示例请求和响应体。

  5. 保持描述最新:更改端点时,立即更新其文档。

  6. 使用标签进行组织:使用标签将相关端点分组,使文档更易于导航。

  7. 版本化 API:在 OpenAPI 信息部分包含 API 版本,并考虑基于 URL 的版本控制。

总结

Utoipa 为 Hyperlane 应用提供了一种强大且符合人体工程学的方式来生成 OpenAPI 文档。通过使用 utoipa 的 derive 宏注释路由处理程序和数据类型,你可以生成全面的、始终与代码保持同步的 API 文档。

Hyperlane 的高性能 HTTP 服务器与 utoipa 的文档生成能力的结合,为你提供了构建文档完善的生产级 API 所需的一切。借助交互式 Swagger UI,你的 API 消费者可以直接从浏览器探索和测试端点,减少集成时间并改善开发者体验。

从为你的最重要的端点添加基本 utoipa 注释开始,然后逐步扩展覆盖范围,直到整个 API 都有文档。对文档的投资会以减少的支持负担和更快的集成时间的形式获得回报。


项目代码:github.com/hyperlane-d…