合并 feature-ms-v0.0.1-SNAPSHOOT-20260319 的热重载实现: - 使用 notify-debouncer-mini 监听 mocks 目录变化 - AppState 使用 RwLock<MockRouter> 支持并发读写 - 200ms 防抖避免编辑器保存时的多次触发 保留当前分支的功能: - 文件上传功能 (upload.rs) - 请求体 body 匹配支持 - 完整的测试覆盖 (43个测试) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
106 lines
3.6 KiB
Rust
106 lines
3.6 KiB
Rust
use axum::{
|
||
body::Body,
|
||
extract::{Query, State},
|
||
http::{HeaderMap, Method, Request, StatusCode},
|
||
response::{IntoResponse, Response},
|
||
};
|
||
use std::collections::HashMap;
|
||
use std::sync::{Arc, RwLock};
|
||
use tokio_util::io::ReaderStream;
|
||
|
||
use crate::router::MockRouter;
|
||
|
||
/// 共享的应用状态,router 由 RwLock 保护以支持热重载
|
||
pub struct AppState {
|
||
pub router: RwLock<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().to_string();
|
||
let method_str = method.as_str().to_string();
|
||
|
||
// 1. 将需要的数据克隆出来,断开与 req 的借用关系
|
||
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();
|
||
}
|
||
};
|
||
|
||
let incoming_json: Option<serde_json::Value> = serde_json::from_slice(&body_bytes).ok();
|
||
|
||
// 2. 将 Axum HeaderMap 转换为简单的 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());
|
||
}
|
||
}
|
||
|
||
// 3. 执行匹配逻辑:先获取读锁 (Read Lock)
|
||
let maybe_rule = {
|
||
let router = state.router.read().expect("Failed to acquire read lock");
|
||
router.match_rule(&method_str, &path, ¶ms, &req_headers, &incoming_json).cloned()
|
||
// 使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁
|
||
};
|
||
|
||
if let Some(rule) = maybe_rule {
|
||
// 4. 处理模拟延迟
|
||
if let Some(ref settings) = rule.settings {
|
||
if let Some(delay) = settings.delay_ms {
|
||
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
|
||
}
|
||
}
|
||
|
||
// 5. 构建响应
|
||
let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
|
||
let mut response_builder = Response::builder().status(status);
|
||
|
||
if let Some(ref h) = rule.response.headers {
|
||
for (k, v) in h {
|
||
response_builder = response_builder.header(k, v);
|
||
}
|
||
}
|
||
|
||
// 6. Smart Body 逻辑
|
||
if let Some(file_path) = rule.response.get_file_path() {
|
||
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 {
|
||
// 内联模式:直接返回字符串内容
|
||
response_builder
|
||
.body(Body::from(rule.response.body.clone()))
|
||
.unwrap()
|
||
}
|
||
} else {
|
||
println!("请求头{:?}", req_headers);
|
||
// 匹配失败返回 404
|
||
Response::builder()
|
||
.status(StatusCode::NOT_FOUND)
|
||
.body(Body::from("No mock rule matched this request"))
|
||
.unwrap()
|
||
}
|
||
}
|