# Body 匹配优化：基于 Content-Type 的智能解析

## Context

**问题**: 当前只支持 JSON body 匹配，非 JSON 请求无法正确匹配。

**需求**: 根据请求的 Content-Type 智能解析 body，同时支持 header 匹配校验。

---

## 核心设计

### 关键原则

1. **Body 解析**：始终以【请求的 Content-Type】为准，因为这是数据的真实格式
2. **Header 匹配**：Content-Type 当作普通 header 处理，写了就匹配，没写跳过
3. **自动补充的 header**：Content-Length、Accept-Encoding 等不参与匹配（除非 YAML 显式配置）

### 处理流程

```
┌─────────────────────────────────────────────────────────────┐
│                        请求处理流程                          │
├─────────────────────────────────────────────────────────────┤
│  1. 解析请求 body                                            │
│     └── 始终以【请求的 Content-Type】为准解析                 │
│                                                              │
│  2. Header 匹配（包含 Content-Type）                         │
│     └── YAML 写了就匹配，没写就跳过                          │
│                                                              │
│  3. Body 匹配                                                │
│     └── 根据匹配成功的 Content-Type 决定比较方式              │
└─────────────────────────────────────────────────────────────┘
```

### 匹配示例

| YAML Content-Type | 请求 Content-Type | Header 匹配 | Body 解析 | 结果 |
|-------------------|-------------------|-------------|-----------|------|
| `application/xml` | `application/json` | 失败 | - | 不匹配 |
| `application/json` | `application/json` | 成功 | JSON | 比较 body |
| 无 | `application/json` | 跳过 | JSON | 比较 body |
| 无 | `application/xml` | 跳过 | XML | 比较 body |
| 无 | 无 | 跳过 | 字符串 | 比较 body |

---

## 实现方案

### 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(),
        }
    }

    /// 获取对应的 Content-Type 名称
    pub fn content_type_name(&self) -> &'static str {
        match self {
            ParsedBody::Json(_) => "application/json",
            ParsedBody::Xml(_) => "application/xml",
            ParsedBody::Form(_) => "application/x-www-form-urlencoded",
            ParsedBody::Multipart(_) => "multipart/form-data",
            ParsedBody::Text(_) => "text/plain",
            ParsedBody::None => "none",
        }
    }
}
```

### Step 2: Body 解析函数 (handler.rs)

```rust
/// 提取请求的 Content-Type（去掉参数部分，如 boundary）
fn extract_content_type(headers: &HeaderMap) -> Option<String> {
    headers
        .get(axum::http::header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .map(|s| s.split(';').next().unwrap_or(s).trim().to_lowercase())
}

/// 根据 Content-Type 解析 Body（始终以请求的 Content-Type 为准）
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(|_| {
                    // JSON 解析失败，降级为文本
                    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())
        }
    }
}

/// 解析 urlencoded 格式
fn parse_urlencoded(bytes: &[u8]) -> HashMap<String, String> {
    let body = String::from_utf8_lossy(bytes);
    let mut map = HashMap::new();
    for pair in body.split('&') {
        if let Some((key, value)) = pair.split_once('=') {
            // URL 解码
            let decoded_key = urlencoding_decode(key);
            let decoded_value = urlencoding_decode(value);
            map.insert(decoded_key, decoded_value);
        }
    }
    map
}
```

### Step 3: 修改 handler.rs 主函数

```rust
pub async fn mock_handler(
    State(state): State<Arc<AppState>>,
    method: Method,
    headers: HeaderMap,
    Query(params): Query<HashMap<String, String>>,
    req: Request<Body>,
) -> impl IntoResponse {
    let path = req.uri().path().to_string();
    let method_str = method.as_str().to_string();

    // 1. 提取请求的 Content-Type
    let req_content_type = extract_content_type(&headers);

    // 2. 读取请求 body
    let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await {
        Ok(bytes) => bytes,
        Err(_) => {
            return Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body(Body::from("Read body error"))
                .unwrap();
        }
    };

    // 3. 根据【请求的 Content-Type】解析 body
    let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes);

    // 4. 转换 headers 为 HashMap
    let mut req_headers = HashMap::new();
    for (name, value) in headers.iter() {
        if let Ok(v) = value.to_str() {
            req_headers.insert(name.as_str().to_string(), v.to_string());
        }
    }

    // 5. 执行匹配
    let maybe_rule = {
        let router = state.router.read().expect("Failed to acquire read lock");
        router.match_rule(&method_str, &path, &params, &req_headers, &parsed_body).cloned()
    };

    // 6. 构建响应（与现有逻辑相同）
    // ...
}
```

### Step 4: 修改 router.rs 匹配逻辑

```rust
/// 核心匹配函数
pub fn match_rule(
    &self,
    method: &str,
    path: &str,
    queries: &HashMap<String, String>,
    headers: &HashMap<String, String>,
    parsed_body: &ParsedBody,  // 改为 ParsedBody
) -> Option<&MockRule> {
    let key = self.extract_first_segment(path);

    if let Some(rules) = self.index.get(&key) {
        for rule in rules {
            if self.is_match(rule, method, path, queries, headers, parsed_body) {
                return Some(rule);
            }
        }
    }
    None
}

