在actix-web中创建一个提取器

1. 核心业务与测试

背景: 技术负责人兼任产品负责人,可直接主导项目决策。订阅功能需改进为收集"可用于邮件问候的标识符"(如昵称),而非强制真实姓名。

数据传递与编码

  • 前端通过 HTML 表单使用 POST 请求提交数据。

  • 编码格式为 application/x-www-form-urlencoded

    • 键值对格式为 key=value,多个键值对以 & 分隔。

    • 特殊字符采用百分号编码(如空格→%20@%40)。

  • 示例:name=le%20guin&email=ursula_le_guin%40gmail.com

后端响应规则

  • nameemail 均有效 → 返回 200 OK

  • 若任一字段缺失 → 返回 400 BAD REQUEST

测试要求 : 在 tests/health_check.rs 中新增集成测试,验证端点行为:

  • 提交有效数据时应得到 200。

  • 提交不完整数据时应得到 400。

接口的核心实现(待修改)

rust 复制代码
async fn subscriptions() -> impl Responder {
    HttpResponse::Ok().finish()
}

服务器中注册路由

rust 复制代码
App::new()
    .route("/subscriptions", web::post().to(subscriptions))

测试文件:tests/health_check.rs

rust 复制代码
// tests/health_check.rs

use std::net::TcpListener;
use actix_web::Responder; // 补充actix_web必要的引用
use tokio; // 补充tokio必要的引用
// 假设 zero2prod::run 函数已定义在 crate 的 lib.rs 或 main.rs 中

// # 表单数据完整时,返回 200
#[tokio::test]
async fn subscribe_returns_a_200_for_valid_form_data() {
    let address = spawn_app();
    let client = reqwest::Client::new();
    let body = "name=cang%20li&email=gl0wniapar%40gmail.com";
    let response = client
        .post(&format!("{}/subscriptions", &address))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .body(body)
        .send()
        .await
        .expect("Failed to execute request.");
    assert_eq!(response.status().as_u16(), 200);
}

// # 数据缺失时,应返回 400
#[tokio::test]
async fn subscribe_returns_a_400_when_data_is_missing() {
    let app_address = spawn_app();
    let client = reqwest::Client::new();
    let test_cases = vec![
        ("name=cangli", "Did not have email"), // 优化错误信息
        ("email=gl0wniapar%40gmail.com", "Did not have name"), // 优化错误信息
        ("", "Did not have both name and email"),
    ];
    for (invalid_body, error_message) in test_cases {
        let response = client
            .post(format!("{}/subscriptions", app_address))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .body(invalid_body)
            .send()
            .await
            .expect("Failed to execute request.");
        assert_eq!(response.status().as_u16(), 400, "The API did not return 400 when {} was expected.", error_message);
    }
}

fn spawn_app() -> String {
    // ... 启动应用实例的逻辑,需要 actix_web::server::HttpServer 和 tokio::spawn
    // 假设 zero2prod::run 是一个返回 actix_web::server::HttpServer 的函数
    let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
    let port = listener.local_addr().expect("Failed to get local address").port();
    let server = zero2prod::run(listener).expect("Failed to run server"); // 假设 zero2prod::run 可用
    tokio::spawn(server);

    format!("http://127.0.0.1:{}", port)
}

2. 提取器 (Extractor) 优化

提取器 提取器用于从传入的请求中智能地提取特定信息,并将其转化为 Rust 类型。 actix-web 提供了多种内置提取器,如:

  • Path: 用于从请求路径中获取动态路径参数。

  • Query: 用于获取 URL 查询参数。

  • Json: 用于解析 application/json 编码的请求体。

  • Form: 用于解析 application/x-www-form-urlencoded 编码的请求体(即 HTML 表单提交数据)。

满足需求的提取器 使用 web::Form<T> 提取器。它会自动处理表单数据的反序列化和验证。

提取器示例

rust 复制代码
// 1. 定义一个结构体来匹配表单数据的字段
#[derive(serde::Deserialize)]
struct FormData {
    // 字段名必须与表单中的 'key' 匹配
    name: String, // 用于邮件问候的标识符(昵称)
    email: String,
}

// 2. 将 Form 提取器作为处理器函数的参数
/// 仅当请求头 Content-Type 为 application/x-www-form-urlencoded 
/// 且请求体能反序列化为 `FormData` 结构体时,才会调用此处理器。
async fn subscriptions(form: web::Form<FormData>) -> HttpResponse {
    // 成功提取数据,返回 200 OK
    // form.0 或 form.into_inner() 可以访问内部的 FormData 实例
    HttpResponse::Ok().finish()
}

为什么测试通过了?

原始的 subscriptions 函数实现是:

rust 复制代码
async fn subscriptions() -> impl Responder {
    HttpResponse::Ok().finish()
}

这个函数不接受任何参数(提取器),因此它不会检查请求体或 Content-Type,直接返回 200 OK

  • 有效数据测试:返回 200,通过。

  • 不完整数据测试 :返回 200,但测试期望 400,因此这个测试实际上会失败!

  • 如果使用新的 web::Form<FormData> 提取器,不完整数据测试才会如预期般成功返回 400

    正确的解释: 当使用 新的、带有 web::Form<FormData> 提取器subscriptions 函数时:

    1. 有效数据Form 提取成功,处理器执行并返回 200 OK

    2. 不完整/无效数据Form 提取失败(serde 无法反序列化),Form 提取器根据 FromRequest 实现的默认行为,返回 400 BAD REQUEST,处理器函数甚至不会被调用。

