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",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@@ -109,6 +115,15 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"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]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -271,12 +286,52 @@ dependencies = [
|
|||||||
"hashbrown",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.16"
|
version = "1.0.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -335,6 +390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
|
"log",
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -345,6 +401,8 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"notify",
|
||||||
|
"notify-debouncer-mini",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
@@ -356,6 +414,42 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
@@ -442,7 +536,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -451,7 +545,7 @@ version = "1.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ tracing-subscriber = "0.3.22"
|
|||||||
# 性能优化:快速哈希(可选,用于路由匹配)
|
# 性能优化:快速哈希(可选,用于路由匹配)
|
||||||
#dashmap = "7.0.0-rc2"
|
#dashmap = "7.0.0-rc2"
|
||||||
# 热加载支持(扩展功能)
|
# 热加载支持(扩展功能)
|
||||||
#notify = "8.2.0"
|
notify = "8.2.0"
|
||||||
|
notify-debouncer-mini = "0.6.0"
|
||||||
# 路径处理
|
# 路径处理
|
||||||
#pathdiff = "0.2.3"
|
#pathdiff = "0.2.3"
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ request:
|
|||||||
Content-Type: "application/json"
|
Content-Type: "application/json"
|
||||||
Authorization: "111"
|
Authorization: "111"
|
||||||
host: "127.0.0.1:8080"
|
host: "127.0.0.1:8080"
|
||||||
|
body: >
|
||||||
|
{}
|
||||||
response:
|
response:
|
||||||
status: 200
|
status: 200
|
||||||
headers:
|
headers:
|
||||||
@@ -20,4 +22,4 @@ response:
|
|||||||
"msg": "success"
|
"msg": "success"
|
||||||
}
|
}
|
||||||
settings:
|
settings:
|
||||||
delay_ms: 200 # 模拟真实网络延迟
|
delay_ms: 2000 # 模拟真实网络延迟
|
||||||
@@ -5,27 +5,27 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, RwLock}; // 必须引入 RwLock
|
||||||
use tokio_util::io::ReaderStream; // 需在 Cargo.toml 确认有 tokio-util
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::router::MockRouter;
|
use crate::router::MockRouter;
|
||||||
|
|
||||||
/// 共享的应用状态
|
/// 共享的应用状态,router 现在由 RwLock 保护以支持热重载
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub router: MockRouter,
|
pub router: RwLock<MockRouter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 全局统一请求处理函数
|
/// 全局统一请求处理函数
|
||||||
pub async fn mock_handler(
|
pub async fn mock_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>, // State 必须是第一个或靠前的参数
|
||||||
method: Method,
|
method: Method,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(params): Query<HashMap<String, String>>,
|
Query(params): Query<HashMap<String, String>>,
|
||||||
req: Request<Body>,
|
req: Request<Body>, // Request<Body> 必须是最后一个参数
|
||||||
) -> impl IntoResponse {
|
) -> 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();
|
let mut req_headers = HashMap::new();
|
||||||
for (name, value) in headers.iter() {
|
for (name, value) in headers.iter() {
|
||||||
if let Ok(v) = value.to_str() {
|
if let Ok(v) = value.to_str() {
|
||||||
@@ -33,9 +33,14 @@ pub async fn mock_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 执行匹配逻辑
|
// 2. 执行匹配逻辑:先获取读锁 (Read Lock)
|
||||||
if let Some(rule) = state.router.match_rule(method.as_str(), path, ¶ms, &req_headers) {
|
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. 处理模拟延迟
|
// 3. 处理模拟延迟
|
||||||
if let Some(ref settings) = rule.settings {
|
if let Some(ref settings) = rule.settings {
|
||||||
if let Some(delay) = settings.delay_ms {
|
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 status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK);
|
||||||
let mut response_builder = Response::builder().status(status);
|
let mut response_builder = Response::builder().status(status);
|
||||||
|
|
||||||
// 注入 YAML 定义的 Header
|
|
||||||
if let Some(ref h) = rule.response.headers {
|
if let Some(ref h) = rule.response.headers {
|
||||||
|
|
||||||
for (k, v) in h {
|
for (k, v) in h {
|
||||||
// println!("{}:{}",k.clone(), v.clone());
|
|
||||||
response_builder = response_builder.header(k, v);
|
response_builder = response_builder.header(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 执行 Smart Body 协议逻辑
|
// 5. Smart Body 逻辑
|
||||||
if let Some(file_path) = rule.response.get_file_path() {
|
if let Some(file_path) = rule.response.get_file_path() {
|
||||||
// A. 文件模式:异步打开文件并转换为流,实现低内存占用
|
|
||||||
match tokio::fs::File::open(file_path).await {
|
match tokio::fs::File::open(file_path).await {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
let stream = ReaderStream::new(file);
|
let stream = ReaderStream::new(file);
|
||||||
let body = Body::from_stream(stream);
|
response_builder.body(Body::from_stream(stream)).unwrap()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
Err(_) => Response::builder()
|
||||||
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Body::from(format!("File not found: {}", file_path)))
|
||||||
|
.unwrap(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// B. 内联模式:直接返回字符串内容
|
|
||||||
response_builder.body(Body::from(rule.response.body.clone())).unwrap()
|
response_builder.body(Body::from(rule.response.body.clone())).unwrap()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("请求头{:?}",req_headers.clone());
|
// 匹配失败
|
||||||
// 匹配失败返回 404
|
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::NOT_FOUND)
|
.status(StatusCode::NOT_FOUND)
|
||||||
.body(Body::from("No mock rule matched this request"))
|
.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::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
use axum::{routing::any, Router};
|
use axum::{routing::any, Router};
|
||||||
|
use notify_debouncer_mini::{new_debouncer, notify::*};
|
||||||
|
|
||||||
use mock_server::loader::MockLoader;
|
use mock_server::loader::MockLoader;
|
||||||
use mock_server::router::MockRouter;
|
use mock_server::router::MockRouter;
|
||||||
use mock_server::handler::{mock_handler, AppState};
|
use mock_server::handler::{mock_handler, AppState};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// 1. 初始化日志(建议添加,方便观察加载情况)
|
|
||||||
// 需要在 Cargo.toml 添加 tracing-subscriber = "0.3"
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// 2. 递归加载所有的 YAML 配置文件
|
|
||||||
let mocks_dir = Path::new("./mocks");
|
let mocks_dir = Path::new("./mocks");
|
||||||
if !mocks_dir.exists() {
|
if !mocks_dir.exists() {
|
||||||
println!("Warning: 'mocks/' directory not found. Creating it...");
|
|
||||||
std::fs::create_dir_all(mocks_dir).unwrap();
|
std::fs::create_dir_all(mocks_dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. 初始加载
|
||||||
println!("Scanning mocks directory...");
|
println!("Scanning mocks directory...");
|
||||||
let index = MockLoader::load_all_from_dir(mocks_dir);
|
let index = MockLoader::load_all_from_dir(mocks_dir);
|
||||||
|
|
||||||
// 3. 构建路由引擎并包装为共享状态
|
|
||||||
let router_engine = MockRouter::new(index);
|
|
||||||
let shared_state = Arc::new(AppState {
|
let shared_state = Arc::new(AppState {
|
||||||
router: router_engine,
|
router: RwLock::new(MockRouter::new(index)),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. 配置 Axum 路由
|
// 2. 设置热加载监听器
|
||||||
// 使用 any(mock_handler) 意味着它会接管所有 HTTP 方法和所有路径的请求
|
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()
|
let app = Router::new()
|
||||||
.fallback(any(mock_handler))
|
.fallback(any(mock_handler))
|
||||||
.with_state(shared_state);
|
.with_state(shared_state);
|
||||||
|
|
||||||
// 5. 启动服务
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
println!("🚀 Rust Mock Server is running on http://{}", addr);
|
println!("🚀 Server running at http://{}", addr);
|
||||||
println!("Ready to handle requests based on your YAML definitions.");
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user