Files
mock-server/src/router.rs
CNWei d364307131 feat: mock配置迁移至JSON格式并修复body匹配
- 将mock配置从YAML格式迁移到JSON格式
- 修复JSON字符串格式body匹配失败问题
- 添加MCP功能模块
- 更新mock-spec.md规范文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 09:43:11 +08:00

218 lines
7.5 KiB
Rust
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.

use std::collections::HashMap;
use crate::models::{MockRule, Payload};
pub struct MockRouter {
// 索引表Key 是路径首段(如 "api"Value 是该段下的所有 Mock 规则
pub index: HashMap<String, Vec<MockRule>>,
}
impl MockRouter {
pub fn new(index: HashMap<String, Vec<MockRule>>) -> Self {
Self { index }
}
/// 核心匹配函数:根据请求信息寻找匹配的 Mock 规则
pub fn match_rule(
&self,
method: &str,
path: &str,
queries: &HashMap<String, String>,
headers: &HashMap<String, String>,
payload: &Payload,
) -> Option<&MockRule> {
// 1. 提取请求路径的首段作为索引 Key
let key = self.extract_first_segment(path);
println!("DEBUG: Request Key: '{}', Available Keys: {:?}", key, self.index.keys());
// 2. 从 HashMap 中快速定位候选规则列表 (O(k) 复杂度)
if let Some(rules) = self.index.get(&key) {
// 3. 在候选集中进行线性深度匹配
for rule in rules {
if self.is_match(rule, method, path, queries, headers, payload) {
return Some(rule);
}
}
}
None
}
/// 细粒度匹配逻辑
fn is_match(
&self,
rule: &MockRule,
method: &str,
path: &str,
queries: &HashMap<String, String>,
headers: &HashMap<String, String>,
payload: &Payload,
) -> bool {
// A. 基础校验Method 和 Path 必须完全一致 (忽略末尾斜杠)
if rule.request.method.to_uppercase() != method.to_uppercase() {
println!("DEBUG: [NAME:{}] Method Mismatch: YAML={}, Req={}", rule.name, rule.request.method, method);
return false;
}
if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
println!("DEBUG: [NAME:{}] Path Mismatch: YAML='{}', Req='{}'", rule.name, rule.request.path, path);
return false;
}
println!("DEBUG: [NAME:{}] Method and Path matched! Checking headers...", rule.name);
// B. 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;
}
}
}
// C. Header 校验 (大小写不敏感Content-Type 使用前缀匹配)
if let Some(ref required_headers) = rule.request.headers {
for (key, val) in required_headers {
let key_lower = key.to_lowercase();
let matched = headers.iter().any(|(k, v)| {
if k.to_lowercase() != key_lower {
return false;
}
// Content-Type 使用前缀匹配(因为可能包含 boundary 等参数)
if key_lower == "content-type" {
v.to_lowercase().starts_with(&val.to_lowercase())
} else {
v == val
}
});
if !matched {
return false;
}
}
}
// D. Body 匹配(根据 Payload 类型智能比较)
if let Some(ref rule_body) = rule.request.body {
return self.match_body(rule_body, payload);
}
true
}
/// Body 匹配逻辑
fn match_body(&self, rule_body: &serde_json::Value, payload: &Payload) -> bool {
match payload {
Payload::Json(actual) => {
// 如果 rule_body 是字符串,尝试解析为 JSON 后比较
if let Some(rule_str) = rule_body.as_str() {
// 尝试将字符串解析为 JSON
if let Ok(parsed_rule) = serde_json::from_str::<serde_json::Value>(rule_str) {
return &parsed_rule == actual;
}
}
// 直接比较
rule_body == actual
}
Payload::Xml(actual) => {
// XML 字符串比较(规范化后比较)
rule_body.as_str()
.map(|expected| normalize_xml(expected) == normalize_xml(actual))
.unwrap_or(false)
}
Payload::Form(actual) => {
// Form 键值对比较(子集匹配)
compare_form_with_json(rule_body, actual)
}
Payload::Multipart(actual_data) => {
// Multipart 匹配:支持键值对或字段名列表
compare_multipart_with_json(rule_body, actual_data)
}
Payload::Text(actual) => {
// 字符串比较(去掉首尾空白)
rule_body.as_str()
.map(|expected| expected.trim() == actual.trim())
.unwrap_or_else(|| rule_body.to_string().trim() == actual.trim())
}
Payload::None => {
false // 配置了 body但请求没有 body
}
}
}
/// 与 Loader 保持一致的 Key 提取算法
fn extract_first_segment(&self, path: &str) -> String {
path.trim_start_matches('/')
.split('/')
.next()
.unwrap_or("root")
.to_string()
}
}
/// 规范化 XML 字符串:去掉声明、多余空白、格式化为紧凑形式
fn normalize_xml(xml: &str) -> String {
let mut result = xml.to_string();
// 去掉 XML 声明
if let Some(pos) = result.find("?>") {
if result[..pos].contains("<?xml") {
result = result[pos + 2..].to_string();
}
}
// 去掉多余空白字符
result = result
.chars()
.collect::<Vec<_>>()
.chunks(1)
.map(|c| c[0])
.collect::<String>();
// 分割成行,去掉每行首尾空白,过滤空行
result = result
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<String>();
result
}
/// Form 比较JSON 中的键值对必须是请求的子集
fn compare_form_with_json(rule_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
let rule_map = match rule_body.as_object() {
Some(obj) => obj,
None => return false,
};
for (key, rule_val) in rule_map {
let expected = rule_val.as_str().map(|s| s.to_string()).unwrap_or_else(|| rule_val.to_string());
if actual.get(key) != Some(&expected) {
return false;
}
}
true
}
/// Multipart 比较:对象和数组形式都只匹配字段名是否存在
fn compare_multipart_with_json(rule_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
// 方式 1对象形式 - 只匹配键名是否存在(忽略值)
if let Some(rule_map) = rule_body.as_object() {
for key in rule_map.keys() {
if !actual.contains_key(key) {
return false;
}
}
return true;
}
// 方式 2数组形式 - 只匹配字段名是否存在
if let Some(rule_array) = rule_body.as_array() {
for rule_field in rule_array {
let field_name = rule_field.as_str().map(|s| s.to_string()).unwrap_or_else(|| rule_field.to_string());
if !actual.contains_key(&field_name) {
return false;
}
}
return true;
}
false
}