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

6.6 KiB
Raw Blame History

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"

验证

  1. cargo test
  2. 手动测试各类型:
    • JSON 请求 + JSON 规则 → 匹配
    • XML 请求 + XML 规则 → 匹配
    • Form 请求 + Form 规则 → 匹配
    • 无 Content-Type → 字符串比较