schema-dsl 1.1.0 → 1.1.2
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 +302 -9
- package/README.md +870 -279
- package/STATUS.md +65 -3
- package/docs/conditional-api.md +257 -11
- package/docs/number-operators.md +442 -0
- package/examples/i18n-error.examples.js +181 -0
- package/index.d.ts +3268 -3132
- package/index.js +35 -2
- package/lib/adapters/DslAdapter.js +56 -7
- package/lib/core/ConditionalBuilder.js +115 -13
- package/lib/core/DslBuilder.js +47 -1
- package/lib/core/Validator.js +7 -3
- package/lib/errors/I18nError.js +222 -0
- package/lib/locales/en-US.js +25 -0
- package/lib/locales/zh-CN.js +25 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -22,6 +22,10 @@ const ErrorCodes = require('./lib/core/ErrorCodes');
|
|
|
22
22
|
const MessageTemplate = require('./lib/core/MessageTemplate');
|
|
23
23
|
const Locale = require('./lib/core/Locale');
|
|
24
24
|
|
|
25
|
+
// ========== 错误类 ==========
|
|
26
|
+
const ValidationError = require('./lib/errors/ValidationError');
|
|
27
|
+
const I18nError = require('./lib/errors/I18nError');
|
|
28
|
+
|
|
25
29
|
// ========== String 扩展 ==========
|
|
26
30
|
const { installStringExtensions, uninstallStringExtensions } = require('./lib/core/StringExtensions');
|
|
27
31
|
|
|
@@ -48,6 +52,36 @@ dsl.if = function(...args) {
|
|
|
48
52
|
return ConditionalBuilder.start(args[0]);
|
|
49
53
|
};
|
|
50
54
|
|
|
55
|
+
// ✅ dsl.error:统一的多语言错误抛出(v1.1.1+)
|
|
56
|
+
dsl.error = {
|
|
57
|
+
/**
|
|
58
|
+
* 创建多语言错误(不抛出)
|
|
59
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
60
|
+
* @param {Object} params - 错误参数
|
|
61
|
+
* @param {number} statusCode - HTTP 状态码
|
|
62
|
+
* @returns {I18nError} 错误实例
|
|
63
|
+
*/
|
|
64
|
+
create: (code, params, statusCode) => I18nError.create(code, params, statusCode),
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 抛出多语言错误
|
|
68
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
69
|
+
* @param {Object} params - 错误参数
|
|
70
|
+
* @param {number} statusCode - HTTP 状态码
|
|
71
|
+
* @throws {I18nError} 直接抛出错误
|
|
72
|
+
*/
|
|
73
|
+
throw: (code, params, statusCode) => I18nError.throw(code, params, statusCode),
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 断言方法 - 条件不满足时抛错
|
|
77
|
+
* @param {boolean} condition - 条件表达式
|
|
78
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
79
|
+
* @param {Object} params - 错误参数
|
|
80
|
+
* @param {number} statusCode - HTTP 状态码
|
|
81
|
+
*/
|
|
82
|
+
assert: (condition, code, params, statusCode) => I18nError.assert(condition, code, params, statusCode)
|
|
83
|
+
};
|
|
84
|
+
|
|
51
85
|
/**
|
|
52
86
|
* 全局配置
|
|
53
87
|
* @param {Object} options - 配置选项
|
|
@@ -182,8 +216,6 @@ installStringExtensions(dsl);
|
|
|
182
216
|
|
|
183
217
|
// ========== 导出 ==========
|
|
184
218
|
|
|
185
|
-
// 导入 ValidationError
|
|
186
|
-
const ValidationError = require('./lib/errors/ValidationError');
|
|
187
219
|
|
|
188
220
|
// 导入 validateAsync
|
|
189
221
|
const { validateAsync } = require('./lib/adapters/DslAdapter');
|
|
@@ -214,6 +246,7 @@ module.exports = {
|
|
|
214
246
|
|
|
215
247
|
// 错误类 (v2.1.0 新增)
|
|
216
248
|
ValidationError,
|
|
249
|
+
I18nError, // v1.1.1 新增:多语言错误类
|
|
217
250
|
|
|
218
251
|
// 错误消息系统
|
|
219
252
|
ErrorCodes,
|
|
@@ -268,12 +268,59 @@ class DslAdapter {
|
|
|
268
268
|
* @param {string} baseType - 基础类型
|
|
269
269
|
* @param {string} constraint - 约束字符串
|
|
270
270
|
* @returns {Object} 约束对象
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* // 比较运算符 (v1.2.0+)
|
|
274
|
+
* _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
|
|
275
|
+
* _parseConstraint('number', '>=18') // { minimum: 18 }
|
|
276
|
+
* _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
|
|
277
|
+
* _parseConstraint('number', '<=100') // { maximum: 100 }
|
|
278
|
+
* _parseConstraint('number', '=100') // { enum: [100] }
|
|
279
|
+
* _parseConstraint('number', '>0.5') // { exclusiveMinimum: 0.5 } 支持小数
|
|
271
280
|
*/
|
|
272
281
|
static _parseConstraint(baseType, constraint) {
|
|
273
282
|
if (!constraint) return {};
|
|
274
283
|
|
|
275
284
|
const result = {};
|
|
276
285
|
|
|
286
|
+
// ========== 比较运算符(v1.1.2新增,最高优先级)==========
|
|
287
|
+
if (baseType === 'number' || baseType === 'integer') {
|
|
288
|
+
// 1. 大于等于: >=18, >=-10 (支持负数)
|
|
289
|
+
const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
|
|
290
|
+
if (gteMatch) {
|
|
291
|
+
result.minimum = parseFloat(gteMatch[1]);
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 2. 小于等于: <=100, <=-10 (支持负数)
|
|
296
|
+
const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
|
|
297
|
+
if (lteMatch) {
|
|
298
|
+
result.maximum = parseFloat(lteMatch[1]);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 3. 大于: >0, >-10 (不包括边界值,支持负数)
|
|
303
|
+
const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
|
|
304
|
+
if (gtMatch) {
|
|
305
|
+
result.exclusiveMinimum = parseFloat(gtMatch[1]);
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 4. 小于: <100, <-10 (不包括边界值,支持负数)
|
|
310
|
+
const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
|
|
311
|
+
if (ltMatch) {
|
|
312
|
+
result.exclusiveMaximum = parseFloat(ltMatch[1]);
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 5. 等于: =100, =-10 (支持负数)
|
|
317
|
+
const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
|
|
318
|
+
if (eqMatch) {
|
|
319
|
+
result.enum = [parseFloat(eqMatch[1])];
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
277
324
|
// 枚举: value1|value2|value3 (优先检查,避免被数字范围误判)
|
|
278
325
|
if (constraint.includes('|') && !/^\d+-\d+$/.test(constraint)) {
|
|
279
326
|
result.enum = constraint.split('|').map(v => v.trim());
|
|
@@ -281,7 +328,8 @@ class DslAdapter {
|
|
|
281
328
|
}
|
|
282
329
|
|
|
283
330
|
// 范围约束: min-max 或 min- 或 -max
|
|
284
|
-
|
|
331
|
+
// 支持小数: 0.5-99.9
|
|
332
|
+
const rangeMatch = constraint.match(/^(\d*\.?\d*)-(\d*\.?\d*)$/);
|
|
285
333
|
if (rangeMatch) {
|
|
286
334
|
const [, min, max] = rangeMatch;
|
|
287
335
|
if (baseType === 'string') {
|
|
@@ -291,20 +339,21 @@ class DslAdapter {
|
|
|
291
339
|
if (min) result.minItems = parseInt(min, 10);
|
|
292
340
|
if (max) result.maxItems = parseInt(max, 10);
|
|
293
341
|
} else {
|
|
294
|
-
if (min) result.minimum =
|
|
295
|
-
if (max) result.maximum =
|
|
342
|
+
if (min) result.minimum = parseFloat(min);
|
|
343
|
+
if (max) result.maximum = parseFloat(max);
|
|
296
344
|
}
|
|
297
345
|
return result;
|
|
298
346
|
}
|
|
299
347
|
|
|
300
348
|
// 单个约束: min, max
|
|
301
|
-
|
|
349
|
+
// 支持小数: 99.99
|
|
350
|
+
const singleMatch = constraint.match(/^(\d+(?:\.\d+)?)$/);
|
|
302
351
|
if (singleMatch) {
|
|
303
|
-
const value =
|
|
352
|
+
const value = parseFloat(singleMatch[1]);
|
|
304
353
|
if (baseType === 'string') {
|
|
305
|
-
result.maxLength = value;
|
|
354
|
+
result.maxLength = Math.floor(value);
|
|
306
355
|
} else if (baseType === 'array') {
|
|
307
|
-
result.maxItems = value;
|
|
356
|
+
result.maxItems = Math.floor(value);
|
|
308
357
|
} else {
|
|
309
358
|
result.maximum = value;
|
|
310
359
|
}
|
|
@@ -59,7 +59,7 @@ class ConditionalBuilder {
|
|
|
59
59
|
this._conditions.push({
|
|
60
60
|
type: 'if',
|
|
61
61
|
condition: conditionFn,
|
|
62
|
-
combinedConditions: [{ op: 'root', fn: conditionFn }]
|
|
62
|
+
combinedConditions: [{ op: 'root', fn: conditionFn, message: null }]
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
return this;
|
|
@@ -73,6 +73,7 @@ class ConditionalBuilder {
|
|
|
73
73
|
* @example
|
|
74
74
|
* dsl.if((data) => data.age >= 18)
|
|
75
75
|
* .and((data) => data.userType === 'admin')
|
|
76
|
+
* .message('必须是管理员')
|
|
76
77
|
* .then('email!')
|
|
77
78
|
*/
|
|
78
79
|
and(conditionFn) {
|
|
@@ -85,7 +86,7 @@ class ConditionalBuilder {
|
|
|
85
86
|
throw new Error('.and() must follow .if() or .elseIf()');
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
last.combinedConditions.push({ op: 'and', fn: conditionFn });
|
|
89
|
+
last.combinedConditions.push({ op: 'and', fn: conditionFn, message: null });
|
|
89
90
|
return this;
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -109,7 +110,7 @@ class ConditionalBuilder {
|
|
|
109
110
|
throw new Error('.or() must follow .if() or .elseIf()');
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
last.combinedConditions.push({ op: 'or', fn: conditionFn });
|
|
113
|
+
last.combinedConditions.push({ op: 'or', fn: conditionFn, message: null });
|
|
113
114
|
return this;
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -136,7 +137,7 @@ class ConditionalBuilder {
|
|
|
136
137
|
this._conditions.push({
|
|
137
138
|
type: 'elseIf',
|
|
138
139
|
condition: conditionFn,
|
|
139
|
-
combinedConditions: [{ op: 'root', fn: conditionFn }]
|
|
140
|
+
combinedConditions: [{ op: 'root', fn: conditionFn, message: null }]
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
return this;
|
|
@@ -144,7 +145,7 @@ class ConditionalBuilder {
|
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
147
|
* 设置错误消息(支持多语言 key)
|
|
147
|
-
|
|
148
|
+
* 条件为 true 时自动抛出此错误,条件为 false 时通过验证
|
|
148
149
|
*
|
|
149
150
|
* @param {string} msg - 错误消息或多语言 key
|
|
150
151
|
* @returns {ConditionalBuilder} 当前实例(支持链式调用)
|
|
@@ -153,6 +154,13 @@ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
|
|
|
153
154
|
* // 如果是未成年人,抛出错误
|
|
154
155
|
* dsl.if((data) => data.age < 18)
|
|
155
156
|
* .message('未成年用户不能注册')
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* // 为 and 条件设置独立消息
|
|
160
|
+
* dsl.if((data) => !data)
|
|
161
|
+
* .message('账户不存在')
|
|
162
|
+
* .and((data) => data.balance < 100)
|
|
163
|
+
* .message('余额不足')
|
|
156
164
|
*/
|
|
157
165
|
message(msg) {
|
|
158
166
|
if (typeof msg !== 'string') {
|
|
@@ -164,6 +172,14 @@ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
|
|
|
164
172
|
throw new Error('.message() must follow .if() or .elseIf()');
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
// 找到最后一个添加的条件(可能是 root、and 或 or)
|
|
176
|
+
const lastCombined = last.combinedConditions[last.combinedConditions.length - 1];
|
|
177
|
+
if (lastCombined) {
|
|
178
|
+
// 为最后一个组合条件设置消息
|
|
179
|
+
lastCombined.message = msg;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 同时设置整体消息(作为后备)
|
|
167
183
|
last.message = msg;
|
|
168
184
|
last.action = 'throw'; // 有 message 就自动 throw
|
|
169
185
|
return this;
|
|
@@ -216,31 +232,117 @@ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
|
|
|
216
232
|
* @private
|
|
217
233
|
* @param {Object} conditionObj - 条件对象
|
|
218
234
|
* @param {*} data - 待验证数据对象
|
|
219
|
-
* @returns {boolean}
|
|
235
|
+
* @returns {Object} { result: boolean, failedMessage: string|null } 条件结果和失败消息
|
|
236
|
+
*
|
|
237
|
+
* 语义说明:
|
|
238
|
+
* - 传统 AND 模式:所有条件都为 true 才失败
|
|
239
|
+
* - 链式检查模式:root 有 message,且任何 .and() 也有独立 message 时
|
|
240
|
+
* → 依次检查,第一个为 true 的失败
|
|
220
241
|
*/
|
|
221
242
|
_evaluateCondition(conditionObj, data) {
|
|
222
243
|
try {
|
|
244
|
+
// 检查是否是链式检查模式:
|
|
245
|
+
// 1. 必须是 message 模式(action='throw')
|
|
246
|
+
// 2. root 条件有 message
|
|
247
|
+
// 3. 有 .and() 条件(不管是否有独立 message)
|
|
248
|
+
// 4. 没有 .or() 条件(有 OR 就使用传统逻辑)
|
|
249
|
+
const isMessageMode = conditionObj.action === 'throw';
|
|
250
|
+
const rootHasMessage = conditionObj.combinedConditions[0]?.message != null;
|
|
251
|
+
const hasAndConditions = conditionObj.combinedConditions.some(c => c.op === 'and');
|
|
252
|
+
const hasOrConditions = conditionObj.combinedConditions.some(c => c.op === 'or');
|
|
253
|
+
const isChainCheckMode = isMessageMode && rootHasMessage && hasAndConditions && !hasOrConditions;
|
|
254
|
+
|
|
223
255
|
let result = false;
|
|
256
|
+
let failedMessage = null;
|
|
224
257
|
|
|
225
258
|
for (let i = 0; i < conditionObj.combinedConditions.length; i++) {
|
|
226
259
|
const combined = conditionObj.combinedConditions[i];
|
|
260
|
+
let conditionResult = false;
|
|
227
261
|
|
|
228
262
|
if (combined.op === 'root') {
|
|
229
263
|
// 第一个条件
|
|
230
|
-
|
|
264
|
+
conditionResult = combined.fn(data);
|
|
265
|
+
result = conditionResult;
|
|
266
|
+
|
|
267
|
+
if (isChainCheckMode) {
|
|
268
|
+
// 链式检查模式:第一个条件为 true 就失败
|
|
269
|
+
if (result) {
|
|
270
|
+
failedMessage = combined.message || conditionObj.message;
|
|
271
|
+
return { result: true, failedMessage };
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// 传统模式
|
|
275
|
+
failedMessage = combined.message || conditionObj.message;
|
|
276
|
+
|
|
277
|
+
// 如果第一个条件为 false,检查是否有 OR 条件
|
|
278
|
+
if (!result) {
|
|
279
|
+
const hasOrConditions = conditionObj.combinedConditions.some(c => c.op === 'or');
|
|
280
|
+
if (!hasOrConditions) {
|
|
281
|
+
// 没有 OR 条件,直接通过
|
|
282
|
+
return { result: false, failedMessage: null };
|
|
283
|
+
}
|
|
284
|
+
// 有 OR 条件,继续检查
|
|
285
|
+
}
|
|
286
|
+
}
|
|
231
287
|
} else if (combined.op === 'and') {
|
|
232
|
-
|
|
233
|
-
|
|
288
|
+
conditionResult = combined.fn(data);
|
|
289
|
+
|
|
290
|
+
if (isChainCheckMode) {
|
|
291
|
+
// 链式检查模式:任一条件为 true 就失败
|
|
292
|
+
if (conditionResult) {
|
|
293
|
+
// 使用独立消息,如果没有则使用整体消息
|
|
294
|
+
failedMessage = combined.message || conditionObj.message;
|
|
295
|
+
return { result: true, failedMessage };
|
|
296
|
+
}
|
|
297
|
+
// 条件为 false,继续检查下一个条件
|
|
298
|
+
} else {
|
|
299
|
+
// 传统 AND 模式:所有条件都必须为 true 才失败
|
|
300
|
+
if (!conditionResult) {
|
|
301
|
+
// AND 条件为 false
|
|
302
|
+
// 检查是否有 OR 条件
|
|
303
|
+
if (hasOrConditions) {
|
|
304
|
+
// 有 OR 条件,将 result 设为 false,继续检查 OR
|
|
305
|
+
result = false;
|
|
306
|
+
} else {
|
|
307
|
+
// 没有 OR 条件,任一 AND 条件为 false 就验证通过
|
|
308
|
+
return { result: false, failedMessage: null };
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
// AND 条件为 true,继续累积
|
|
312
|
+
result = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
234
315
|
} else if (combined.op === 'or') {
|
|
235
|
-
// OR
|
|
236
|
-
|
|
316
|
+
// OR 逻辑:任一条件为 true 就失败
|
|
317
|
+
if (!result) {
|
|
318
|
+
conditionResult = combined.fn(data);
|
|
319
|
+
|
|
320
|
+
if (conditionResult) {
|
|
321
|
+
// OR 条件为 true
|
|
322
|
+
result = true; // 更新 result
|
|
323
|
+
|
|
324
|
+
if (isMessageMode) {
|
|
325
|
+
// message 模式:立即返回失败
|
|
326
|
+
failedMessage = combined.message || conditionObj.message;
|
|
327
|
+
return { result: true, failedMessage };
|
|
328
|
+
}
|
|
329
|
+
// then/else 模式:继续累积 result(已经设置为true)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
237
332
|
}
|
|
238
333
|
}
|
|
239
334
|
|
|
240
|
-
|
|
335
|
+
// 返回最终结果
|
|
336
|
+
if (isChainCheckMode) {
|
|
337
|
+
// 链式检查模式:所有条件都为 false,验证通过
|
|
338
|
+
return { result: false, failedMessage: null };
|
|
339
|
+
} else {
|
|
340
|
+
// 传统模式:返回累积结果
|
|
341
|
+
return { result, failedMessage };
|
|
342
|
+
}
|
|
241
343
|
} catch (error) {
|
|
242
344
|
// 条件函数执行出错,视为不满足
|
|
243
|
-
return false;
|
|
345
|
+
return { result: false, failedMessage: null };
|
|
244
346
|
}
|
|
245
347
|
}
|
|
246
348
|
|
package/lib/core/DslBuilder.js
CHANGED
|
@@ -383,12 +383,58 @@ class DslBuilder {
|
|
|
383
383
|
/**
|
|
384
384
|
* 解析约束
|
|
385
385
|
* @private
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* // 比较运算符 (v1.2.0+)
|
|
389
|
+
* _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
|
|
390
|
+
* _parseConstraint('number', '>=18') // { minimum: 18 }
|
|
391
|
+
* _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
|
|
392
|
+
* _parseConstraint('number', '<=100') // { maximum: 100 }
|
|
393
|
+
* _parseConstraint('number', '=100') // { enum: [100] }
|
|
386
394
|
*/
|
|
387
395
|
_parseConstraint(type, constraint) {
|
|
388
396
|
const result = {};
|
|
389
397
|
|
|
390
398
|
if (type === 'string' || type === 'number' || type === 'integer') {
|
|
391
|
-
//
|
|
399
|
+
// ========== 比较运算符(v1.1.2新增,仅number/integer,最高优先级)==========
|
|
400
|
+
if (type === 'number' || type === 'integer') {
|
|
401
|
+
// 1. 大于等于: >=18, >=-10 (支持负数)
|
|
402
|
+
const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
|
|
403
|
+
if (gteMatch) {
|
|
404
|
+
result.minimum = parseFloat(gteMatch[1]);
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 2. 小于等于: <=100, <=-10 (支持负数)
|
|
409
|
+
const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
|
|
410
|
+
if (lteMatch) {
|
|
411
|
+
result.maximum = parseFloat(lteMatch[1]);
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 3. 大于: >0, >-10 (不包括边界值,支持负数)
|
|
416
|
+
const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
|
|
417
|
+
if (gtMatch) {
|
|
418
|
+
result.exclusiveMinimum = parseFloat(gtMatch[1]);
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 4. 小于: <100, <-10 (不包括边界值,支持负数)
|
|
423
|
+
const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
|
|
424
|
+
if (ltMatch) {
|
|
425
|
+
result.exclusiveMaximum = parseFloat(ltMatch[1]);
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 5. 等于: =100, =-10 (支持负数)
|
|
430
|
+
const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
|
|
431
|
+
if (eqMatch) {
|
|
432
|
+
result.enum = [parseFloat(eqMatch[1])];
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ========== 范围约束: min-max ==========
|
|
392
438
|
if (constraint.includes('-')) {
|
|
393
439
|
const [min, max] = constraint.split('-').map(v => v.trim());
|
|
394
440
|
|
package/lib/core/Validator.js
CHANGED
|
@@ -464,16 +464,20 @@ class Validator {
|
|
|
464
464
|
for (let i = 0; i < conditionalSchema.conditions.length; i++) {
|
|
465
465
|
const cond = conditionalSchema.conditions[i];
|
|
466
466
|
|
|
467
|
-
// 执行组合条件(支持 and/or
|
|
468
|
-
const
|
|
467
|
+
// 执行组合条件(支持 and/or),获取结果和失败消息
|
|
468
|
+
const evaluation = conditionalSchema._evaluateCondition(cond, data);
|
|
469
|
+
const matched = evaluation.result;
|
|
470
|
+
const failedMessage = evaluation.failedMessage;
|
|
469
471
|
|
|
470
472
|
if (cond.action === 'throw') {
|
|
471
473
|
// ✅ message 模式:条件为 true 时抛错,条件为 false 时通过
|
|
472
474
|
if (matched) {
|
|
473
475
|
// ✅ 条件满足(true),抛出错误
|
|
476
|
+
// 优先使用具体的失败消息,如果没有则使用整体消息
|
|
477
|
+
const errorMsg = failedMessage || cond.message;
|
|
474
478
|
// 支持多语言:如果 message 是 key(如 'conditional.underAge'),从语言包获取翻译
|
|
475
479
|
// 传递 locale 参数以支持动态语言切换
|
|
476
|
-
const errorMessage = Locale.getMessage(
|
|
480
|
+
const errorMessage = Locale.getMessage(errorMsg, options.messages || {}, locale);
|
|
477
481
|
|
|
478
482
|
return {
|
|
479
483
|
valid: false,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18nError - 多语言错误工具类
|
|
3
|
+
*
|
|
4
|
+
* 提供统一的多语言错误抛出机制,支持:
|
|
5
|
+
* - 多语言 key 自动翻译
|
|
6
|
+
* - 参数插值(如 {{field}}, {{limit}})
|
|
7
|
+
* - 自定义错误代码
|
|
8
|
+
* - Express/Koa 集成
|
|
9
|
+
*
|
|
10
|
+
* @module lib/errors/I18nError
|
|
11
|
+
* @version 1.1.1
|
|
12
|
+
*
|
|
13
|
+
* @example 基础用法
|
|
14
|
+
* const { I18nError } = require('schema-dsl');
|
|
15
|
+
*
|
|
16
|
+
* // 抛出多语言错误
|
|
17
|
+
* throw I18nError.create('error.notFound', { resource: '账户' });
|
|
18
|
+
* // 中文: "找不到账户"
|
|
19
|
+
* // 英文: "Account not found"
|
|
20
|
+
*
|
|
21
|
+
* @example 业务代码中使用
|
|
22
|
+
* function getAccount(id) {
|
|
23
|
+
* const account = db.findAccount(id);
|
|
24
|
+
* if (!account) {
|
|
25
|
+
* throw I18nError.create('account.notFound', { accountId: id });
|
|
26
|
+
* }
|
|
27
|
+
* if (account.balance < 100) {
|
|
28
|
+
* throw I18nError.create('account.insufficientBalance', {
|
|
29
|
+
* balance: account.balance,
|
|
30
|
+
* required: 100
|
|
31
|
+
* });
|
|
32
|
+
* }
|
|
33
|
+
* return account;
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* @example Express 中间件
|
|
37
|
+
* app.use((error, req, res, next) => {
|
|
38
|
+
* if (error instanceof I18nError) {
|
|
39
|
+
* return res.status(error.statusCode).json(error.toJSON());
|
|
40
|
+
* }
|
|
41
|
+
* next(error);
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const Locale = require('../core/Locale');
|
|
46
|
+
const MessageTemplate = require('../core/MessageTemplate');
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 多语言错误类
|
|
50
|
+
*
|
|
51
|
+
* @class I18nError
|
|
52
|
+
* @extends Error
|
|
53
|
+
*
|
|
54
|
+
* @property {string} name - 错误名称(固定为 'I18nError')
|
|
55
|
+
* @property {string} message - 错误消息(已翻译)
|
|
56
|
+
* @property {string} code - 错误代码(多语言 key)
|
|
57
|
+
* @property {Object} params - 错误参数(用于插值)
|
|
58
|
+
* @property {number} statusCode - HTTP 状态码(默认 400)
|
|
59
|
+
* @property {string} locale - 使用的语言环境
|
|
60
|
+
*/
|
|
61
|
+
class I18nError extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* 构造函数
|
|
64
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
65
|
+
* @param {Object} params - 错误参数(用于插值)
|
|
66
|
+
* @param {number} statusCode - HTTP 状态码(默认 400)
|
|
67
|
+
* @param {string} locale - 语言环境(默认使用当前语言)
|
|
68
|
+
*/
|
|
69
|
+
constructor(code, params = {}, statusCode = 400, locale = null) {
|
|
70
|
+
// 获取翻译后的消息模板
|
|
71
|
+
const actualLocale = locale || Locale.getLocale();
|
|
72
|
+
const template = Locale.getMessage(code, {}, actualLocale);
|
|
73
|
+
|
|
74
|
+
// 使用 MessageTemplate 进行参数插值
|
|
75
|
+
const messageTemplate = new MessageTemplate(template);
|
|
76
|
+
const message = messageTemplate.render(params || {});
|
|
77
|
+
|
|
78
|
+
super(message);
|
|
79
|
+
|
|
80
|
+
this.name = 'I18nError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.params = params || {};
|
|
83
|
+
this.statusCode = statusCode;
|
|
84
|
+
this.locale = actualLocale;
|
|
85
|
+
|
|
86
|
+
// 保持堆栈跟踪
|
|
87
|
+
if (Error.captureStackTrace) {
|
|
88
|
+
Error.captureStackTrace(this, I18nError);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 静态工厂方法 - 创建并抛出错误
|
|
94
|
+
*
|
|
95
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
96
|
+
* @param {Object} params - 错误参数
|
|
97
|
+
* @param {number} statusCode - HTTP 状态码
|
|
98
|
+
* @returns {I18nError} 错误实例
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // 创建错误(不抛出)
|
|
102
|
+
* const error = I18nError.create('error.notFound', { resource: '用户' });
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // 直接抛出
|
|
106
|
+
* throw I18nError.create('error.notFound', { resource: '用户' });
|
|
107
|
+
*/
|
|
108
|
+
static create(code, params = {}, statusCode = 400) {
|
|
109
|
+
return new I18nError(code, params, statusCode);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 静态工厂方法 - 快速抛出错误
|
|
114
|
+
*
|
|
115
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
116
|
+
* @param {Object} params - 错误参数
|
|
117
|
+
* @param {number} statusCode - HTTP 状态码
|
|
118
|
+
* @throws {I18nError} 直接抛出错误
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* I18nError.throw('error.notFound', { resource: '用户' });
|
|
122
|
+
* // 等同于:throw I18nError.create('error.notFound', { resource: '用户' });
|
|
123
|
+
*/
|
|
124
|
+
static throw(code, params = {}, statusCode = 400) {
|
|
125
|
+
throw new I18nError(code, params, statusCode);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 断言方法 - 条件不满足时抛错
|
|
130
|
+
*
|
|
131
|
+
* @param {boolean} condition - 条件表达式
|
|
132
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
133
|
+
* @param {Object} params - 错误参数
|
|
134
|
+
* @param {number} statusCode - HTTP 状态码
|
|
135
|
+
* @throws {I18nError} 条件为 false 时抛出错误
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* I18nError.assert(account, 'account.notFound', { accountId: id });
|
|
139
|
+
* // 等同于:if (!account) throw I18nError.create('account.notFound', { accountId: id });
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* I18nError.assert(
|
|
143
|
+
* account.balance >= 100,
|
|
144
|
+
* 'account.insufficientBalance',
|
|
145
|
+
* { balance: account.balance, required: 100 }
|
|
146
|
+
* );
|
|
147
|
+
*/
|
|
148
|
+
static assert(condition, code, params = {}, statusCode = 400) {
|
|
149
|
+
if (!condition) {
|
|
150
|
+
throw new I18nError(code, params, statusCode);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 检查错误是否为指定代码
|
|
156
|
+
*
|
|
157
|
+
* @param {string} code - 错误代码
|
|
158
|
+
* @returns {boolean} 是否匹配
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* try {
|
|
162
|
+
* // ...
|
|
163
|
+
* } catch (error) {
|
|
164
|
+
* if (error instanceof I18nError && error.is('account.notFound')) {
|
|
165
|
+
* // 处理账户不存在的情况
|
|
166
|
+
* }
|
|
167
|
+
* }
|
|
168
|
+
*/
|
|
169
|
+
is(code) {
|
|
170
|
+
return this.code === code;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 转换为 JSON 格式(用于 API 响应)
|
|
175
|
+
*
|
|
176
|
+
* @returns {Object} JSON 对象
|
|
177
|
+
* @returns {string} return.error - 错误名称
|
|
178
|
+
* @returns {string} return.code - 错误代码
|
|
179
|
+
* @returns {string} return.message - 错误消息(已翻译)
|
|
180
|
+
* @returns {Object} return.params - 错误参数
|
|
181
|
+
* @returns {number} return.statusCode - 状态码
|
|
182
|
+
* @returns {string} return.locale - 语言环境
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* const json = error.toJSON();
|
|
186
|
+
* res.status(error.statusCode).json(json);
|
|
187
|
+
* // {
|
|
188
|
+
* // error: 'I18nError',
|
|
189
|
+
* // code: 'account.notFound',
|
|
190
|
+
* // message: '找不到账户',
|
|
191
|
+
* // params: { accountId: '123' },
|
|
192
|
+
* // statusCode: 404,
|
|
193
|
+
* // locale: 'zh-CN'
|
|
194
|
+
* // }
|
|
195
|
+
*/
|
|
196
|
+
toJSON() {
|
|
197
|
+
return {
|
|
198
|
+
error: this.name,
|
|
199
|
+
code: this.code,
|
|
200
|
+
message: this.message,
|
|
201
|
+
params: this.params,
|
|
202
|
+
statusCode: this.statusCode,
|
|
203
|
+
locale: this.locale
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 转换为字符串
|
|
209
|
+
*
|
|
210
|
+
* @returns {string} 格式化的错误信息
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* console.log(error.toString());
|
|
214
|
+
* // "I18nError [account.notFound]: 找不到账户"
|
|
215
|
+
*/
|
|
216
|
+
toString() {
|
|
217
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = I18nError;
|
|
222
|
+
|