diff --git a/Cargo.lock b/Cargo.lock index 659de77..607bd6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 5b9b191..f831bc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/mocks/v1/auth/login.yaml b/mocks/v1/auth/login.yaml index 64ec57c..d2e56be 100644 --- a/mocks/v1/auth/login.yaml +++ b/mocks/v1/auth/login.yaml @@ -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 # 模拟真实网络延迟 \ No newline at end of file + delay_ms: 2000 # 模拟真实网络延迟 \ No newline at end of file diff --git a/src/handler.rs b/src/handler.rs index a094735..f991c4f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -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, } /// 全局统一请求处理函数 pub async fn mock_handler( - State(state): State>, + State(state): State>, // State 必须是第一个或靠前的参数 method: Method, headers: HeaderMap, Query(params): Query>, - req: Request, + req: Request, // Request 必须是最后一个参数 ) -> 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")) diff --git a/src/main.rs b/src/main.rs index ef70ecd..7f8fa92 100644 --- a/src/main.rs +++ b/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();