feat: 实现配置加载系统与多级目录递归扫描

- 定义支持“一接口一文件”与“一文件多接口”的数据模型
- 实现基于路径首段的 HashMap 索引构建逻辑
- 新增集成测试 tests/integration_test.rs,验证 YAML 解析与目录递归加载
- 优化 Cargo.toml 配置,解决连字符项目名引用问题
This commit is contained in:
2025-12-25 18:01:09 +08:00
parent 9a48c156c8
commit 748cfa8e7f
11 changed files with 395 additions and 7 deletions

74
src/config.rs Normal file
View File

@@ -0,0 +1,74 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// 顶层包装:支持单对象或数组,自动打平
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum MockSource {
/// 对应“一接口一文件”模式
Single(MockRule),
/// 对应“一文件多接口”模式
Multiple(Vec<MockRule>),
}
impl MockSource {
/// 将不同的解析模式统一转化为列表,供 Loader 构建索引
pub fn flatten(self) -> Vec<MockRule> {
match self {
Self::Single(rule) => vec![rule],
Self::Multiple(rules) => rules,
}
}
}
/// 核心 Mock 规则定义
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
pub struct MockRule {
pub id: String,
pub request: RequestMatcher,
pub response: MockResponse,
pub settings: Option<MockSettings>,
}
/// 请求匹配条件
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
pub struct RequestMatcher {
pub method: String,
pub path: String,
/// 选填:只有请求包含这些参数时才匹配
pub query_params: Option<HashMap<String, String>>,
/// 选填:只有请求包含这些 Header 时才匹配
pub headers: Option<HashMap<String, String>>,
}
/// 响应内容定义
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
pub struct MockResponse {
pub status: u16,
pub headers: Option<HashMap<String, String>>,
/// 统一字段:支持 Inline 文本或 file:// 前缀的路径
pub body: String,
}
/// 模拟器行为设置
#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)]
pub struct MockSettings {
/// 模拟网络延迟(毫秒)
pub delay_ms: Option<u64>,
}
impl MockResponse {
/// 辅助方法:判断是否为文件协议
pub fn is_file_protocol(&self) -> bool {
self.body.starts_with("file://")
}
/// 获取去掉协议前缀后的纯物理路径
pub fn get_file_path(&self) -> Option<&str> {
if self.is_file_protocol() {
Some(&self.body[7..]) // 截取 "file://" 之后的内容
} else {
None
}
}
}

3
src/lib.rs Normal file
View File

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

58
src/loader.rs Normal file
View File

@@ -0,0 +1,58 @@
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir; // 需在 Cargo.toml 添加 walkdir 依赖
use crate::config::{MockRule, MockSource}; // 假设 config.rs 中定义了这两个类型
pub struct MockLoader;
impl MockLoader {
/// 递归扫描指定目录并构建索引表
pub fn load_all_from_dir(dir: &Path) -> HashMap<String, Vec<MockRule>> {
let mut index: HashMap<String, Vec<MockRule>> = HashMap::new();
// 1. 使用 walkdir 递归遍历目录,不限层级
for entry in WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "yaml" || ext == "yml"))
{
if let Some(rules) = Self::parse_yaml_file(entry.path()) {
for rule in rules {
// 2. 提取路径首段作为索引 Key
let key = Self::extract_first_segment(&rule.request.path);
// 3. 将规则插入到对应的索引桶中
index.entry(key).or_insert_with(Vec::new).push(rule);
}
}
}
println!("Successfully loaded {} segments from {:?}", index.len(), dir);
index
}
/// 解析单个 YAML 文件,支持单接口和多接口模式
fn parse_yaml_file(path: &Path) -> Option<Vec<MockRule>> {
let content = fs::read_to_string(path).ok()?;
// 利用 serde_yaml 的反序列化能力处理 MockSource 枚举
match serde_yaml::from_str::<MockSource>(&content) {
Ok(source) => Some(source.flatten()), // 统一打平为 Vec<MockRule>
Err(e) => {
eprintln!("Failed to parse YAML at {:?}: {}", path, e);
None
}
}
}
/// 提取路径的第一级作为 Key例如 "/api/v1/login" -> "api"
fn extract_first_segment(path: &str) -> String {
path.trim_start_matches('/')
.split('/')
.next()
.unwrap_or("root") // 如果是根路径 "/",则归类到 "root"
.to_string()
}
}

View File

@@ -1,3 +1,9 @@
fn main() {
println!("Hello, mock-server!");
}
// 使用项目名(下划线形式)引用 lib 中的内容
use mock_server::loader::MockLoader;
#[tokio::main]
async fn main() {
// 你的启动逻辑...
let index = MockLoader::load_all_from_dir(std::path::Path::new("./mocks"));
println!("服务启动中...");
}