6.6 KiB
6.6 KiB
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)
/// 解析后的请求 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)
/// 提取 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)
// 在 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)
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 配置示例
# 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"
验证
cargo test- 手动测试各类型:
- JSON 请求 + JSON 规则 → 匹配
- XML 请求 + XML 规则 → 匹配
- Form 请求 + Form 规则 → 匹配
- 无 Content-Type → 字符串比较