- 将mock配置从YAML格式迁移到JSON格式 - 修复JSON字符串格式body匹配失败问题 - 添加MCP功能模块 - 更新mock-spec.md规范文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
218 lines
7.5 KiB
Rust
218 lines
7.5 KiB
Rust
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
|
||
}
|