fix: 修复 YAML 块语法 body 匹配失败问题
- normalize_yaml_body 函数在解析 JSON 前添加 trim() 处理,解决 YAML `|` 和 `>` 语法产生的前导空格问题 - 修复 multiple_login.yaml 中 response body 格式错误(YAML 对象改为 JSON 字符串) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +0,0 @@
|
|||||||
id: "auth_login_001"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/auth/login"
|
|
||||||
# 必须包含此 Header 才会匹配
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "111"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"username":"user",
|
|
||||||
"password":"123"
|
|
||||||
}
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
X-Mock-Engine: "Rust-Gemini-v1.2"
|
|
||||||
# 直接内联 JSON 字符串
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6" },
|
|
||||||
"msg": "success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 2000 # 模拟真实网络延迟
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
- id: "auth_login_out_001"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/auth/login_out"
|
|
||||||
# 必须包含此 Header 才会匹配
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "111"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body:
|
|
||||||
type: true
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
X-Mock-Engine: "Rust-Gemini-v1.2"
|
|
||||||
# 直接内联 JSON 字符串
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": "退出成功",
|
|
||||||
"msg": "success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 200 # 模拟真实网络延迟
|
|
||||||
- id: "auth_login_out_002"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/auth/login_out"
|
|
||||||
# 必须包含此 Header 才会匹配
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
Authorization: "111"
|
|
||||||
host: "127.0.0.1:8080"
|
|
||||||
body:
|
|
||||||
type: false
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
X-Mock-Engine: "Rust-Gemini-v1.2"
|
|
||||||
# 直接内联 JSON 字符串
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 1,
|
|
||||||
"data": "退出失败",
|
|
||||||
"msg": "success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 200 # 模拟真实网络延迟
|
|
||||||
118
mocks/v1/auth/multiple_login.yaml
Normal file
118
mocks/v1/auth/multiple_login.yaml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# 用户登录 - JSON 格式
|
||||||
|
- name: "user_login_002"
|
||||||
|
request:
|
||||||
|
path: "/v1/auth/login"
|
||||||
|
method: "POST"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6"
|
||||||
|
host: "127.0.0.1:8080"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"username": "user002",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6",
|
||||||
|
"userId": 10002,
|
||||||
|
"username": "user002",
|
||||||
|
"role": "administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
delay_ms: 2000 # 模拟真实网络延迟
|
||||||
|
|
||||||
|
- name: "user_login_003"
|
||||||
|
request:
|
||||||
|
path: "/v1/auth/login"
|
||||||
|
method: "POST"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6"
|
||||||
|
host: "127.0.0.1:8080"
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"username": "user003",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
body: >
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6",
|
||||||
|
"userId": 10003,
|
||||||
|
"username": "user003",
|
||||||
|
"role": "administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
delay_ms: 2000 # 模拟真实网络延迟
|
||||||
|
|
||||||
|
- name: "user_login_004"
|
||||||
|
request:
|
||||||
|
path: "/v1/auth/login"
|
||||||
|
method: "POST"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6"
|
||||||
|
host: "127.0.0.1:8080"
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"username": "user004",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6",
|
||||||
|
"userId": 10004,
|
||||||
|
"username": "user004",
|
||||||
|
"role": "administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: "user_login_005"
|
||||||
|
request:
|
||||||
|
path: "/v1/auth/login"
|
||||||
|
method: "POST"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6"
|
||||||
|
host: "127.0.0.1:8080"
|
||||||
|
body:
|
||||||
|
username: "user005"
|
||||||
|
password: "password123"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/json"
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "登录成功",
|
||||||
|
"data": {
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6",
|
||||||
|
"userId": 10005,
|
||||||
|
"username": "user005",
|
||||||
|
"role": "administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
id: "prod_export_pdf"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/products/report"
|
|
||||||
body: '{"username":"user","password":"123"}'
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/pdf"
|
|
||||||
Content-Disposition: "attachment; filename=report.pdf"
|
|
||||||
# 智能协议:引擎会自动识别前缀并异步读取磁盘文件
|
|
||||||
body: "file://./storage/reports/annual_2024.pdf"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# 使用 YAML 数组语法定义多个规则
|
|
||||||
- id: "sys_ping"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/ping"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: "pong"
|
|
||||||
|
|
||||||
- id: "sys_version"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/version"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: '{"version": "1.2.0-smart"}'
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
id: "upload_file"
|
|
||||||
request:
|
|
||||||
method: "POST"
|
|
||||||
path: "/api/v1/upload"
|
|
||||||
headers:
|
|
||||||
Content-Type: "multipart/form-data"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body: >
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"data": {
|
|
||||||
"filename": "example.txt",
|
|
||||||
"path": "storage/2024-01-15/example.txt",
|
|
||||||
"size": 1024,
|
|
||||||
"url": "/storage/2024-01-15/example.txt"
|
|
||||||
},
|
|
||||||
"msg": "upload success"
|
|
||||||
}
|
|
||||||
settings:
|
|
||||||
delay_ms: 100
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
id: "user_search_admin"
|
|
||||||
request:
|
|
||||||
method: "GET"
|
|
||||||
path: "/api/v1/users"
|
|
||||||
# 请求中必须包含 role=admin 且 status=active
|
|
||||||
query_params:
|
|
||||||
role: "admin"
|
|
||||||
status: "active"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
body: '{"users": [{"id": 1, "name": "SuperAdmin"}]}'
|
|
||||||
173
src/router.rs
173
src/router.rs
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::config::MockRule;
|
use crate::models::{MockRule, Payload};
|
||||||
|
|
||||||
pub struct MockRouter {
|
pub struct MockRouter {
|
||||||
// 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则
|
// 索引表:Key 是路径首段(如 "api"),Value 是该段下的所有 Mock 规则
|
||||||
@@ -18,7 +18,7 @@ impl MockRouter {
|
|||||||
path: &str,
|
path: &str,
|
||||||
queries: &HashMap<String, String>,
|
queries: &HashMap<String, String>,
|
||||||
headers: &HashMap<String, String>,
|
headers: &HashMap<String, String>,
|
||||||
incoming_body: &Option<serde_json::Value>, // 修改 1: 增加参数
|
payload: &Payload,
|
||||||
) -> Option<&MockRule> {
|
) -> Option<&MockRule> {
|
||||||
// 1. 提取请求路径的首段作为索引 Key
|
// 1. 提取请求路径的首段作为索引 Key
|
||||||
let key = self.extract_first_segment(path);
|
let key = self.extract_first_segment(path);
|
||||||
@@ -27,7 +27,7 @@ impl MockRouter {
|
|||||||
if let Some(rules) = self.index.get(&key) {
|
if let Some(rules) = self.index.get(&key) {
|
||||||
// 3. 在候选集中进行线性深度匹配
|
// 3. 在候选集中进行线性深度匹配
|
||||||
for rule in rules {
|
for rule in rules {
|
||||||
if self.is_match(rule, method, path, queries, headers,incoming_body) {
|
if self.is_match(rule, method, path, queries, headers, payload) {
|
||||||
return Some(rule);
|
return Some(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,18 +44,19 @@ impl MockRouter {
|
|||||||
path: &str,
|
path: &str,
|
||||||
queries: &HashMap<String, String>,
|
queries: &HashMap<String, String>,
|
||||||
headers: &HashMap<String, String>,
|
headers: &HashMap<String, String>,
|
||||||
incoming_body: &Option<serde_json::Value>, // 修改 3: 增加参数
|
payload: &Payload,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠)
|
// A. 基础校验:Method 和 Path 必须完全一致 (忽略末尾斜杠)
|
||||||
if rule.request.method.to_uppercase() != method.to_uppercase() {
|
if rule.request.method.to_uppercase() != method.to_uppercase() {
|
||||||
println!("DEBUG: [ID:{}] Method Mismatch: YAML={}, Req={}", rule.id, rule.request.method, method);
|
println!("DEBUG: [NAME:{}] Method Mismatch: YAML={}, Req={}", rule.name, rule.request.method, method);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
|
if rule.request.path.trim_end_matches('/') != path.trim_end_matches('/') {
|
||||||
println!("DEBUG: [ID:{}] Path Mismatch: YAML='{}', Req='{}'", rule.id, rule.request.path, path);
|
println!("DEBUG: [NAME:{}] Path Mismatch: YAML='{}', Req='{}'", rule.name, rule.request.path, path);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
println!("DEBUG: [ID:{}] Method and Path matched! Checking headers...", rule.id);
|
println!("DEBUG: [NAME:{}] Method and Path matched! Checking headers...", rule.name);
|
||||||
|
|
||||||
// B. Query 参数校验 (子集匹配原则)
|
// B. Query 参数校验 (子集匹配原则)
|
||||||
if let Some(ref required_queries) = rule.request.query_params {
|
if let Some(ref required_queries) = rule.request.query_params {
|
||||||
for (key, val) in required_queries {
|
for (key, val) in required_queries {
|
||||||
@@ -65,46 +66,71 @@ impl MockRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// C. Header 校验 (优化版)
|
// C. Header 校验 (大小写不敏感,Content-Type 使用前缀匹配)
|
||||||
if let Some(ref required_headers) = rule.request.headers {
|
if let Some(ref required_headers) = rule.request.headers {
|
||||||
for (key, val) in required_headers {
|
for (key, val) in required_headers {
|
||||||
println!("{}:{}",key.clone(), val.clone());
|
let key_lower = key.to_lowercase();
|
||||||
// 方案:将 Key 统一转为小写比较,并检查请求头是否“包含”期望的值
|
|
||||||
let matched = headers.iter().any(|(k, v)| {
|
let matched = headers.iter().any(|(k, v)| {
|
||||||
// k.to_lowercase() == key.to_lowercase() && v.contains(val)
|
if k.to_lowercase() != key_lower {
|
||||||
k.to_lowercase() == key.to_lowercase() && v==val
|
return false;
|
||||||
|
}
|
||||||
|
// Content-Type 使用前缀匹配(因为可能包含 boundary 等参数)
|
||||||
|
if key_lower == "content-type" {
|
||||||
|
v.to_lowercase().starts_with(&val.to_lowercase())
|
||||||
|
} else {
|
||||||
|
v == val
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if !matched {
|
if !matched {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// D. 智能 Body 全量比对逻辑
|
|
||||||
if let Some(ref required_val) = rule.request.body {
|
|
||||||
match incoming_body {
|
|
||||||
Some(actual_val) => {
|
|
||||||
// 实现你的想法:尝试将 YAML 中的 String 转换为 Object 再对比
|
|
||||||
let final_required = if let Some(s) = required_val.as_str() {
|
|
||||||
// 如果能解析成 JSON,就用解析后的对象,否则用原始字符串 Value
|
|
||||||
serde_json::from_str::<serde_json::Value>(s).unwrap_or_else(|_| required_val.clone())
|
|
||||||
} else {
|
|
||||||
required_val.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行全量相等比对
|
// D. Body 匹配(根据 Payload 类型智能比较)
|
||||||
if final_required != *actual_val {
|
if let Some(ref yaml_body) = rule.request.body {
|
||||||
println!("DEBUG: [ID:{}] Body Mismatch", rule.id);
|
return self.match_body(yaml_body, payload);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => return false, // YAML 要求有 Body 但请求为空
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Body 匹配逻辑
|
||||||
|
fn match_body(&self, yaml_body: &serde_json::Value, payload: &Payload) -> bool {
|
||||||
|
// 将 YAML body 规范化:如果是字符串,尝试解析为 JSON
|
||||||
|
let normalized_body = normalize_yaml_body(yaml_body);
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
Payload::Json(actual) => {
|
||||||
|
// JSON 对象比较
|
||||||
|
&normalized_body == actual
|
||||||
|
}
|
||||||
|
Payload::Xml(actual) => {
|
||||||
|
// XML 字符串比较(规范化后比较)
|
||||||
|
normalized_body.as_str()
|
||||||
|
.map(|expected| normalize_xml(expected) == normalize_xml(actual))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
Payload::Form(actual) => {
|
||||||
|
// Form 键值对比较(子集匹配)
|
||||||
|
compare_form_with_yaml(&normalized_body, actual)
|
||||||
|
}
|
||||||
|
Payload::Multipart(actual_data) => {
|
||||||
|
// Multipart 匹配:支持键值对或字段名列表
|
||||||
|
compare_multipart_with_yaml(&normalized_body, actual_data)
|
||||||
|
}
|
||||||
|
Payload::Text(actual) => {
|
||||||
|
// 字符串比较(去掉首尾空白)
|
||||||
|
normalized_body.as_str()
|
||||||
|
.map(|expected| expected.trim() == actual.trim())
|
||||||
|
.unwrap_or_else(|| normalized_body.to_string().trim() == actual.trim())
|
||||||
|
}
|
||||||
|
Payload::None => {
|
||||||
|
false // YAML 配置了 body,但请求没有 body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 与 Loader 保持一致的 Key 提取算法
|
/// 与 Loader 保持一致的 Key 提取算法
|
||||||
fn extract_first_segment(&self, path: &str) -> String {
|
fn extract_first_segment(&self, path: &str) -> String {
|
||||||
path.trim_start_matches('/')
|
path.trim_start_matches('/')
|
||||||
@@ -114,3 +140,86 @@ impl MockRouter {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 规范化 YAML body:如果是字符串,尝试解析为 JSON
|
||||||
|
fn normalize_yaml_body(yaml_body: &serde_json::Value) -> serde_json::Value {
|
||||||
|
if let Some(s) = yaml_body.as_str() {
|
||||||
|
// 先 trim 去掉前导/尾随空格,再尝试解析为 JSON
|
||||||
|
let trimmed = s.trim();
|
||||||
|
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(trimmed) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yaml_body.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 规范化 XML 字符串:去掉声明、多余空白、格式化为紧凑形式
|
||||||
|
fn normalize_xml(xml: &str) -> String {
|
||||||
|
let mut result = xml.to_string();
|
||||||
|
|
||||||
|
// 去掉 XML 声明
|
||||||
|
if let Some(pos) = result.find("?>") {
|
||||||
|
if result[..pos].contains("<?xml") {
|
||||||
|
result = result[pos + 2..].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉多余空白字符
|
||||||
|
result = result
|
||||||
|
.chars()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.chunks(1)
|
||||||
|
.map(|c| c[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// 分割成行,去掉每行首尾空白,过滤空行
|
||||||
|
result = result
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim())
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Form 比较:YAML 中的键值对必须是请求的子集
|
||||||
|
fn compare_form_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> 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().map(|s| s.to_string()).unwrap_or_else(|| yaml_val.to_string());
|
||||||
|
if actual.get(key) != Some(&expected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multipart 比较:对象和数组形式都只匹配字段名是否存在
|
||||||
|
fn compare_multipart_with_yaml(yaml_body: &serde_json::Value, actual: &HashMap<String, String>) -> bool {
|
||||||
|
// 方式 1:对象形式 - 只匹配键名是否存在(忽略值)
|
||||||
|
if let Some(yaml_map) = yaml_body.as_object() {
|
||||||
|
for key in yaml_map.keys() {
|
||||||
|
if !actual.contains_key(key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式 2:数组形式 - 只匹配字段名是否存在
|
||||||
|
if let Some(yaml_array) = yaml_body.as_array() {
|
||||||
|
for yaml_field in yaml_array {
|
||||||
|
let field_name = yaml_field.as_str().map(|s| s.to_string()).unwrap_or_else(|| yaml_field.to_string());
|
||||||
|
if !actual.contains_key(&field_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user