Files
mock-server/plan.md
2026-03-26 19:20:22 +08:00

240 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 → 字符串比较