fn is_match(
    &self,
    rule: &MockRule,
    method: &str,
    path: &str,
    queries: &HashMap<String, String>,
    headers: &HashMap<String, String>,
    parsed_body: &ParsedBody,
) -> bool {
    // A. Method 匹配
    if rule.request.method.to_uppercase() != method.to_uppercase() {
        return false;
    }

    // B. Path 匹配
    if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
        return false;
    }

    // C. Query 匹配（子集匹配）
    if let Some(ref required_queries) = rule.request.query_params {
        for (key, val) in required_queries {
            if queries.get(key) != Some(val) {
                return false;
            }
        }
    }

    // D. Header 匹配（包含 Content-Type）
    //    YAML 写了就匹配，没写就跳过
    if let Some(ref required_headers) = rule.request.headers {
        for (key, val) in required_headers {
            let matched = headers.iter().any(|(k, v)| {
                k.to_lowercase() == key.to_lowercase() && v == val
            });
            if !matched {
                return false;  // Header 不匹配，包括 Content-Type
            }
        }
    }

    // E. Body 匹配
    //    YAML 写了 body 才匹配，没写跳过
    if let Some(ref yaml_body) = rule.request.body {
        return self.match_body(yaml_body, parsed_body);
    }

    true
}

/// Body 匹配逻辑
fn match_body(&self, yaml_body: &serde_json::Value, parsed_body: &ParsedBody) -> bool {
    match parsed_body {
        ParsedBody::Json(actual) => {
            // JSON 对象比较
            yaml_body == actual
        }
        ParsedBody::Xml(actual) => {
            // XML 字符串比较
            yaml_body.as_str()
                .map(|expected| expected.trim() == actual.trim())
                .unwrap_or(false)
        }
        ParsedBody::Form(actual) => {
            // Form 键值对比较（子集匹配）
            compare_form_with_yaml(yaml_body, actual)
        }
        ParsedBody::Multipart(actual_fields) => {
            // Multipart 字段名比较
            compare_multipart_with_yaml(yaml_body, actual_fields)
        }
        ParsedBody::Text(actual) => {
            // 字符串比较
            yaml_body.as_str()
                .map(|expected| expected.trim() == actual.trim())
                .unwrap_or_else(|| yaml_body.to_string().trim() == actual.trim())
        }
        ParsedBody::None => {
            false  // YAML 配置了 body，但请求没有 body
        }
    }
}

/// Form 比较：YAML 中的键值对必须是请求的子集
fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
    let yaml_map = match yaml_body.as_object() {
        Some(obj) => obj,
        None => return false,
    };

    for (key, yaml_val) in yaml_map {
        let expected = yaml_val.as_str().unwrap_or(&yaml_val.to_string());
        if actual.get(key) != Some(&expected.to_string()) {
            return false;
        }
    }
    true
}
```

---

## 需要修改的文件

| 文件 | 改动 |
|------|------|
| `src/model.rs` | 新增 `ParsedBody` 枚举和相关方法 |
| `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、修改 `mock_handler()` |
| `src/router.rs` | 修改 `match_rule()` 和 `is_match()` 参数，新增 `match_body()` |
| `Cargo.toml` | 可能需要 `urlencoding` 依赖 |

---

## YAML 配置示例

```yaml
# 严格匹配：要求 Content-Type + body
- id: login-strict
  request:
    method: POST
    path: /api/login
    headers:
      Content-Type: application/json
    body:
      username: admin
      password: "123456"

# 宽松匹配：只匹配 method + path + body（不检查 Content-Type）
- id: login-loose
  request:
    method: POST
    path: /api/login
    body:
      username: admin

# 最宽松：只匹配 method + path
- id: any-body
  request:
    method: POST
    path: /api/echo
```

---

## 验证

1. `cargo test`
2. 手动测试：
   - YAML 配置 `Content-Type: application/json`，请求发送 JSON → 匹配
   - YAML 配置 `Content-Type: application/xml`，请求发送 JSON → Header 不匹配
   - YAML 无 Content-Type，请求发送 XML → Body 字符串比较
   - YAML 无 body 配置 → 跳过 body 检查
