feat: 实现核心匹配引擎与请求处理器并修复解析逻辑

- 新增 router 模块:实现基于路径首段索引子集匹配。
- 新增 handler 模块:集成 Axum 处理器,支持 Smart Body 协议与延迟模拟。
- 修复解析与匹配故障:修正 YAML 字段类型解析错误。
This commit is contained in:
2025-12-26 15:21:03 +08:00
parent 748cfa8e7f
commit 1775d3659d
9 changed files with 497 additions and 9 deletions

87
src/handler.rs Normal file
View File

@@ -0,0 +1,87 @@
use axum::{
body::Body,
extract::{Query, State},
http::{HeaderMap, Method, Request, StatusCode},
response::{IntoResponse, Response},
};
use std::collections::HashMap;
use std::sync::Arc;
use tokio_util::io::ReaderStream; // 需在 Cargo.toml 确认有 tokio-util
use crate::router::MockRouter;
/// 共享的应用状态
pub struct AppState {
pub router: MockRouter,
}
/// 全局统一请求处理函数
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();
// 1. 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用
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());
}
}
// 2. 执行匹配逻辑
if let Some(rule) = state.router.match_rule(method.as_str(), path, &params, &req_headers) {
// 3. 处理模拟延迟
if let Some(ref settings) = rule.settings {
if let Some(delay) = settings.delay_ms {
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
}
}
// 4. 构建响应基础信息
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
let mut response_builder = Response::builder().status(status);
// 注入 YAML 定义的 Header
if let Some(ref h) = rule.response.headers {
for (k, v) in h {
// println!("{}:{}",k.clone(), v.clone());
response_builder = response_builder.header(k, v);
}
}
// 5. 执行 Smart Body 协议逻辑
if let Some(file_path) = rule.response.get_file_path() {
// A. 文件模式:异步打开文件并转换为流,实现低内存占用
match tokio::fs::File::open(file_path).await {
Ok(file) => {
let stream = ReaderStream::new(file);
let body = Body::from_stream(stream);
response_builder.body(body).unwrap()
}
Err(_) => {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Mock Error: File not found at {}", file_path)))
.unwrap()
}
}
} else {
// B. 内联模式:直接返回字符串内容
response_builder.body(Body::from(rule.response.body.clone())).unwrap()
}
} else {
println!("请求头{:?}",req_headers.clone());
// 匹配失败返回 404
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("No mock rule matched this request"))
.unwrap()
}
}

View File

@@ -1,3 +1,5 @@
// 声明模块并设为 pub这样 tests/ 目录才能看到它们
pub mod config;
pub mod loader;
pub mod loader;
pub mod router;
pub mod handler;

View File

@@ -1,9 +1,44 @@
// 使用项目名(下划线形式)引用 lib 中的内容
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use axum::{routing::any, Router};
use mock_server::loader::MockLoader;
use mock_server::router::MockRouter;
use mock_server::handler::{mock_handler, AppState};
#[tokio::main]
async fn main() {
// 你的启动逻辑...
let index = MockLoader::load_all_from_dir(std::path::Path::new("./mocks"));
println!("服务启动中...");
// 1. 初始化日志(建议添加,方便观察加载情况)
// 需要在 Cargo.toml 添加 tracing-subscriber = "0.3"
tracing_subscriber::fmt::init();
// 2. 递归加载所有的 YAML 配置文件
let mocks_dir = Path::new("./mocks");
if !mocks_dir.exists() {
println!("Warning: 'mocks/' directory not found. Creating it...");
std::fs::create_dir_all(mocks_dir).unwrap();
}
println!("Scanning mocks directory...");
let index = MockLoader::load_all_from_dir(mocks_dir);
// 3. 构建路由引擎并包装为共享状态
let router_engine = MockRouter::new(index);
let shared_state = Arc::new(AppState {
router: router_engine,
});
// 4. 配置 Axum 路由
// 使用 any(mock_handler) 意味着它会接管所有 HTTP 方法和所有路径的请求
let app = Router::new()
.fallback(any(mock_handler))
.with_state(shared_state);
// 5. 启动服务
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
println!("🚀 Rust Mock Server is running on http://{}", addr);
println!("Ready to handle requests based on your YAML definitions.");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}

93
src/router.rs Normal file
View File

@@ -0,0 +1,93 @@
use std::collections::HashMap;
use crate::config::MockRule;
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>,
) -> 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) {
return Some(rule);
}
}
}
None
}
/// 细粒度匹配逻辑
fn is_match(
&self,
rule: &MockRule,
method: &str,
path: &str,
queries: &HashMap<String, String>,
headers: &HashMap<String, String>,
) -> bool {
// A. 基础校验Method 和 Path 必须完全一致 (忽略末尾斜杠)
if rule.request.method.to_uppercase() != method.to_uppercase() {
println!("DEBUG: [ID:{}] Method Mismatch: YAML={}, Req={}", rule.id, rule.request.method, method);
return false;
}
if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
println!("DEBUG: [ID:{}] Path Mismatch: YAML='{}', Req='{}'", rule.id, rule.request.path, path);
return false;
}
println!("DEBUG: [ID:{}] Method and Path matched! Checking headers...", rule.id);
// 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 校验 (优化版)
if let Some(ref required_headers) = rule.request.headers {
for (key, val) in required_headers {
println!("{}:{}",key.clone(), val.clone());
// 方案:将 Key 统一转为小写比较,并检查请求头是否“包含”期望的值
let matched = headers.iter().any(|(k, v)| {
// k.to_lowercase() == key.to_lowercase() && v.contains(val)
k.to_lowercase() == key.to_lowercase() && v==val
});
if !matched {
return false;
}
}
}
true
}
/// 与 Loader 保持一致的 Key 提取算法
fn extract_first_segment(&self, path: &str) -> String {
path.trim_start_matches('/')
.split('/')
.next()
.unwrap_or("root")
.to_string()
}
}