3. 提取器背后的机制:FormFromRequest (修正和精炼)

Form 结构体

rust 复制代码
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Form<T>(pub T);

Form<T> 只是一个围绕泛型类型 T 的包装器。其核心功能通过实现 FromRequest trait 来实现。

FromRequest Trait 在 actix-web 中,所有作为处理器函数参数的类型都必须实现 FromRequest trait。该 trait 允许 actix-web 在处理传入的 HTTP 请求时,从请求头 (HttpRequest) 和有效载荷 (Payload) 中提取数据。

rust 复制代码
pub trait FromRequest: Sized {
    type Error: Into<actix_web::Error>; // 错误类型必须能转换为 actix_web::Error

    // 核心异步方法,尝试从请求中提取自身
    async fn from_request(
        req: &HttpRequest, 
        payload: &mut Payload
    ) -> Result<Self, Self::Error>;
    
    // [...]
}

工作流程

  1. actix-web 接收请求,并确定要调用的处理器函数。

  2. actix-web 会依次调用处理器函数中每个参数from_request 异步方法。

  3. 成功:所有参数都成功提取,执行处理器函数。

  4. 失败 :任何一个参数提取失败,则将提取器返回的错误 (Self::Error) 转换为 actix_web::Error,再由框架将其转换为相应的 HttpResponse (通常是 400 BAD REQUEST413 PAYLOAD TOO LARGE),并返回给客户端,处理器函数不会被调用。

Form<T>FromRequest 实现 Form<T> 的实现依赖于 T 必须实现 serde::de::DeserializeOwned trait。

rust 复制代码
impl<T> FromRequest for Form<T>
where 
    T: DeserializeOwned + 'static, 
    // ... 忽略其他约束
{
    type Error = actix_web::Error;

    async fn from_request(/* ... */) -> Result<Self, Self::Error> {
        // ... (内部使用 actix_web 提供的逻辑来处理 URL 编码)
        match UrlEncoded::new(req, payload).await {
            Ok(item) => Ok(Form(item)),
            // 默认情况下,解析失败(如数据缺失或格式错误)
            // 会被转换为 actix_web::Error,默认返回 400 BAD REQUEST
            Err(e) => Err(error_handle(e)) 
        }
    }
}

其中的关键步骤是:

  1. 读取整个请求体字节流。

  2. 使用 serde_urlencoded::from_bytes::<T>(&body) 进行反序列化。

    • 成功 :包装成 Form<T> 并返回。

    • 失败 :返回一个 urlencodedError::Parse 或其他错误,该错误最终被转换为 400 BAD REQUEST

4. serde:数据序列化与反序列化的通用框架 (精炼和修正)

为什么需要 serde
serde (Serializer/Deserializer) 是 Rust 生态中高效且通用 的数据结构序列化和反序列化框架。它本身不处理任何特定数据格式(如 JSON、YAML),而是作为数据格式库和 Rust 类型之间的翻译中间层

serde 的核心机制

  1. 数据模型serde 定义了一套通用的数据模型(如布尔、整数、字符串、序列、映射、结构体等),涵盖了 Rust 类型可能的所有结构。

  2. 核心 Trait

    • Serialize:定义了如何将 Rust 类型分解为 Serde 数据模型。

    • Deserialize:定义了如何将 Serde 数据模型构建为 Rust 类型。

    • Serializer/Deserializer:由具体数据格式库(如 serde_jsonserde_urlencoded)实现,用于处理实际的格式编码/解码。

工作流程(序列化为例)

  • Rust 类型 (如 Vec<T>)实现 Serialize trait。

  • serialize 方法中,类型调用 Serializer 提供的接口(如 serialize_seqserialize_element),将自身结构描述给序列化器。

  • 格式库 (如 serde_urlencoded)实现了 Serializer,负责根据接收到的结构描述,生成最终的 URL 编码字符串或字节。

效率与零成本抽象 serde 在编译期利用 Rust 的 单态化(Monomorphization) 特性,为每种具体类型生成独立的函数实现。这消除了运行时的类型检查和反射开销,实现了零成本抽象(Zero-cost Abstraction)。特定格式(反)序列化信息在编译期即可完全确定,无需运行时查找,保证了高性能。

便利性:#[derive(Serialize)]#[derive(Deserialize)] 这两个过程宏是 serde 的核心便捷工具。它们自动解析用户定义的 structenum,并生成对应 SerializeDeserialize trait 的实现代码。这极大地简化了开发工作,避免了手动编写冗长、易错的序列化逻辑。

相关推荐
^_^ 纵歌4 小时前
rust主要用于哪些领域
开发语言·后端·rust
JaguarJack4 小时前
现代 PHP8+ 实战特性介绍 Enums、Fibers 和 Attributes
后端·php
绝无仅有4 小时前
面试真实经历某商银行大厂Java问题和答案总结(四)
后端·面试·github
绝无仅有4 小时前
面试真实经历某商银行大厂Java问题和答案总结(六)
后端·面试·github
IT_陈寒4 小时前
Vue3性能优化实战:这7个技巧让我的应用提速50%,尤雨溪都点赞!
前端·人工智能·后端
yuniko-n4 小时前
【力扣 SQL 50】连接
数据库·后端·sql·算法·leetcode
白萤4 小时前
SpringBoot用户登录注册系统设计与实现
java·spring boot·后端
canonical_entropy4 小时前
告别异常继承树:从 NopException 的设计看“组合”模式如何重塑错误处理
后端·架构
Victor3565 小时前
Redis(65)如何优化Redis的AOF持久化?
后端