# Body 匹配优化:基于 Content-Type 的智能解析 ## Context **问题**: 当前只支持 JSON body 匹配,非 JSON 请求无法正确匹配。 **需求**: 根据配置和请求的 Content-Type 智能选择解析方式,支持 JSON、XML、Form、Text 等类型。 --- ## 核心设计 ### Body 解析规则(优先级) | 优先级 | YAML Content-Type | 请求 Content-Type | 解析方式 | |--------|-------------------|-------------------|----------| | 1 | 有 | 任意 | 按 **YAML** 的类型解析 | | 2 | 无 | 有 | 按 **请求**的 Content-Type 解析 | | 3 | 无 | 无 | **字符串**比较 | ### 支持的 Content-Type | Content-Type | 解析结果 | |--------------|----------| | `application/json` | `ParsedBody::Json(Value)` | | `application/xml`, `text/xml` | `ParsedBody::Xml(String)` | | `application/x-www-form-urlencoded` | `ParsedBody::Form(HashMap)` | | `multipart/form-data` | `ParsedBody::Multipart(Vec)` | | `text/plain` 或其他 | `ParsedBody::Text(String)` | --- ## 实现方案 ### Step 1: 新增数据结构 (model.rs) ```rust /// 解析后的请求 Body #[derive(Debug, Clone)] pub enum ParsedBody { Json(serde_json::Value), Xml(String), Form(HashMap), Multipart(Vec), // 字段名列表 Text(String), None, } impl ParsedBody { /// 转换为字符串(用于兜底比较) pub fn to_compare_string(&self) -> String { match self { ParsedBody::Json(v) => v.to_string(), ParsedBody::Xml(s) | ParsedBody::Text(s) => s.clone(), ParsedBody::Form(map) => { let mut pairs: Vec<_> = map.iter().collect(); pairs.sort_by_key(|(k, _)| *k); pairs.iter().map(|(k, v)| format!("{}={}", k, v)).join("&") } ParsedBody::Multipart(fields) => fields.join(","), ParsedBody::None => String::new(), } } } ``` ### Step 2: 解析函数 (handler.rs) ```rust /// 提取 Content-Type(去掉参数部分) fn extract_content_type(headers: &HashMap) -> Option { headers.iter() .find(|(k, _)| k.to_lowercase() == "content-type") .map(|(_, v)| v.split(';').next().unwrap_or(v).trim().to_lowercase()) } /// 根据 Content-Type 解析 Body fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> ParsedBody { if bytes.is_empty() { return ParsedBody::None; } match content_type { Some(ct) if ct.contains("application/json") => { serde_json::from_slice(bytes) .map(ParsedBody::Json) .unwrap_or_else(|_| ParsedBody::Text(String::from_utf8_lossy(bytes).to_string())) } Some(ct) if ct.contains("xml") => { ParsedBody::Xml(String::from_utf8_lossy(bytes).to_string()) } Some(ct) if ct.contains("form-urlencoded") => { ParsedBody::Form(parse_urlencoded(bytes)) } Some(ct) if ct.contains("multipart/form-data") => { ParsedBody::Multipart(extract_multipart_fields(bytes)) } _ => { ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()) } } } ``` ### Step 3: 确定解析类型 (handler.rs) ```rust // 在 mock_handler 中: // 1. 提取请求的 Content-Type let req_content_type = extract_content_type(&req_headers); // 2. 读取请求 body let body_bytes = ...; let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes); // 3. 匹配时传递 req_headers 和 parsed_body // router 会根据 YAML 中是否配置了 Content-Type 来决定使用哪个类型 ``` ### Step 4: 匹配逻辑 (router.rs) ```rust fn match_body( &self, rule: &MockRule, parsed_body: &ParsedBody, req_content_type: Option<&str>, ) -> bool { let yaml_body = match &rule.request.body { Some(b) => b, None => return true, // YAML 没配置 body,跳过检查 }; // 确定用于解析/比较的 Content-Type let effective_content_type = rule.request.headers .as_ref() .and_then(|h| h.iter() .find(|(k, _)| k.to_lowercase() == "content-type") .map(|(_, v)| v.as_str())) .or(req_content_type); // YAML 优先,没有则用请求的 match effective_content_type { Some(ct) if ct.contains("application/json") => { // JSON 比较 match parsed_body { ParsedBody::Json(actual) => yaml_body == actual, _ => false, } } Some(ct) if ct.contains("xml") => { // XML 字符串比较 match parsed_body { ParsedBody::Xml(actual) => yaml_body.as_str() .map(|expected| expected.trim() == actual.trim()) .unwrap_or(false), _ => false, } } Some(ct) if ct.contains("form-urlencoded") => { // Form 比较 match parsed_body { ParsedBody::Form(actual) => compare_form(yaml_body, actual), _ => false, } } _ => { // 无 Content-Type 或其他类型:字符串比较 let actual_str = parsed_body.to_compare_string(); let expected_str = yaml_body.to_string(); expected_str.trim() == actual_str.trim() } } } ``` --- ## 需要修改的文件 | 文件 | 改动 | |------|------| | `src/model.rs` | 新增 `ParsedBody` 枚举和 `to_compare_string()` 方法 | | `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、辅助解析函数 | | `src/router.rs` | 新增 `match_body()` 方法,调整 `is_match()` 调用 | | `Cargo.toml` | 可能需要 `urlencoding` 依赖(form 解码) | --- ## YAML 配置示例 ```yaml # JSON 匹配(指定 Content-Type) - id: login-json request: method: POST path: /api/login headers: Content-Type: application/json body: username: admin # XML 匹配 - id: login-xml request: method: POST path: /api/login headers: Content-Type: application/xml body: "admin" # Form 匹配 - id: login-form request: method: POST path: /api/login headers: Content-Type: application/x-www-form-urlencoded body: username: admin # 字符串匹配(无 Content-Type) - id: echo request: method: POST path: /api/echo body: "hello world" ``` --- ## 验证 1. `cargo test` 2. 手动测试各类型: - JSON 请求 + JSON 规则 → 匹配 - XML 请求 + XML 规则 → 匹配 - Form 请求 + Form 规则 → 匹配 - 无 Content-Type → 字符串比较