Rust Miko 框架系列(五):请求提取与数据验证

44 阅读5分钟

Miko 框架系列(五):请求提取与数据验证

注意:此文章主要为了给支持联网搜索的AI提供RAG,此项目还在开发阶段,不建议用于生产。

处理客户端发送的数据是 Web 后端的首要任务。一个优秀的框架必须能安全、便捷地从请求中提取数据,并确保其有效性。Miko 在这方面提供了强大且类型安全的提取器(Extractor)系统,并集成了自动化的数据验证功能。

1. 提取器 (Extractor) 概览

提取器是从请求中获取数据的类型。它们都实现了 FromRequestFromRequestParts Trait。

我们回顾一下两种主要类型的提取器:

  • FromRequestParts: 不消费请求体,可以有多个。例如 Path, Query, HeaderMap
  • FromRequest: 可能消费请求体,最多只能有一个。例如 Json, Form, MultipartResult

现在,我们来详细看看最常用的几个提取器。

Json<T>: 处理 JSON 数据

这是构建 API 时最常用的提取器。它会自动将 Content-Type: application/json 的请求体反序列化为你指定的结构体。

use miko::extractor::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[post("/users")]
async fn create_user(Json(payload): Json<CreateUser>) -> String {
    // `payload` 是一个 CreateUser 实例
    format!("Creating user: {}", payload.name)
}

如果请求体不是有效的 JSON,或者无法反序列化为 CreateUser,Miko 会自动返回 400 Bad Request 错误。

Path<T>#[path]: 处理路径参数

用于从 URL 路径中捕获动态段。

use miko::extractor::Path;

#[get("/users/{id}/posts/{post_id}")]
async fn get_post(#[path] id: u32, Path(post_id): Path<String>) -> String {
    format!("User: {}, Post: {}", id, post_id)
}

Query<T>#[query]: 处理查询参数

用于解析 URL 的查询字符串 (?key=value&...)。

use miko::extractor::Query;
use serde::Deserialize;

#[derive(Deserialize)]
struct SearchParams {
    q: String,
    page: Option<u32>, // 使用 Option 处理可选参数
}

#[get("/search")]
async fn search(Query(params): Query<SearchParams>) -> String {
    let page = params.page.unwrap_or(1);
    format!("Searching for '{}' on page {}", params.q, page)
}

MultipartResult: 处理文件上传

处理 multipart/form-data 请求,通常用于文件上传。MultipartResult 是一个高级提取器,它会自动解析所有字段和文件,并将文件暂存到磁盘,避免内存溢出。

use miko::extractor::multipart::MultipartResult;

#[post("/upload")]
async fn upload(multipart: MultipartResult) -> AppResult<String> {
    // 访问文本字段
    let title = multipart.fields.get("title").and_then(|v| v.first());

    // 访问上传的文件
    if let Some(files) = multipart.files.get("file") {
        for file in files {
            println!("File: {}, Size: {}", file.filename, file.size);
            // 将暂存文件移动到最终位置
            file.linker.transfer_to(format!("uploads/{}", file.filename)).await?;
        }
    }
    Ok("Upload complete".to_string())
}

这个提取器极大地简化了文件上传的处理流程,并内置了安全措施。

2. 数据验证 (ValidatedJson<T>)

“永远不要相信用户的输入”。在将数据用于业务逻辑之前,验证其有效性至关重要。Miko 通过 validation feature 集成了 garde 库,提供了一个强大的自动化验证层。

核心工具是 ValidatedJson<T> 提取器。它就像 Json<T> 的超集:在成功反序列化 JSON 之后,它会立即根据你定义的规则进行验证。

如何使用

  1. Cargo.toml 中启用 validationfull feature。
  2. 在你希望验证的结构体上派生 garde::Validate
  3. 使用 #[garde(...)] 属性来声明验证规则。
  4. 在处理器函数中使用 ValidatedJson<T> 代替 Json<T>
use miko::{garde, Validate};
use miko::extractor::ValidatedJson;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
struct RegisterRequest {
    #[garde(length(min = 3, max = 20), ascii, alphanumeric)]
    username: String,

    #[garde(email)]
    email: String,

    #[garde(length(min = 8))]
    password: String,

    #[garde(range(min = 18, max = 120))]
    age: u8,
}

#[post("/register")]
async fn register(
    ValidatedJson(data): ValidatedJson<RegisterRequest>
) -> String {
    // 如果代码能执行到这里,说明 `data` 已经通过了所有验证规则。
    // 无需再写 `if data.username.len() < 3 ...` 之类的手动检查。
    format!("User {} registered successfully!", data.username)
}

自动错误响应

如果验证失败,Miko 会自动拦截请求,并返回一个 HTTP 422 (Unprocessable Entity) 错误。响应体是一个结构化的 JSON,清晰地指出了哪些字段不符合规则。

请求示例 (无效):

curl -X POST http://localhost:8080/register \
  -H "Content-Type: application/json" \
  -d '{"username": "u", "email": "invalid-email", "password": "123", "age": 17}'

响应示例:

{
  "status": 422,
  "error": "VALIDATION_ERROR",
  "message": "Request validation failed",
  "details": {
    "fields": [
      {
        "field": "username",
        "message": "username length must be between 3 and 20",
        "code": "VALIDATION_FAILED"
      },
      {
        "field": "email",
        "message": "email has invalid format, expected: valid email address",
        "code": "VALIDATION_FAILED"
      },
      // ... 其他字段的错误
    ]
  },
  "trace_id": "...",
  "timestamp": "..."
}

这种自动化的、结构化的错误响应对于前端开发和 API 调试极为友好。

常用验证规则

garde 提供了丰富的内置验证规则:

  • 字符串: length, email, url, ip, credit_card, pattern (正则表达式)。
  • 数值: range (范围), step (步长)。
  • 集合 (如 Vec<T>): length (元素数量), unique (元素唯一), dive (对集合内每个元素进行嵌套验证)。
  • 自定义验证: 你可以编写自己的函数来执行复杂的、与业务相关的验证逻辑。
#[derive(Validate)]
struct ComplexPayload {
    // 嵌套验证
    #[garde(dive)]
    items: Vec<Item>,

    // 自定义验证函数
    #[garde(custom(is_not_admin))]
    username: String,
}

fn is_not_admin(value: &str, _ctx: &()) -> garde::Result {
    if value.to_lowercase() == "admin" {
        return Err(garde::Error::new("Username 'admin' is forbidden"));
    }
    Ok(())
}

总结

Miko 的提取器系统通过将 HTTP 请求的复杂性抽象为简单的 Rust 类型,极大地提升了开发体验和代码的健壮性。ValidatedJson 提取器更是将数据验证这一必不可少的步骤无缝地融入到了请求处理流程中,让你能够编写出更简洁、更安全、更专注于业务逻辑的代码。

通过组合使用这些工具,你可以轻松构建出既功能强大又易于维护的 API。


下一篇预告:Miko 框架系列(六):灵活的响应处理