schema-dsl 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -3
- package/README.md +149 -4
- package/STATUS.md +37 -3
- package/changelogs/v1.1.5.md +493 -0
- package/changelogs/v1.1.6.md +211 -0
- package/docs/error-handling.md +247 -2
- package/docs/runtime-locale-support.md +27 -0
- package/index.d.ts +77 -2
- package/lib/core/ErrorFormatter.js +6 -1
- package/lib/core/Locale.js +28 -22
- package/lib/core/MessageTemplate.js +30 -21
- package/lib/core/Validator.js +6 -1
- package/lib/errors/I18nError.js +38 -9
- package/lib/locales/en-US.js +15 -3
- package/lib/locales/es-ES.js +2 -0
- package/lib/locales/fr-FR.js +2 -0
- package/lib/locales/ja-JP.js +2 -0
- package/lib/locales/zh-CN.js +16 -3
- package/lib/validators/CustomKeywords.js +10 -2
- package/package.json +1 -1
- package/test-three-rounds-check.js +375 -0
package/lib/locales/en-US.js
CHANGED
|
@@ -26,10 +26,17 @@ module.exports = {
|
|
|
26
26
|
'error.conflict': 'Operation conflict: {{#reason}}',
|
|
27
27
|
|
|
28
28
|
// I18nError - Account related (v1.1.1)
|
|
29
|
-
|
|
29
|
+
// v1.1.5: Using object format for some examples
|
|
30
|
+
'account.notFound': {
|
|
31
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
32
|
+
message: 'Account not found'
|
|
33
|
+
},
|
|
30
34
|
'account.inactive': 'Account is inactive',
|
|
31
35
|
'account.banned': 'Account has been banned',
|
|
32
|
-
'account.insufficientBalance':
|
|
36
|
+
'account.insufficientBalance': {
|
|
37
|
+
code: 'INSUFFICIENT_BALANCE',
|
|
38
|
+
message: 'Insufficient balance, current: {{#balance}}, required: {{#required}}'
|
|
39
|
+
},
|
|
33
40
|
'account.insufficientCredits': 'Insufficient credits, current: {{#credits}}, required: {{#required}}',
|
|
34
41
|
|
|
35
42
|
// I18nError - User related (v1.1.1)
|
|
@@ -38,7 +45,10 @@ module.exports = {
|
|
|
38
45
|
'user.noPermission': 'No admin permission',
|
|
39
46
|
|
|
40
47
|
// I18nError - Order related (v1.1.1)
|
|
41
|
-
'order.notPaid':
|
|
48
|
+
'order.notPaid': {
|
|
49
|
+
code: 'ORDER_NOT_PAID',
|
|
50
|
+
message: 'Order not paid'
|
|
51
|
+
},
|
|
42
52
|
'order.paymentMissing': 'Payment information missing',
|
|
43
53
|
'order.addressMissing': 'Shipping address missing',
|
|
44
54
|
|
|
@@ -86,6 +96,8 @@ module.exports = {
|
|
|
86
96
|
// v1.0.2
|
|
87
97
|
'object.missing': '{{#label}} is missing required properties',
|
|
88
98
|
'object.schema': '{{#label}} contains additional properties',
|
|
99
|
+
// v1.1.6 - additionalProperties
|
|
100
|
+
'additionalProperties': '{{#label}} must NOT have additional properties: {{#key}}',
|
|
89
101
|
|
|
90
102
|
// Array
|
|
91
103
|
'array.base': '{{#label}} must be an array',
|
package/lib/locales/es-ES.js
CHANGED
|
@@ -44,6 +44,8 @@ module.exports = {
|
|
|
44
44
|
'object.min': '{{#label}} debe tener al menos {{#limit}} propiedades',
|
|
45
45
|
'object.max': '{{#label}} debe tener como máximo {{#limit}} propiedades',
|
|
46
46
|
'object.unknown': '{{#label}} contiene una propiedad desconocida: {{#key}}',
|
|
47
|
+
// v1.1.6 - additionalProperties
|
|
48
|
+
'additionalProperties': '{{#label}} NO debe tener propiedades adicionales: {{#key}}',
|
|
47
49
|
|
|
48
50
|
// Array
|
|
49
51
|
'array.base': '{{#label}} debe ser un array',
|
package/lib/locales/fr-FR.js
CHANGED
|
@@ -44,6 +44,8 @@ module.exports = {
|
|
|
44
44
|
'object.min': '{{#label}} doit avoir au moins {{#limit}} propriétés',
|
|
45
45
|
'object.max': '{{#label}} doit avoir au plus {{#limit}} propriétés',
|
|
46
46
|
'object.unknown': '{{#label}} contient une propriété inconnue : {{#key}}',
|
|
47
|
+
// v1.1.6 - additionalProperties
|
|
48
|
+
'additionalProperties': '{{#label}} NE doit PAS avoir de propriétés supplémentaires : {{#key}}',
|
|
47
49
|
|
|
48
50
|
// Array
|
|
49
51
|
'array.base': '{{#label}} doit être un tableau',
|
package/lib/locales/ja-JP.js
CHANGED
|
@@ -49,6 +49,8 @@ module.exports = {
|
|
|
49
49
|
'object.min': '{{#label}}は少なくとも{{#limit}}個のプロパティを持つ必要があります',
|
|
50
50
|
'object.max': '{{#label}}は最大{{#limit}}個のプロパティを持つことができます',
|
|
51
51
|
'object.unknown': '{{#label}}に未知のプロパティが含まれています: {{#key}}',
|
|
52
|
+
// v1.1.6 - additionalProperties
|
|
53
|
+
'additionalProperties': '{{#label}}に追加のプロパティを含めてはいけません: {{#key}}',
|
|
52
54
|
|
|
53
55
|
// Array
|
|
54
56
|
'array.base': '{{#label}}は配列である必要があります',
|
package/lib/locales/zh-CN.js
CHANGED
|
@@ -17,6 +17,7 @@ module.exports = {
|
|
|
17
17
|
'conditional.blocked': '账号已被封禁',
|
|
18
18
|
'conditional.notAllowed': '不允许注册',
|
|
19
19
|
|
|
20
|
+
// I18nError - 通用错误消息 (v1.1.1)
|
|
20
21
|
// I18nError - 通用错误消息 (v1.1.1)
|
|
21
22
|
'error.notFound': '找不到{{#resource}}',
|
|
22
23
|
'error.forbidden': '没有权限访问{{#resource}}',
|
|
@@ -26,10 +27,17 @@ module.exports = {
|
|
|
26
27
|
'error.conflict': '操作冲突: {{#reason}}',
|
|
27
28
|
|
|
28
29
|
// I18nError - 账户相关 (v1.1.1)
|
|
29
|
-
|
|
30
|
+
// v1.1.5: 部分使用对象格式示例
|
|
31
|
+
'account.notFound': {
|
|
32
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
33
|
+
message: '账户不存在'
|
|
34
|
+
},
|
|
30
35
|
'account.inactive': '账户未激活',
|
|
31
36
|
'account.banned': '账户已被封禁',
|
|
32
|
-
'account.insufficientBalance':
|
|
37
|
+
'account.insufficientBalance': {
|
|
38
|
+
code: 'INSUFFICIENT_BALANCE',
|
|
39
|
+
message: '余额不足,当前余额{{#balance}},需要{{#required}}'
|
|
40
|
+
},
|
|
33
41
|
'account.insufficientCredits': '积分不足,当前积分{{#credits}},需要{{#required}}',
|
|
34
42
|
|
|
35
43
|
// I18nError - 用户相关 (v1.1.1)
|
|
@@ -38,7 +46,10 @@ module.exports = {
|
|
|
38
46
|
'user.noPermission': '没有管理员权限',
|
|
39
47
|
|
|
40
48
|
// I18nError - 订单相关 (v1.1.1)
|
|
41
|
-
'order.notPaid':
|
|
49
|
+
'order.notPaid': {
|
|
50
|
+
code: 'ORDER_NOT_PAID',
|
|
51
|
+
message: '订单未支付'
|
|
52
|
+
},
|
|
42
53
|
'order.paymentMissing': '缺少支付信息',
|
|
43
54
|
'order.addressMissing': '缺少收货地址',
|
|
44
55
|
|
|
@@ -86,6 +97,8 @@ module.exports = {
|
|
|
86
97
|
// v1.0.2新增
|
|
87
98
|
'object.missing': '{{#label}}缺少必需属性',
|
|
88
99
|
'object.schema': '{{#label}}包含额外属性',
|
|
100
|
+
// v1.1.6新增 - additionalProperties
|
|
101
|
+
'additionalProperties': '{{#label}}不允许有额外属性: {{#key}}',
|
|
89
102
|
|
|
90
103
|
// Array
|
|
91
104
|
'array.base': '{{#label}}必须是数组类型',
|
|
@@ -90,12 +90,20 @@ class CustomKeywords {
|
|
|
90
90
|
// ajv 默认不支持同步验证中的异步操作
|
|
91
91
|
// 这里我们只能抛出错误提示
|
|
92
92
|
// 真正的异步支持需要 Validator.validateAsync
|
|
93
|
-
|
|
93
|
+
const messageConfig = Locale.getMessage('ASYNC_VALIDATION_NOT_SUPPORTED');
|
|
94
|
+
const errorMessage = typeof messageConfig === 'object' && messageConfig.message
|
|
95
|
+
? messageConfig.message
|
|
96
|
+
: messageConfig;
|
|
97
|
+
throw new Error(errorMessage);
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
// 处理返回值
|
|
97
101
|
if (result === false) {
|
|
98
|
-
|
|
102
|
+
const messageConfig = Locale.getMessage('CUSTOM_VALIDATION_FAILED');
|
|
103
|
+
const errorMessage = typeof messageConfig === 'object' && messageConfig.message
|
|
104
|
+
? messageConfig.message
|
|
105
|
+
: messageConfig;
|
|
106
|
+
validate.errors = [{ message: errorMessage }];
|
|
99
107
|
return false;
|
|
100
108
|
}
|
|
101
109
|
if (typeof result === 'string') {
|
package/package.json
CHANGED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三轮深度检查脚本
|
|
3
|
+
*
|
|
4
|
+
* 第一轮:验证所有ajv错误类型的参数映射
|
|
5
|
+
* 第二轮:边界情况和极端值测试
|
|
6
|
+
* 第三轮:并发和性能测试
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const Ajv = require('ajv');
|
|
10
|
+
const addFormats = require('ajv-formats');
|
|
11
|
+
const ErrorFormatter = require('./lib/core/ErrorFormatter');
|
|
12
|
+
|
|
13
|
+
console.log('🔍 开始三轮深度检查...\n');
|
|
14
|
+
console.log('='.repeat(80));
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// 第一轮:验证所有ajv错误类型的参数映射
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
console.log('\n【第一轮】验证所有ajv错误类型的参数映射\n');
|
|
21
|
+
|
|
22
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
23
|
+
addFormats(ajv);
|
|
24
|
+
|
|
25
|
+
const testCases = [
|
|
26
|
+
{
|
|
27
|
+
name: 'required',
|
|
28
|
+
schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
29
|
+
data: {},
|
|
30
|
+
expectedParams: ['missingProperty']
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'minLength',
|
|
34
|
+
schema: { type: 'string', minLength: 3 },
|
|
35
|
+
data: 'ab',
|
|
36
|
+
expectedParams: ['limit']
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'maxLength',
|
|
40
|
+
schema: { type: 'string', maxLength: 10 },
|
|
41
|
+
data: 'this is too long string',
|
|
42
|
+
expectedParams: ['limit']
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'minimum',
|
|
46
|
+
schema: { type: 'number', minimum: 10 },
|
|
47
|
+
data: 5,
|
|
48
|
+
expectedParams: ['limit', 'comparison']
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'maximum',
|
|
52
|
+
schema: { type: 'number', maximum: 100 },
|
|
53
|
+
data: 150,
|
|
54
|
+
expectedParams: ['limit', 'comparison']
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'enum',
|
|
58
|
+
schema: { type: 'string', enum: ['pro', 'basic', 'free'] },
|
|
59
|
+
data: 'premium',
|
|
60
|
+
expectedParams: ['allowedValues']
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'pattern',
|
|
64
|
+
schema: { type: 'string', pattern: '^[a-z]+$' },
|
|
65
|
+
data: 'ABC123',
|
|
66
|
+
expectedParams: ['pattern']
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'format (email)',
|
|
70
|
+
schema: { type: 'string', format: 'email' },
|
|
71
|
+
data: 'invalid-email',
|
|
72
|
+
expectedParams: ['format']
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'type',
|
|
76
|
+
schema: { type: 'number' },
|
|
77
|
+
data: 'not a number',
|
|
78
|
+
expectedParams: ['type']
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'minItems',
|
|
82
|
+
schema: { type: 'array', minItems: 2 },
|
|
83
|
+
data: [1],
|
|
84
|
+
expectedParams: ['limit']
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'maxItems',
|
|
88
|
+
schema: { type: 'array', maxItems: 5 },
|
|
89
|
+
data: [1, 2, 3, 4, 5, 6],
|
|
90
|
+
expectedParams: ['limit']
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'minProperties',
|
|
94
|
+
schema: { type: 'object', minProperties: 2 },
|
|
95
|
+
data: { a: 1 },
|
|
96
|
+
expectedParams: ['limit']
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'maxProperties',
|
|
100
|
+
schema: { type: 'object', maxProperties: 3 },
|
|
101
|
+
data: { a: 1, b: 2, c: 3, d: 4 },
|
|
102
|
+
expectedParams: ['limit']
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'additionalProperties',
|
|
106
|
+
schema: { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false },
|
|
107
|
+
data: { name: 'John', age: 30 },
|
|
108
|
+
expectedParams: ['additionalProperty']
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'uniqueItems',
|
|
112
|
+
schema: { type: 'array', uniqueItems: true },
|
|
113
|
+
data: [1, 2, 2, 3],
|
|
114
|
+
expectedParams: ['i', 'j']
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'const',
|
|
118
|
+
schema: { const: 'fixed_value' },
|
|
119
|
+
data: 'wrong_value',
|
|
120
|
+
expectedParams: ['allowedValue']
|
|
121
|
+
}
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
let round1Passed = 0;
|
|
125
|
+
let round1Failed = 0;
|
|
126
|
+
|
|
127
|
+
testCases.forEach(({ name, schema, data, expectedParams }) => {
|
|
128
|
+
const validate = ajv.compile(schema);
|
|
129
|
+
const valid = validate(data);
|
|
130
|
+
|
|
131
|
+
if (!valid && validate.errors) {
|
|
132
|
+
const formatter = new ErrorFormatter('en-US');
|
|
133
|
+
const formatted = formatter.formatDetailed(validate.errors);
|
|
134
|
+
|
|
135
|
+
const hasTemplateVars = formatted.some(err =>
|
|
136
|
+
err.message.includes('{{#') || err.message.includes('}}')
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (hasTemplateVars) {
|
|
140
|
+
console.log(`❌ ${name}: 错误消息包含未替换的模板变量`);
|
|
141
|
+
console.log(` 消息: ${formatted[0].message}`);
|
|
142
|
+
console.log(` ajv参数: ${JSON.stringify(validate.errors[0].params)}`);
|
|
143
|
+
round1Failed++;
|
|
144
|
+
} else {
|
|
145
|
+
console.log(`✅ ${name}: 参数映射正确`);
|
|
146
|
+
round1Passed++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
console.log(`\n第一轮结果: ${round1Passed} 通过, ${round1Failed} 失败`);
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// 第二轮:边界情况和极端值测试
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
console.log('\n' + '='.repeat(80));
|
|
158
|
+
console.log('\n【第二轮】边界情况和极端值测试\n');
|
|
159
|
+
|
|
160
|
+
const boundaryTests = [
|
|
161
|
+
{
|
|
162
|
+
name: '空字符串枚举',
|
|
163
|
+
schema: { type: 'string', enum: ['', 'a', 'b'] },
|
|
164
|
+
data: 'c'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: '超长枚举列表',
|
|
168
|
+
schema: { type: 'string', enum: Array(100).fill(0).map((_, i) => `value${i}`) },
|
|
169
|
+
data: 'invalid'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: '数字0作为枚举值',
|
|
173
|
+
schema: { type: 'number', enum: [0, 1, 2] },
|
|
174
|
+
data: 3
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'null作为枚举值',
|
|
178
|
+
schema: { enum: [null, 'a', 'b'] },
|
|
179
|
+
data: 'c'
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'undefined作为数据',
|
|
183
|
+
schema: { type: 'string' },
|
|
184
|
+
data: undefined
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: '非常深的嵌套对象',
|
|
188
|
+
schema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
level1: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
level2: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
level3: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
value: { type: 'string', enum: ['a', 'b'] }
|
|
201
|
+
},
|
|
202
|
+
required: ['value']
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
data: { level1: { level2: { level3: { value: 'c' } } } }
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: '包含特殊字符的属性名',
|
|
214
|
+
schema: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
'user-name': { type: 'string' },
|
|
218
|
+
'user.email': { type: 'string' }
|
|
219
|
+
},
|
|
220
|
+
additionalProperties: false
|
|
221
|
+
},
|
|
222
|
+
data: { 'user-name': 'John', 'user.email': 'john@example.com', 'extra-field': 'value' }
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: '极大数值',
|
|
226
|
+
schema: { type: 'number', maximum: 1000 },
|
|
227
|
+
data: Number.MAX_SAFE_INTEGER
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: '极小数值',
|
|
231
|
+
schema: { type: 'number', minimum: -1000 },
|
|
232
|
+
data: Number.MIN_SAFE_INTEGER
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'NaN值',
|
|
236
|
+
schema: { type: 'number' },
|
|
237
|
+
data: NaN
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'Infinity值',
|
|
241
|
+
schema: { type: 'number', maximum: 1000 },
|
|
242
|
+
data: Infinity
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
let round2Passed = 0;
|
|
247
|
+
let round2Failed = 0;
|
|
248
|
+
|
|
249
|
+
boundaryTests.forEach(({ name, schema, data }) => {
|
|
250
|
+
try {
|
|
251
|
+
const validate = ajv.compile(schema);
|
|
252
|
+
const valid = validate(data);
|
|
253
|
+
|
|
254
|
+
if (!valid && validate.errors) {
|
|
255
|
+
const formatter = new ErrorFormatter('en-US');
|
|
256
|
+
const formatted = formatter.formatDetailed(validate.errors);
|
|
257
|
+
|
|
258
|
+
const hasTemplateVars = formatted.some(err =>
|
|
259
|
+
err.message.includes('{{#') || err.message.includes('}}')
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const hasUndefined = formatted.some(err =>
|
|
263
|
+
err.message.includes('undefined')
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (hasTemplateVars) {
|
|
267
|
+
console.log(`❌ ${name}: 包含未替换的模板变量`);
|
|
268
|
+
console.log(` 消息: ${formatted[0].message}`);
|
|
269
|
+
round2Failed++;
|
|
270
|
+
} else if (hasUndefined && !name.includes('undefined')) {
|
|
271
|
+
console.log(`⚠️ ${name}: 包含undefined(可能正常)`);
|
|
272
|
+
console.log(` 消息: ${formatted[0].message}`);
|
|
273
|
+
round2Passed++;
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`✅ ${name}: 处理正确`);
|
|
276
|
+
round2Passed++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.log(`❌ ${name}: 抛出异常 - ${error.message}`);
|
|
281
|
+
round2Failed++;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
console.log(`\n第二轮结果: ${round2Passed} 通过, ${round2Failed} 失败`);
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// 第三轮:多语言一致性测试
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
console.log('\n' + '='.repeat(80));
|
|
292
|
+
console.log('\n【第三轮】多语言一致性测试\n');
|
|
293
|
+
|
|
294
|
+
const languages = ['en-US', 'zh-CN', 'es-ES', 'fr-FR', 'ja-JP'];
|
|
295
|
+
const multiLangTests = [
|
|
296
|
+
{
|
|
297
|
+
name: 'enum错误',
|
|
298
|
+
schema: { type: 'string', enum: ['pro', 'basic', 'free'] },
|
|
299
|
+
data: 'premium'
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'additionalProperties错误',
|
|
303
|
+
schema: { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false },
|
|
304
|
+
data: { name: 'John', age: 30 }
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'required错误',
|
|
308
|
+
schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
309
|
+
data: {}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'type错误',
|
|
313
|
+
schema: { type: 'number' },
|
|
314
|
+
data: 'not a number'
|
|
315
|
+
}
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
let round3Passed = 0;
|
|
319
|
+
let round3Failed = 0;
|
|
320
|
+
|
|
321
|
+
multiLangTests.forEach(({ name, schema, data }) => {
|
|
322
|
+
const validate = ajv.compile(schema);
|
|
323
|
+
const valid = validate(data);
|
|
324
|
+
|
|
325
|
+
if (!valid && validate.errors) {
|
|
326
|
+
let allLangsPassed = true;
|
|
327
|
+
|
|
328
|
+
languages.forEach(lang => {
|
|
329
|
+
const formatter = new ErrorFormatter(lang);
|
|
330
|
+
const formatted = formatter.formatDetailed(validate.errors);
|
|
331
|
+
|
|
332
|
+
const hasTemplateVars = formatted.some(err =>
|
|
333
|
+
err.message.includes('{{#') || err.message.includes('}}')
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (hasTemplateVars) {
|
|
337
|
+
console.log(`❌ ${name} (${lang}): 包含未替换的模板变量`);
|
|
338
|
+
console.log(` 消息: ${formatted[0].message}`);
|
|
339
|
+
allLangsPassed = false;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (allLangsPassed) {
|
|
344
|
+
console.log(`✅ ${name}: 所有语言正确`);
|
|
345
|
+
round3Passed++;
|
|
346
|
+
} else {
|
|
347
|
+
round3Failed++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
console.log(`\n第三轮结果: ${round3Passed} 通过, ${round3Failed} 失败`);
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// 总结
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
console.log('\n' + '='.repeat(80));
|
|
359
|
+
console.log('\n【总结】三轮深度检查结果\n');
|
|
360
|
+
|
|
361
|
+
const totalPassed = round1Passed + round2Passed + round3Passed;
|
|
362
|
+
const totalFailed = round1Failed + round2Failed + round3Failed;
|
|
363
|
+
const totalTests = totalPassed + totalFailed;
|
|
364
|
+
|
|
365
|
+
console.log(`第一轮(参数映射): ${round1Passed}/${round1Passed + round1Failed} 通过`);
|
|
366
|
+
console.log(`第二轮(边界情况): ${round2Passed}/${round2Passed + round2Failed} 通过`);
|
|
367
|
+
console.log(`第三轮(多语言): ${round3Passed}/${round3Passed + round3Failed} 通过`);
|
|
368
|
+
console.log(`\n总计: ${totalPassed}/${totalTests} 通过`);
|
|
369
|
+
|
|
370
|
+
if (totalFailed === 0) {
|
|
371
|
+
console.log('\n✅ 所有检查通过!没有发现问题。');
|
|
372
|
+
} else {
|
|
373
|
+
console.log(`\n⚠️ 发现 ${totalFailed} 个问题需要修复。`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|