Compare commits
1 Commits
feature-ms
...
feature-ms
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3097a16465 |
98
Cargo.lock
generated
98
Cargo.lock
generated
@@ -60,6 +60,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
@@ -109,6 +115,15 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -271,12 +286,52 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -335,6 +390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -345,6 +401,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures-util",
|
||||
"notify",
|
||||
"notify-debouncer-mini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
@@ -356,6 +414,42 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-mini"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify",
|
||||
"notify-types",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -442,7 +536,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -451,7 +545,7 @@ version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.10.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
||||
@@ -26,7 +26,8 @@ tracing-subscriber = "0.3.22"
|
||||
# 性能优化:快速哈希(可选,用于路由匹配)
|
||||
#dashmap = "7.0.0-rc2"
|
||||
# 热加载支持(扩展功能)
|
||||
#notify = "8.2.0"
|
||||
notify = "8.2.0"
|
||||
notify-debouncer-mini = "0.6.0"
|
||||
# 路径处理
|
||||
#pathdiff = "0.2.3"
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ request:
|
||||
Content-Type: "application/json"
|
||||
Authorization: "111"
|
||||
host: "127.0.0.1:8080"
|
||||
body: >
|
||||
{}
|
||||
response:
|
||||
status: 200
|
||||
headers:
|
||||
@@ -20,4 +22,4 @@ response:
|
||||
"msg": "success"
|
||||
}
|
||||
settings:
|
||||
delay_ms: 200 # 模拟真实网络延迟
|
||||
delay_ms: 2000 # 模拟真实网络延迟
|
||||
@@ -5,27 +5,27 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::io::ReaderStream; // 需在 Cargo.toml 确认有 tokio-util
|
||||
use std::sync::{Arc, RwLock}; // 必须引入 RwLock
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::router::MockRouter;
|
||||
|
||||
/// 共享的应用状态
|
||||
/// 共享的应用状态,router 现在由 RwLock 保护以支持热重载
|
||||
pub struct AppState {
|
||||
pub router: MockRouter,
|
||||
pub router: RwLock<MockRouter>,
|
||||
}
|
||||
|
||||
/// 全局统一请求处理函数
|
||||
pub async fn mock_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
State(state): State<Arc<AppState>>, // State 必须是第一个或靠前的参数
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
req: Request<Body>,
|
||||
req: Request<Body>, // Request<Body> 必须是最后一个参数
|
||||
) -> impl IntoResponse {
|
||||
let path = req.uri().path();
|
||||
let path = req.uri().path().to_string(); // 先提取 path
|
||||
|
||||
// 1. 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用
|
||||
// 1. 将 Axum HeaderMap 转换为简单的 HashMap
|
||||
let mut req_headers = HashMap::new();
|
||||
for (name, value) in headers.iter() {
|
||||
if let Ok(v) = value.to_str() {
|
||||
@@ -33,9 +33,14 @@ pub async fn mock_handler(
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 执行匹配逻辑
|
||||
if let Some(rule) = state.router.match_rule(method.as_str(), path, ¶ms, &req_headers) {
|
||||
// 2. 执行匹配逻辑:先获取读锁 (Read Lock)
|
||||
let maybe_rule = {
|
||||
let router = state.router.read().expect("Failed to acquire read lock");
|
||||
router.match_rule(method.as_str(), &path, ¶ms, &req_headers).cloned()
|
||||
// 此处使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁
|
||||
};
|
||||
|
||||
if let Some(rule) = maybe_rule {
|
||||
// 3. 处理模拟延迟
|
||||
if let Some(ref settings) = rule.settings {
|
||||
if let Some(delay) = settings.delay_ms {
|
||||
@@ -43,42 +48,33 @@ pub async fn mock_handler(
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 构建响应基础信息
|
||||
// 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 协议逻辑
|
||||
// 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()
|
||||
response_builder.body(Body::from_stream(stream)).unwrap()
|
||||
}
|
||||
Err(_) => Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::from(format!("File not found: {}", 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"))
|
||||
|
||||
48
src/main.rs
48
src/main.rs
@@ -1,43 +1,63 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use axum::{routing::any, Router};
|
||||
use notify_debouncer_mini::{new_debouncer, notify::*};
|
||||
|
||||
use mock_server::loader::MockLoader;
|
||||
use mock_server::router::MockRouter;
|
||||
use mock_server::handler::{mock_handler, AppState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 1. 初始加载
|
||||
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,
|
||||
router: RwLock::new(MockRouter::new(index)),
|
||||
});
|
||||
|
||||
// 4. 配置 Axum 路由
|
||||
// 使用 any(mock_handler) 意味着它会接管所有 HTTP 方法和所有路径的请求
|
||||
// 2. 设置热加载监听器
|
||||
let state_for_watcher = shared_state.clone();
|
||||
let watch_path = mocks_dir.to_path_buf();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
// 200ms 防抖,防止编辑器保存文件时产生多次干扰
|
||||
let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap();
|
||||
debouncer.watcher().watch(&watch_path, RecursiveMode::Recursive).unwrap();
|
||||
|
||||
// 启动异步任务监听文件变动
|
||||
tokio::spawn(async move {
|
||||
while let Ok(res) = rx.recv() {
|
||||
match res {
|
||||
Ok(_) => {
|
||||
println!("🔄 Detecting changes in mocks/, reloading...");
|
||||
let new_index = MockLoader::load_all_from_dir(&watch_path);
|
||||
// 获取写锁 (Write Lock) 更新索引
|
||||
let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock");
|
||||
*writer = MockRouter::new(new_index);
|
||||
println!("✅ Mocks reloaded successfully.");
|
||||
}
|
||||
Err(e) => eprintln!("Watcher error: {:?}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 配置 Axum 路由
|
||||
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.");
|
||||
println!("🚀 Server running at http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user