240 lines
6.6 KiB
Markdown
240 lines
6.6 KiB
Markdown
# 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<String>)` |
|
||
| `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<String, String>),
|
||
Multipart(Vec<String>), // 字段名列表
|
||
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<String, String>) -> Option<String> {
|
||
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: "<user><name>admin</name></user>"
|
||
|
||
# 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 → 字符串比较
|