diff --git a/.claude/agents/wechat-article-summarizer.md b/.claude/agents/wechat-article-summarizer.md new file mode 100644 index 0000000..5feef58 --- /dev/null +++ b/.claude/agents/wechat-article-summarizer.md @@ -0,0 +1,103 @@ +--- +name: wechat-article-summarizer +description: "Use this agent when the user wants to summarize WeChat official account articles (公众号文章). This includes when the user shares a WeChat article link, asks for a summary of an article, or wants key points extracted from WeChat content.\\n\\nExamples:\\n\\n\\nContext: User shares a WeChat article link and wants a summary.\\nuser: \"帮我总结一下这篇文章:https://mp.weixin.qq.com/s/xxxxx\"\\nassistant: \"我来使用公众号文章总结代理来帮你总结这篇文章。\"\\n\\nSince the user wants to summarize a WeChat article, use the Agent tool to launch the wechat-article-summarizer agent.\\n\\n\\n\\n\\nContext: User has pasted WeChat article content and wants key points.\\nuser: \"这篇文章讲了什么?[粘贴了公众号文章内容]\"\\nassistant: \"我来使用公众号文章总结代理帮你提取这篇文章的要点。\"\\n\\nThe user has provided WeChat article content and wants to understand the main points. Use the Agent tool to launch the wechat-article-summarizer agent.\\n\\n\\n\\n\\nContext: User wants a quick overview of a shared WeChat article.\\nuser: \"这个公众号文章太长了,帮我看看主要说什么\"\\nassistant: \"我来用公众号文章总结代理帮你快速了解文章主旨。\"\\n\\nThe user finds the article too long and wants a quick overview. Use the Agent tool to launch the wechat-article-summarizer agent.\\n\\n" +model: sonnet +color: blue +memory: project +--- + +你是一位专业的公众号文章总结专家,擅长快速阅读和提炼中文文章的核心内容。你具备优秀的阅读理解能力和信息提取技巧,能够准确把握文章主旨并生成结构清晰的摘要并将总结结果生成rules。 + +## 核心职责 + +你的任务是为用户提供高质量的公众号文章总结,帮助他们快速理解文章内容并生成rules。 + +## 工作流程 + +1. **获取文章内容** + - 如果用户提供链接,使用可用的工具获取文章内容 + - 如果用户直接粘贴内容,直接处理该内容 + +2. **分析文章结构** + - 识别文章标题和主题 + - 找出核心论点和关键信息 + - 标记重要的数据、案例或引用 + - 注意文章的写作目的(科普、观点、新闻等) + +3. **生成摘要** + 输出格式如下: + + ### 📌 核心主旨 + [一句话概括文章主题] + + ### 📝 内容概要 + [100-200字的文章概述] + + ### 🔑 关键要点 + - 要点1 + - 要点2 + - 要点3 + - ...(通常3-6个要点) + + ### 💡 金句摘录 + > [文章中的精彩语句,如有] + + ### ⚖️ 个人观点(可选) + [如果文章有争议性或值得讨论的观点,简要说明] + +## 质量标准 + +- **准确性**:摘要必须忠实于原文,不能曲解或添加原文没有的信息 +- **简洁性**:去除冗余信息,保留核心内容 +- **完整性**:涵盖文章的主要观点和重要细节 +- **可读性**:使用清晰的语言,逻辑结构分明 + +## 注意事项 + +- 如果文章内容无法获取,明确告知用户并说明原因 +- 对于长文章,优先提取核心观点,次要内容可以略过 +- 如果文章包含专业术语,适当提供简单解释 +- 保持客观中立的立场,不在摘要中加入个人偏见 +- 使用中文进行总结 +- 在markdown文件中禁止使用emoji(根据用户配置要求,在最终输出时移除所有emoji) + +## 处理特殊情况 + +- **付费文章**:告知用户无法访问付费内容 +- **已删除文章**:说明文章可能已被删除 +- **图片为主的内容**:说明文章以图片为主,总结可识别的文字部分 + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `D:\CNWei\CNW\Rust\mock-server\.claude\agent-memory\wechat-article-summarizer\`. This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence). Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- When the user corrects you on something you stated from memory, you MUST update or remove the incorrect entry. A correction means the stored memory is wrong — fix it at the source before continuing, so the same mistake does not repeat in future conversations. +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/.claude/commands/git-branch.md b/.claude/commands/git-branch.md new file mode 100644 index 0000000..d934a6a --- /dev/null +++ b/.claude/commands/git-branch.md @@ -0,0 +1,20 @@ +# Git 分支管理 + +读取 `.claude/rules/git-branch-specification.md` 规范,帮助用户: + +1. **生成合规分支名** - 根据用户输入的项目名、版本号,自动生成符合规范的分支名 +2. **校验分支名** - 检查当前分支名是否符合规范 +3. **创建分支** - 按规范创建新分支 + +## 使用示例 + +- `/git-branch new feature 项目名 v1.0.0` - 创建功能分支 +- `/git-branch new hotfix 项目名 v1.0.1` - 创建热修复分支 +- `/git-branch check` - 校验当前分支名 +- `/git-branch help` - 显示命名规范 + +## 执行步骤 + +1. 读取规范文件 `.claude/rules/git-branch-specification.md` +2. 根据用户指令执行对应操作 +3. 生成分支名时使用当天日期(格式:YYYYMMDD) diff --git a/.claude/rules/git-branch-specification.md b/.claude/rules/git-branch-specification.md new file mode 100644 index 0000000..620452e --- /dev/null +++ b/.claude/rules/git-branch-specification.md @@ -0,0 +1,75 @@ +# Git分支管理规范 + +## 分支类型 + +| 类型 | 分支名称 | 用途 | 特点 | +|------|----------|------|------| +| 主分支 | master | 正式版本代码归档 | 受保护,仅运维可合并 | +| 主分支 | develop | 日常开发主分支 | 团队开发基准 | +| 主分支 | doc | 文档、SQL脚本、配置 | 文档管理 | +| 辅助分支 | feature | 功能开发分支 | 临时性,合并后删除 | +| 辅助分支 | hotfix | Bug紧急修复 | 临时性,合并后删除 | + +## 分支命名规则 + +### 功能分支 +``` +feature-{项目名}-{版本号}-SNAPSHOT-{日期} +``` +示例:`feature-javadog-v2.1.1-SNAPSHOT-20240703` + +### 个人分支 +``` +{功能分支}-{开发者姓名} +``` +示例:`feature-javadog-v2.1.1-SNAPSHOT-20240703-zhangsan` + +### 预生产分支 +``` +feature-{项目名}-{版本号}-{日期} +``` +示例:`feature-javadog-v2.1.1-20240703`(去除SNAPSHOT标识) + +### 热修复分支 +``` +hotfix-{项目名}-{版本号}-{日期} +``` +示例:`hotfix-javadog-v2.1.2-20240705` + +## 分支权限规则 + +1. master/develop/doc 分支受保护,仅运维可合并 +2. 辅助分支为临时分支,合并后询问开发人员是否需要删除 + +## 开发流程规则 + +### 功能开发流程(五阶段) + +| 阶段 | 操作 | 核心目的 | +|------|------|----------| +| 开发前 | 从develop拉取功能分支 | 保持团队起始点一致 | +| 开发中 | 组员从功能分支拉取个人临时分支 | 保证开发灵活性 | +| 提测中 | 个人分支合并到功能分支 | 流水线打包提测 | +| 预生产 | 从功能分支拉取预生产分支(去SNAPSHOT标识) | 环境验证解耦 | +| 上线 | 蓝绿部署,分支合并 | 无缝切换,稳定上线 | + +### 热修复流程 + +1. 从 master 拉取 hotfix 分支 +2. 修复 Bug 后合并回 master 和 develop +3. 合并后删除 hotfix 分支 + +## 蓝绿部署策略 + +- 定义:同时运行两个生产环境,通过切换实现无缝发布 +- 优势:新版本测试期间不影响线上环境 +- 流程:蓝线发布新版本 -> 验证通过 -> 蓝绿切换 -> 负载均衡 + +## 命名示例汇总 + +| 分支类型 | 命名格式 | 示例 | +|---------|---------|------| +| 功能分支 | `feature-{项目}-{版本}-SNAPSHOT-{日期}` | `feature-javadog-v2.1.1-SNAPSHOT-20240703` | +| 个人分支 | `{功能分支}-{姓名}` | `feature-javadog-v2.1.1-SNAPSHOT-20240703-zhangsan` | +| 预生产分支 | `feature-{项目}-{版本}-{日期}` | `feature-javadog-v2.1.1-20240703` | +| 热修复分支 | `hotfix-{项目}-{版本}-{日期}` | `hotfix-javadog-v2.1.2-20240705` | diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ea1c057..da7756b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,8 @@ "permissions": { "allow": [ "Bash(find:*)", - "Bash(cargo search:*)" + "Bash(cargo search:*)", + "WebSearch" ] } } diff --git a/Cargo.lock b/Cargo.lock index 607bd6e..939f909 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,6 +412,7 @@ dependencies = [ "tracing", "tracing-subscriber", "walkdir", + "winres", ] [[package]] @@ -772,6 +773,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tower" version = "0.5.2" @@ -999,6 +1009,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winres" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" +dependencies = [ + "toml", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index f831bc6..9d5c71b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,7 @@ notify-debouncer-mini = "0.6.0" #pathdiff = "0.2.3" [dev-dependencies] -tempfile = "3.24.0" \ No newline at end of file +tempfile = "3.24.0" + +[build-dependencies] +winres = "0.1" \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..be57857 --- /dev/null +++ b/build.rs @@ -0,0 +1,29 @@ +fn main() { + // 仅在 Windows 平台上编译资源 + if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" { + let mut res = winres::WindowsResource::new(); + + // 1. 设置图标路径 (请确保 icons 文件夹下有 icon.ico) + res.set_icon("icons/icon.ico"); + + // 2. 自动同步 Cargo.toml 中的元数据 + // 使用 env! 宏在编译阶段获取 package 信息 + res.set("FileDescription", env!("CARGO_PKG_DESCRIPTION")); + res.set("ProductName", "Mock Server"); + res.set("ProductVersion", env!("CARGO_PKG_VERSION")); + res.set("FileVersion", env!("CARGO_PKG_VERSION")); + res.set("InternalName", &format!("{}.exe", env!("CARGO_PKG_NAME"))); + + // 3. 设置版权信息 + res.set("LegalCopyright", "Copyright © 2026 Ways"); + + // 4. 设置语言为中文 (中国) + res.set_language(0x0804); + + // 执行编译,如果失败则打印错误并停止 + if let Err(e) = res.compile() { + eprintln!("资源编译失败: {}", e); + std::process::exit(1); + } + } +} \ No newline at end of file diff --git a/icons/128x128.png b/icons/128x128.png new file mode 100644 index 0000000..7ff4526 Binary files /dev/null and b/icons/128x128.png differ diff --git a/icons/128x128@2x.png b/icons/128x128@2x.png new file mode 100644 index 0000000..8e59eb7 Binary files /dev/null and b/icons/128x128@2x.png differ diff --git a/icons/32x32.png b/icons/32x32.png new file mode 100644 index 0000000..e20cfc6 Binary files /dev/null and b/icons/32x32.png differ diff --git a/icons/64x64.png b/icons/64x64.png new file mode 100644 index 0000000..1dcfef7 Binary files /dev/null and b/icons/64x64.png differ diff --git a/icons/Square107x107Logo.png b/icons/Square107x107Logo.png new file mode 100644 index 0000000..1fae436 Binary files /dev/null and b/icons/Square107x107Logo.png differ diff --git a/icons/Square142x142Logo.png b/icons/Square142x142Logo.png new file mode 100644 index 0000000..dd13a22 Binary files /dev/null and b/icons/Square142x142Logo.png differ diff --git a/icons/Square150x150Logo.png b/icons/Square150x150Logo.png new file mode 100644 index 0000000..521ab74 Binary files /dev/null and b/icons/Square150x150Logo.png differ diff --git a/icons/Square284x284Logo.png b/icons/Square284x284Logo.png new file mode 100644 index 0000000..f20a4f9 Binary files /dev/null and b/icons/Square284x284Logo.png differ diff --git a/icons/Square30x30Logo.png b/icons/Square30x30Logo.png new file mode 100644 index 0000000..fe400c6 Binary files /dev/null and b/icons/Square30x30Logo.png differ diff --git a/icons/Square310x310Logo.png b/icons/Square310x310Logo.png new file mode 100644 index 0000000..b3141e0 Binary files /dev/null and b/icons/Square310x310Logo.png differ diff --git a/icons/Square44x44Logo.png b/icons/Square44x44Logo.png new file mode 100644 index 0000000..38f4cdf Binary files /dev/null and b/icons/Square44x44Logo.png differ diff --git a/icons/Square71x71Logo.png b/icons/Square71x71Logo.png new file mode 100644 index 0000000..7047da6 Binary files /dev/null and b/icons/Square71x71Logo.png differ diff --git a/icons/Square89x89Logo.png b/icons/Square89x89Logo.png new file mode 100644 index 0000000..f7dadcb Binary files /dev/null and b/icons/Square89x89Logo.png differ diff --git a/icons/StoreLogo.png b/icons/StoreLogo.png new file mode 100644 index 0000000..527fcd3 Binary files /dev/null and b/icons/StoreLogo.png differ diff --git a/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/icons/android/mipmap-hdpi/ic_launcher.png b/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..3090c6b Binary files /dev/null and b/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8b1796a Binary files /dev/null and b/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/icons/android/mipmap-hdpi/ic_launcher_round.png b/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..402bd00 Binary files /dev/null and b/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/icons/android/mipmap-mdpi/ic_launcher.png b/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..954fcbb Binary files /dev/null and b/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c268ca5 Binary files /dev/null and b/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/icons/android/mipmap-mdpi/ic_launcher_round.png b/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..de326fc Binary files /dev/null and b/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/icons/android/mipmap-xhdpi/ic_launcher.png b/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..7b38a48 Binary files /dev/null and b/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a47469e Binary files /dev/null and b/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/icons/android/mipmap-xhdpi/ic_launcher_round.png b/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..314548e Binary files /dev/null and b/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/icons/android/mipmap-xxhdpi/ic_launcher.png b/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..5c0ca48 Binary files /dev/null and b/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..12db2fe Binary files /dev/null and b/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..11e16a9 Binary files /dev/null and b/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/icons/android/mipmap-xxxhdpi/ic_launcher.png b/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..d209722 Binary files /dev/null and b/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..73512b1 Binary files /dev/null and b/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8b78c7e Binary files /dev/null and b/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/icons/android/values/ic_launcher_background.xml b/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/icons/icon.icns b/icons/icon.icns new file mode 100644 index 0000000..66157b9 Binary files /dev/null and b/icons/icon.icns differ diff --git a/icons/icon.ico b/icons/icon.ico new file mode 100644 index 0000000..24518e9 Binary files /dev/null and b/icons/icon.ico differ diff --git a/icons/icon.png b/icons/icon.png new file mode 100644 index 0000000..9044e88 Binary files /dev/null and b/icons/icon.png differ diff --git a/icons/ios/AppIcon-20x20@1x.png b/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..1624452 Binary files /dev/null and b/icons/ios/AppIcon-20x20@1x.png differ diff --git a/icons/ios/AppIcon-20x20@2x-1.png b/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..06f7063 Binary files /dev/null and b/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/icons/ios/AppIcon-20x20@2x.png b/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..06f7063 Binary files /dev/null and b/icons/ios/AppIcon-20x20@2x.png differ diff --git a/icons/ios/AppIcon-20x20@3x.png b/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..6cfa902 Binary files /dev/null and b/icons/ios/AppIcon-20x20@3x.png differ diff --git a/icons/ios/AppIcon-29x29@1x.png b/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..84a0153 Binary files /dev/null and b/icons/ios/AppIcon-29x29@1x.png differ diff --git a/icons/ios/AppIcon-29x29@2x-1.png b/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..c9361a1 Binary files /dev/null and b/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/icons/ios/AppIcon-29x29@2x.png b/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..c9361a1 Binary files /dev/null and b/icons/ios/AppIcon-29x29@2x.png differ diff --git a/icons/ios/AppIcon-29x29@3x.png b/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..2edb20f Binary files /dev/null and b/icons/ios/AppIcon-29x29@3x.png differ diff --git a/icons/ios/AppIcon-40x40@1x.png b/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..06f7063 Binary files /dev/null and b/icons/ios/AppIcon-40x40@1x.png differ diff --git a/icons/ios/AppIcon-40x40@2x-1.png b/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..eb14789 Binary files /dev/null and b/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/icons/ios/AppIcon-40x40@2x.png b/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..eb14789 Binary files /dev/null and b/icons/ios/AppIcon-40x40@2x.png differ diff --git a/icons/ios/AppIcon-40x40@3x.png b/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..35d25bc Binary files /dev/null and b/icons/ios/AppIcon-40x40@3x.png differ diff --git a/icons/ios/AppIcon-512@2x.png b/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..85ae0ae Binary files /dev/null and b/icons/ios/AppIcon-512@2x.png differ diff --git a/icons/ios/AppIcon-60x60@2x.png b/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..35d25bc Binary files /dev/null and b/icons/ios/AppIcon-60x60@2x.png differ diff --git a/icons/ios/AppIcon-60x60@3x.png b/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..dbf7d17 Binary files /dev/null and b/icons/ios/AppIcon-60x60@3x.png differ diff --git a/icons/ios/AppIcon-76x76@1x.png b/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..b7b9912 Binary files /dev/null and b/icons/ios/AppIcon-76x76@1x.png differ diff --git a/icons/ios/AppIcon-76x76@2x.png b/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..a265c4d Binary files /dev/null and b/icons/ios/AppIcon-76x76@2x.png differ diff --git a/icons/ios/AppIcon-83.5x83.5@2x.png b/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..3942831 Binary files /dev/null and b/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..f984916 --- /dev/null +++ b/plan.md @@ -0,0 +1,239 @@ +# Body 匹配优化:基于 Content-Type 的智能解析 + +## Context + +**问题**: 当前只支持 JSON body 匹配,非 JSON 请求无法正确匹配。 + +**需求**: 根据配置和请求的 Content-Type 智能选择解析方式,支持 JSON、XML、Form、Text 等类型。 + +--- + +## 核心设计 + +### Body 解析规则(优先级) + +| 优先级 | YAML Content-Type | 请求 Content-Type | 解析方式 | +|--------|-------------------|-------------------|----------| +| 1 | 有 | 任意 | 按 **YAML** 的类型解析 | +| 2 | 无 | 有 | 按 **请求**的 Content-Type 解析 | +| 3 | 无 | 无 | **字符串**比较 | + +### 支持的 Content-Type + +| Content-Type | 解析结果 | +|--------------|----------| +| `application/json` | `ParsedBody::Json(Value)` | +| `application/xml`, `text/xml` | `ParsedBody::Xml(String)` | +| `application/x-www-form-urlencoded` | `ParsedBody::Form(HashMap)` | +| `multipart/form-data` | `ParsedBody::Multipart(Vec)` | +| `text/plain` 或其他 | `ParsedBody::Text(String)` | + +--- + +## 实现方案 + +### Step 1: 新增数据结构 (model.rs) + +```rust +/// 解析后的请求 Body +#[derive(Debug, Clone)] +pub enum ParsedBody { + Json(serde_json::Value), + Xml(String), + Form(HashMap), + Multipart(Vec), // 字段名列表 + Text(String), + None, +} + +impl ParsedBody { + /// 转换为字符串(用于兜底比较) + pub fn to_compare_string(&self) -> String { + match self { + ParsedBody::Json(v) => v.to_string(), + ParsedBody::Xml(s) | ParsedBody::Text(s) => s.clone(), + ParsedBody::Form(map) => { + let mut pairs: Vec<_> = map.iter().collect(); + pairs.sort_by_key(|(k, _)| *k); + pairs.iter().map(|(k, v)| format!("{}={}", k, v)).join("&") + } + ParsedBody::Multipart(fields) => fields.join(","), + ParsedBody::None => String::new(), + } + } +} +``` + +### Step 2: 解析函数 (handler.rs) + +```rust +/// 提取 Content-Type(去掉参数部分) +fn extract_content_type(headers: &HashMap) -> Option { + headers.iter() + .find(|(k, _)| k.to_lowercase() == "content-type") + .map(|(_, v)| v.split(';').next().unwrap_or(v).trim().to_lowercase()) +} + +/// 根据 Content-Type 解析 Body +fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> ParsedBody { + if bytes.is_empty() { + return ParsedBody::None; + } + + match content_type { + Some(ct) if ct.contains("application/json") => { + serde_json::from_slice(bytes) + .map(ParsedBody::Json) + .unwrap_or_else(|_| ParsedBody::Text(String::from_utf8_lossy(bytes).to_string())) + } + Some(ct) if ct.contains("xml") => { + ParsedBody::Xml(String::from_utf8_lossy(bytes).to_string()) + } + Some(ct) if ct.contains("form-urlencoded") => { + ParsedBody::Form(parse_urlencoded(bytes)) + } + Some(ct) if ct.contains("multipart/form-data") => { + ParsedBody::Multipart(extract_multipart_fields(bytes)) + } + _ => { + ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()) + } + } +} +``` + +### Step 3: 确定解析类型 (handler.rs) + +```rust +// 在 mock_handler 中: + +// 1. 提取请求的 Content-Type +let req_content_type = extract_content_type(&req_headers); + +// 2. 读取请求 body +let body_bytes = ...; +let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes); + +// 3. 匹配时传递 req_headers 和 parsed_body +// router 会根据 YAML 中是否配置了 Content-Type 来决定使用哪个类型 +``` + +### Step 4: 匹配逻辑 (router.rs) + +```rust +fn match_body( + &self, + rule: &MockRule, + parsed_body: &ParsedBody, + req_content_type: Option<&str>, +) -> bool { + let yaml_body = match &rule.request.body { + Some(b) => b, + None => return true, // YAML 没配置 body,跳过检查 + }; + + // 确定用于解析/比较的 Content-Type + let effective_content_type = rule.request.headers + .as_ref() + .and_then(|h| h.iter() + .find(|(k, _)| k.to_lowercase() == "content-type") + .map(|(_, v)| v.as_str())) + .or(req_content_type); // YAML 优先,没有则用请求的 + + match effective_content_type { + Some(ct) if ct.contains("application/json") => { + // JSON 比较 + match parsed_body { + ParsedBody::Json(actual) => yaml_body == actual, + _ => false, + } + } + Some(ct) if ct.contains("xml") => { + // XML 字符串比较 + match parsed_body { + ParsedBody::Xml(actual) => yaml_body.as_str() + .map(|expected| expected.trim() == actual.trim()) + .unwrap_or(false), + _ => false, + } + } + Some(ct) if ct.contains("form-urlencoded") => { + // Form 比较 + match parsed_body { + ParsedBody::Form(actual) => compare_form(yaml_body, actual), + _ => false, + } + } + _ => { + // 无 Content-Type 或其他类型:字符串比较 + let actual_str = parsed_body.to_compare_string(); + let expected_str = yaml_body.to_string(); + expected_str.trim() == actual_str.trim() + } + } +} +``` + +--- + +## 需要修改的文件 + +| 文件 | 改动 | +|------|------| +| `src/model.rs` | 新增 `ParsedBody` 枚举和 `to_compare_string()` 方法 | +| `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、辅助解析函数 | +| `src/router.rs` | 新增 `match_body()` 方法,调整 `is_match()` 调用 | +| `Cargo.toml` | 可能需要 `urlencoding` 依赖(form 解码) | + +--- + +## YAML 配置示例 + +```yaml +# JSON 匹配(指定 Content-Type) +- id: login-json + request: + method: POST + path: /api/login + headers: + Content-Type: application/json + body: + username: admin + +# XML 匹配 +- id: login-xml + request: + method: POST + path: /api/login + headers: + Content-Type: application/xml + body: "admin" + +# Form 匹配 +- id: login-form + request: + method: POST + path: /api/login + headers: + Content-Type: application/x-www-form-urlencoded + body: + username: admin + +# 字符串匹配(无 Content-Type) +- id: echo + request: + method: POST + path: /api/echo + body: "hello world" +``` + +--- + +## 验证 + +1. `cargo test` +2. 手动测试各类型: + - JSON 请求 + JSON 规则 → 匹配 + - XML 请求 + XML 规则 → 匹配 + - Form 请求 + Form 规则 → 匹配 + - 无 Content-Type → 字符串比较 diff --git a/plan2.md b/plan2.md new file mode 100644 index 0000000..b316617 --- /dev/null +++ b/plan2.md @@ -0,0 +1,375 @@ +# Body 匹配优化:基于 Content-Type 的智能解析 + +## Context + +**问题**: 当前只支持 JSON body 匹配,非 JSON 请求无法正确匹配。 + +**需求**: 根据请求的 Content-Type 智能解析 body,同时支持 header 匹配校验。 + +--- + +## 核心设计 + +### 关键原则 + +1. **Body 解析**:始终以【请求的 Content-Type】为准,因为这是数据的真实格式 +2. **Header 匹配**:Content-Type 当作普通 header 处理,写了就匹配,没写跳过 +3. **自动补充的 header**:Content-Length、Accept-Encoding 等不参与匹配(除非 YAML 显式配置) + +### 处理流程 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 请求处理流程 │ +├─────────────────────────────────────────────────────────────┤ +│ 1. 解析请求 body │ +│ └── 始终以【请求的 Content-Type】为准解析 │ +│ │ +│ 2. Header 匹配(包含 Content-Type) │ +│ └── YAML 写了就匹配,没写就跳过 │ +│ │ +│ 3. Body 匹配 │ +│ └── 根据匹配成功的 Content-Type 决定比较方式 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 匹配示例 + +| YAML Content-Type | 请求 Content-Type | Header 匹配 | Body 解析 | 结果 | +|-------------------|-------------------|-------------|-----------|------| +| `application/xml` | `application/json` | 失败 | - | 不匹配 | +| `application/json` | `application/json` | 成功 | JSON | 比较 body | +| 无 | `application/json` | 跳过 | JSON | 比较 body | +| 无 | `application/xml` | 跳过 | XML | 比较 body | +| 无 | 无 | 跳过 | 字符串 | 比较 body | + +--- + +## 实现方案 + +### Step 1: 新增数据结构 (model.rs) + +```rust +/// 解析后的请求 Body +#[derive(Debug, Clone)] +pub enum ParsedBody { + Json(serde_json::Value), + Xml(String), + Form(HashMap), + Multipart(Vec), // 字段名列表 + Text(String), + None, +} + +impl ParsedBody { + /// 转换为字符串(用于兜底比较) + pub fn to_compare_string(&self) -> String { + match self { + ParsedBody::Json(v) => v.to_string(), + ParsedBody::Xml(s) | ParsedBody::Text(s) => s.clone(), + ParsedBody::Form(map) => { + let mut pairs: Vec<_> = map.iter().collect(); + pairs.sort_by_key(|(k, _)| *k); + pairs.iter().map(|(k, v)| format!("{}={}", k, v)).join("&") + } + ParsedBody::Multipart(fields) => fields.join(","), + ParsedBody::None => String::new(), + } + } + + /// 获取对应的 Content-Type 名称 + pub fn content_type_name(&self) -> &'static str { + match self { + ParsedBody::Json(_) => "application/json", + ParsedBody::Xml(_) => "application/xml", + ParsedBody::Form(_) => "application/x-www-form-urlencoded", + ParsedBody::Multipart(_) => "multipart/form-data", + ParsedBody::Text(_) => "text/plain", + ParsedBody::None => "none", + } + } +} +``` + +### Step 2: Body 解析函数 (handler.rs) + +```rust +/// 提取请求的 Content-Type(去掉参数部分,如 boundary) +fn extract_content_type(headers: &HeaderMap) -> Option { + headers + .get(axum::http::header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .map(|s| s.split(';').next().unwrap_or(s).trim().to_lowercase()) +} + +/// 根据 Content-Type 解析 Body(始终以请求的 Content-Type 为准) +fn parse_body(content_type: Option<&str>, bytes: &[u8]) -> ParsedBody { + if bytes.is_empty() { + return ParsedBody::None; + } + + match content_type { + Some(ct) if ct.contains("application/json") => { + serde_json::from_slice(bytes) + .map(ParsedBody::Json) + .unwrap_or_else(|_| { + // JSON 解析失败,降级为文本 + ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()) + }) + } + Some(ct) if ct.contains("xml") => { + ParsedBody::Xml(String::from_utf8_lossy(bytes).to_string()) + } + Some(ct) if ct.contains("form-urlencoded") => { + ParsedBody::Form(parse_urlencoded(bytes)) + } + Some(ct) if ct.contains("multipart/form-data") => { + ParsedBody::Multipart(extract_multipart_fields(bytes)) + } + _ => { + ParsedBody::Text(String::from_utf8_lossy(bytes).to_string()) + } + } +} + +/// 解析 urlencoded 格式 +fn parse_urlencoded(bytes: &[u8]) -> HashMap { + let body = String::from_utf8_lossy(bytes); + let mut map = HashMap::new(); + for pair in body.split('&') { + if let Some((key, value)) = pair.split_once('=') { + // URL 解码 + let decoded_key = urlencoding_decode(key); + let decoded_value = urlencoding_decode(value); + map.insert(decoded_key, decoded_value); + } + } + map +} +``` + +### Step 3: 修改 handler.rs 主函数 + +```rust +pub async fn mock_handler( + State(state): State>, + method: Method, + headers: HeaderMap, + Query(params): Query>, + req: Request, +) -> impl IntoResponse { + let path = req.uri().path().to_string(); + let method_str = method.as_str().to_string(); + + // 1. 提取请求的 Content-Type + let req_content_type = extract_content_type(&headers); + + // 2. 读取请求 body + 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(); + } + }; + + // 3. 根据【请求的 Content-Type】解析 body + let parsed_body = parse_body(req_content_type.as_deref(), &body_bytes); + + // 4. 转换 headers 为 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()); + } + } + + // 5. 执行匹配 + let maybe_rule = { + let router = state.router.read().expect("Failed to acquire read lock"); + router.match_rule(&method_str, &path, ¶ms, &req_headers, &parsed_body).cloned() + }; + + // 6. 构建响应(与现有逻辑相同) + // ... +} +``` + +### Step 4: 修改 router.rs 匹配逻辑 + +```rust +/// 核心匹配函数 +pub fn match_rule( + &self, + method: &str, + path: &str, + queries: &HashMap, + headers: &HashMap, + parsed_body: &ParsedBody, // 改为 ParsedBody +) -> Option<&MockRule> { + let key = self.extract_first_segment(path); + + if let Some(rules) = self.index.get(&key) { + for rule in rules { + if self.is_match(rule, method, path, queries, headers, parsed_body) { + return Some(rule); + } + } + } + None +} + +fn is_match( + &self, + rule: &MockRule, + method: &str, + path: &str, + queries: &HashMap, + headers: &HashMap, + parsed_body: &ParsedBody, +) -> bool { + // A. Method 匹配 + if rule.request.method.to_uppercase() != method.to_uppercase() { + return false; + } + + // B. Path 匹配 + if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') { + return false; + } + + // C. Query 匹配(子集匹配) + if let Some(ref required_queries) = rule.request.query_params { + for (key, val) in required_queries { + if queries.get(key) != Some(val) { + return false; + } + } + } + + // D. Header 匹配(包含 Content-Type) + // YAML 写了就匹配,没写就跳过 + if let Some(ref required_headers) = rule.request.headers { + for (key, val) in required_headers { + let matched = headers.iter().any(|(k, v)| { + k.to_lowercase() == key.to_lowercase() && v == val + }); + if !matched { + return false; // Header 不匹配,包括 Content-Type + } + } + } + + // E. Body 匹配 + // YAML 写了 body 才匹配,没写跳过 + if let Some(ref yaml_body) = rule.request.body { + return self.match_body(yaml_body, parsed_body); + } + + true +} + +/// Body 匹配逻辑 +fn match_body(&self, yaml_body: &serde_json::Value, parsed_body: &ParsedBody) -> bool { + match parsed_body { + ParsedBody::Json(actual) => { + // JSON 对象比较 + yaml_body == actual + } + ParsedBody::Xml(actual) => { + // XML 字符串比较 + yaml_body.as_str() + .map(|expected| expected.trim() == actual.trim()) + .unwrap_or(false) + } + ParsedBody::Form(actual) => { + // Form 键值对比较(子集匹配) + compare_form_with_yaml(yaml_body, actual) + } + ParsedBody::Multipart(actual_fields) => { + // Multipart 字段名比较 + compare_multipart_with_yaml(yaml_body, actual_fields) + } + ParsedBody::Text(actual) => { + // 字符串比较 + yaml_body.as_str() + .map(|expected| expected.trim() == actual.trim()) + .unwrap_or_else(|| yaml_body.to_string().trim() == actual.trim()) + } + ParsedBody::None => { + false // YAML 配置了 body,但请求没有 body + } + } +} + +/// Form 比较:YAML 中的键值对必须是请求的子集 +fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap) -> bool { + let yaml_map = match yaml_body.as_object() { + Some(obj) => obj, + None => return false, + }; + + for (key, yaml_val) in yaml_map { + let expected = yaml_val.as_str().unwrap_or(&yaml_val.to_string()); + if actual.get(key) != Some(&expected.to_string()) { + return false; + } + } + true +} +``` + +--- + +## 需要修改的文件 + +| 文件 | 改动 | +|------|------| +| `src/model.rs` | 新增 `ParsedBody` 枚举和相关方法 | +| `src/handler.rs` | 新增 `extract_content_type()`、`parse_body()`、修改 `mock_handler()` | +| `src/router.rs` | 修改 `match_rule()` 和 `is_match()` 参数,新增 `match_body()` | +| `Cargo.toml` | 可能需要 `urlencoding` 依赖 | + +--- + +## YAML 配置示例 + +```yaml +# 严格匹配:要求 Content-Type + body +- id: login-strict + request: + method: POST + path: /api/login + headers: + Content-Type: application/json + body: + username: admin + password: "123456" + +# 宽松匹配:只匹配 method + path + body(不检查 Content-Type) +- id: login-loose + request: + method: POST + path: /api/login + body: + username: admin + +# 最宽松:只匹配 method + path +- id: any-body + request: + method: POST + path: /api/echo +``` + +--- + +## 验证 + +1. `cargo test` +2. 手动测试: + - YAML 配置 `Content-Type: application/json`,请求发送 JSON → 匹配 + - YAML 配置 `Content-Type: application/xml`,请求发送 JSON → Header 不匹配 + - YAML 无 Content-Type,请求发送 XML → Body 字符串比较 + - YAML 无 body 配置 → 跳过 body 检查 diff --git a/src/lib.rs b/src/lib.rs index e13f385..528920c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ // 声明模块并设为 pub,这样 tests/ 目录才能看到它们 -pub mod config; +pub mod model; pub mod loader; pub mod router; pub mod handler; \ No newline at end of file diff --git a/src/loader.rs b/src/loader.rs index 0a68dea..049ba3b 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; // 需在 Cargo.toml 添加 walkdir 依赖 +use std::path::{Path}; +use walkdir::WalkDir; -use crate::config::{MockRule, MockSource}; // 假设 config.rs 中定义了这两个类型 +use crate::model::{MockRule, MockSource}; // 假设 model 中定义了这两个类型 pub struct MockLoader; diff --git a/src/main.rs b/src/main.rs index 7f8fa92..01fe4cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,24 @@ use mock_server::loader::MockLoader; use mock_server::router::MockRouter; use mock_server::handler::{mock_handler, AppState}; +/// 打印启动 Banner +fn print_banner() { + let version = env!("CARGO_PKG_VERSION"); + // 蓝色 ANSI 转义码 + println!("\x1b[34m"); + println!(" ███╗ ███╗ ██████╗██████╗ ██████╗ "); + println!(" ████╗ ████║██╔════╝██╔══██╗██╔═══██╗"); + println!(" ██╔████╔██║██║ ██████╔╝██║ ██║"); + println!(" ██║╚██╔╝██║██║ ██╔══██╗██║ ██║"); + println!(" ██║ ╚═╝ ██║╚██████╗██║ ██║╚██████╔╝"); + println!(" ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ "); + println!("\x1b[0m"); // 重置颜色 + println!(" Mock Server v{}\n", version); +} + #[tokio::main] async fn main() { + print_banner(); tracing_subscriber::fmt::init(); let mocks_dir = Path::new("./mocks"); diff --git a/src/config.rs b/src/model.rs similarity index 89% rename from src/config.rs rename to src/model.rs index b2ce262..f748004 100644 --- a/src/config.rs +++ b/src/model.rs @@ -22,7 +22,7 @@ impl MockSource { } /// 核心 Mock 规则定义 -#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct MockRule { pub id: String, pub request: RequestMatcher, @@ -31,7 +31,7 @@ pub struct MockRule { } /// 请求匹配条件 -#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct RequestMatcher { pub method: String, pub path: String, @@ -44,7 +44,7 @@ pub struct RequestMatcher { } /// 响应内容定义 -#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct MockResponse { pub status: u16, pub headers: Option>, @@ -52,13 +52,6 @@ pub struct MockResponse { pub body: String, } -/// 模拟器行为设置 -#[derive(Debug, Deserialize,Serialize, Clone,PartialEq)] -pub struct MockSettings { - /// 模拟网络延迟(毫秒) - pub delay_ms: Option, -} - impl MockResponse { /// 辅助方法:判断是否为文件协议 pub fn is_file_protocol(&self) -> bool { @@ -74,3 +67,10 @@ impl MockResponse { } } } + +/// 模拟器行为设置 +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct MockSettings { + /// 模拟网络延迟(毫秒) + pub delay_ms: Option, +} diff --git a/src/router.rs b/src/router.rs index c68dbcd..0ff23c5 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use crate::config::MockRule; +use crate::model::MockRule; pub struct MockRouter { // 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则 diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 5355fae..4bb54c0 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -3,7 +3,7 @@ use std::io::Write; use tempfile::tempdir; // 确保 Cargo.toml 中有 serde_json use serde_json::json; -use mock_server::config::{MockRule, MockSource}; +use mock_server::model::{MockRule, MockSource}; use mock_server::loader::MockLoader; use mock_server::router::MockRouter; use std::collections::HashMap; diff --git a/tests/loader_test.rs b/tests/loader_test.rs index 64a0735..8c32b7b 100644 --- a/tests/loader_test.rs +++ b/tests/loader_test.rs @@ -2,7 +2,7 @@ use std::fs::{self, File}; use std::io::Write; use tempfile::tempdir; // 假设你的项目名在 Cargo.toml 中叫 mock_server -use mock_server::config::MockSource; +use mock_server::model::MockSource; use mock_server::loader::MockLoader; #[test]