临时提交
This commit is contained in:
239
plan.md
Normal file
239
plan.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# 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 → 字符串比较
|
||||
Reference in New Issue
Block a user