schema-dsl 1.2.4 → 1.2.5

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.
@@ -1,1400 +1,1589 @@
1
- /**
2
- * DSL Builder - 统一的Schema构建器
3
- *
4
- * 支持链式调用扩展DSL功能
5
- *
6
- * @module lib/core/DslBuilder
7
- * @version 2.0.0
8
- *
9
- * @example
10
- * // 简单使用
11
- * const schema = dsl('email!');
12
- *
13
- * // 链式扩展
14
- * const schema = dsl('email!')
15
- * .pattern(/custom/)
16
- * .messages({ 'string.pattern': '格式不正确' })
17
- * .label('邮箱地址');
18
- */
19
-
20
- const ErrorCodes = require('./ErrorCodes');
21
- const MessageTemplate = require('./MessageTemplate');
22
- const Locale = require('./Locale');
23
- const patterns = require('../config/patterns');
24
-
25
- class DslBuilder {
26
- /**
27
- * 静态属性:存储用户自定义类型(插件注册)
28
- * @private
29
- * @type {Map<string, Object|Function>}
30
- */
31
- static _customTypes = new Map();
32
-
33
- /**
34
- * 注册自定义类型(供插件使用)
35
- * @param {string} name - 类型名称
36
- * @param {Object|Function} schema - JSON Schema对象 或 生成函数
37
- * @throws {Error} 类型名称无效时抛出错误
38
- *
39
- * @example
40
- * // 插件中注册自定义类型
41
- * DslBuilder.registerType('phone-cn', {
42
- * type: 'string',
43
- * pattern: '^1[3-9]\\d{9}$'
44
- * });
45
- *
46
- * // 在DSL中使用
47
- * dsl('phone-cn!') // ✅ 可用
48
- * dsl('types:string|phone-cn') // ✅ 可用
49
- */
50
- static registerType(name, schema) {
51
- if (!name || typeof name !== 'string') {
52
- throw new Error('Type name must be a non-empty string');
53
- }
54
-
55
- if (!schema || (typeof schema !== 'object' && typeof schema !== 'function')) {
56
- throw new Error('Schema must be an object or function');
57
- }
58
-
59
- this._customTypes.set(name, schema);
60
- }
61
-
62
- /**
63
- * 检查类型是否已注册(内置或自定义)
64
- * @param {string} type - 类型名称
65
- * @returns {boolean}
66
- *
67
- * @example
68
- * DslBuilder.hasType('string') // true (内置)
69
- * DslBuilder.hasType('phone-cn') // false (未注册)
70
- *
71
- * DslBuilder.registerType('phone-cn', { ... });
72
- * DslBuilder.hasType('phone-cn') // true (已注册)
73
- */
74
- static hasType(type) {
75
- // 检查自定义类型
76
- if (this._customTypes.has(type)) {
77
- return true;
78
- }
79
-
80
- // 检查内置类型
81
- const builtInTypes = [
82
- 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
83
- 'email', 'url', 'uuid', 'date', 'datetime', 'time',
84
- 'ipv4', 'ipv6', 'binary', 'objectId', 'hexColor', 'macAddress',
85
- 'cron', 'slug', 'alphanum', 'lower', 'upper', 'json', 'port',
86
- 'phone', 'idCard', 'creditCard', 'licensePlate', 'postalCode', 'passport', 'any'
87
- ];
88
-
89
- return builtInTypes.includes(type);
90
- }
91
-
92
- /**
93
- * 获取所有已注册的自定义类型
94
- * @returns {Array<string>}
95
- */
96
- static getCustomTypes() {
97
- return Array.from(this._customTypes.keys());
98
- }
99
-
100
- /**
101
- * 清除所有自定义类型(主要用于测试)
102
- */
103
- static clearCustomTypes() {
104
- this._customTypes.clear();
105
- }
106
-
107
- /**
108
- * 创建 DslBuilder 实例
109
- * @param {string} dslString - DSL字符串,如 'string:3-32!' 或 'email!'
110
- */
111
- constructor(dslString) {
112
- if (!dslString || typeof dslString !== 'string') {
113
- throw new Error('DSL string is required');
114
- }
115
-
116
- // 解析DSL字符串
117
- const trimmed = dslString.trim();
118
-
119
- // 特殊处理:array!数字 → array:数字 + 必填
120
- // 例如:array!1-10 → array:1-10!
121
- let processedDsl = trimmed;
122
- if (/^array![\d-]/.test(trimmed)) {
123
- processedDsl = trimmed.replace(/^array!/, 'array:') + '!';
124
- }
125
-
126
- // 🔴 处理必填标记 ! 和可选标记 ?
127
- // 优先级:! > ?(如果同时存在,! 优先)
128
- this._required = processedDsl.endsWith('!');
129
- this._optional = processedDsl.endsWith('?') && !this._required;
130
-
131
- let dslWithoutMarker = processedDsl;
132
- if (this._required) {
133
- dslWithoutMarker = processedDsl.slice(0, -1);
134
- } else if (this._optional) {
135
- dslWithoutMarker = processedDsl.slice(0, -1);
136
- }
137
-
138
- // 简单解析为基础Schema(避免循环依赖)
139
- this._baseSchema = this._parseSimple(dslWithoutMarker);
140
-
141
- // 扩展属性
142
- this._customMessages = {};
143
- this._label = null;
144
- this._customValidators = [];
145
- this._description = null;
146
- this._whenConditions = [];
147
- }
148
-
149
- /**
150
- * 简单解析DSL字符串(避免循环依赖)
151
- * @private
152
- * @param {string} dsl - DSL字符串(不含!)
153
- * @returns {Object} JSON Schema对象
154
- */
155
- _parseSimple(dsl) {
156
- // 🔴 处理跨类型联合:types:type1|type2|type3
157
- if (dsl.startsWith('types:')) {
158
- const typesStr = dsl.substring(6); // 去掉 'types:' 前缀
159
- const types = typesStr.split('|').map(t => t.trim()).filter(t => t);
160
-
161
- if (types.length === 0) {
162
- throw new Error('types: requires at least one type');
163
- }
164
-
165
- if (types.length === 1) {
166
- // 只有一个类型,直接解析(避免不必要的oneOf)
167
- return this._parseSimple(types[0]);
168
- }
169
-
170
- // 多个类型,生成oneOf结构
171
- return {
172
- oneOf: types.map(type => this._parseSimple(type))
173
- };
174
- }
175
-
176
- // 处理数组类型:array:1-10 array<string>
177
- if (dsl.startsWith('array')) {
178
- const schema = { type: 'array' };
179
-
180
- // 匹配:array:min-max<itemType> array:constraint<itemType> array<itemType>
181
- const arrayMatch = dsl.match(/^array(?::([^<]+?))?(?:<(.+)>)?$/);
182
-
183
- if (arrayMatch) {
184
- const [, constraint, itemType] = arrayMatch;
185
-
186
- // 解析约束
187
- if (constraint) {
188
- const trimmedConstraint = constraint.trim();
189
-
190
- if (trimmedConstraint.includes('-')) {
191
- // 范围约束: min-max, min-, -max
192
- const [min, max] = trimmedConstraint.split('-').map(v => v.trim());
193
- if (min) schema.minItems = parseInt(min, 10);
194
- if (max) schema.maxItems = parseInt(max, 10);
195
- } else {
196
- // 单个值 = 最大值
197
- schema.maxItems = parseInt(trimmedConstraint, 10);
198
- }
199
- }
200
-
201
- // 解析元素类型
202
- if (itemType) {
203
- schema.items = this._parseSimple(itemType.trim());
204
- }
205
-
206
- return schema;
207
- }
208
- }
209
-
210
- // 处理枚举(支持多种格式)
211
- if (dsl.includes('|')) {
212
- let enumType = 'string'; // 默认字符串
213
- let enumValues = dsl;
214
-
215
- // 识别 enum:type:values enum:values 格式
216
- if (dsl.startsWith('enum:')) {
217
- const parts = dsl.slice(5).split(':');
218
-
219
- if (parts.length === 2) {
220
- // enum:type:values
221
- enumType = parts[0];
222
- enumValues = parts[1];
223
- } else if (parts.length === 1) {
224
- // enum:values (默认 string)
225
- enumValues = parts[0];
226
- }
227
- } else if (dsl.includes(':') && !this._isKnownType(dsl.split(':')[0])) {
228
- // 如果有冒号但不是已知类型(如 string:3-32),不作为枚举
229
- // 让后续逻辑处理
230
- } else {
231
- // 简写形式:value1|value2
232
- // 自动识别类型
233
- enumType = this._detectEnumType(enumValues);
234
- }
235
-
236
- // 如果是枚举,解析值
237
- if (enumValues.includes('|')) {
238
- return this._parseEnum(enumType, enumValues);
239
- }
240
- }
241
-
242
- // 处理类型:约束格式
243
- const colonIndex = dsl.indexOf(':');
244
- let type, constraint;
245
-
246
- if (colonIndex === -1) {
247
- type = dsl;
248
- constraint = '';
249
- } else {
250
- type = dsl.substring(0, colonIndex);
251
- constraint = dsl.substring(colonIndex + 1);
252
- }
253
-
254
- // 特殊处理 phone:country
255
- if (type === 'phone') {
256
- const country = constraint || 'cn';
257
- const config = patterns.phone[country];
258
- if (!config) throw new Error(`Unsupported country: ${country}`);
259
- return {
260
- type: 'string',
261
- pattern: config.pattern.source,
262
- minLength: config.min,
263
- maxLength: config.max,
264
- _customMessages: { 'pattern': config.key || config.msg }
265
- };
266
- }
267
-
268
- // 特殊处理 idCard:country
269
- if (type === 'idCard') {
270
- const country = constraint || 'cn';
271
- const config = patterns.idCard[country.toLowerCase()];
272
- if (!config) throw new Error(`Unsupported country for idCard: ${country}`);
273
- return {
274
- type: 'string',
275
- pattern: config.pattern.source,
276
- minLength: config.min,
277
- maxLength: config.max,
278
- _customMessages: { 'pattern': config.key || config.msg }
279
- };
280
- }
281
-
282
- // 特殊处理 creditCard:type
283
- if (type === 'creditCard') {
284
- const cardType = constraint || 'visa';
285
- const config = patterns.creditCard[cardType.toLowerCase()];
286
- if (!config) throw new Error(`Unsupported credit card type: ${cardType}`);
287
- return {
288
- type: 'string',
289
- pattern: config.pattern.source,
290
- _customMessages: { 'pattern': config.key || config.msg }
291
- };
292
- }
293
-
294
- // 特殊处理 licensePlate:country
295
- if (type === 'licensePlate') {
296
- const country = constraint || 'cn';
297
- const config = patterns.licensePlate[country.toLowerCase()];
298
- if (!config) throw new Error(`Unsupported country for licensePlate: ${country}`);
299
- return {
300
- type: 'string',
301
- pattern: config.pattern.source,
302
- _customMessages: { 'pattern': config.key || config.msg }
303
- };
304
- }
305
-
306
- // 特殊处理 postalCode:country
307
- if (type === 'postalCode') {
308
- const country = constraint || 'cn';
309
- const config = patterns.postalCode[country.toLowerCase()];
310
- if (!config) throw new Error(`Unsupported country for postalCode: ${country}`);
311
- return {
312
- type: 'string',
313
- pattern: config.pattern.source,
314
- _customMessages: { 'pattern': config.key || config.msg }
315
- };
316
- }
317
-
318
- // 特殊处理 passport:country
319
- if (type === 'passport') {
320
- const country = constraint || 'cn';
321
- const config = patterns.passport[country.toLowerCase()];
322
- if (!config) throw new Error(`Unsupported country for passport: ${country}`);
323
- return {
324
- type: 'string',
325
- pattern: config.pattern.source,
326
- _customMessages: { 'pattern': config.key || config.msg }
327
- };
328
- }
329
-
330
- // 获取基础类型
331
- const schema = this._getBaseType(type);
332
-
333
- // 处理约束
334
- if (constraint) {
335
- Object.assign(schema, this._parseConstraint(schema.type, constraint));
336
- }
337
-
338
- return schema;
339
- }
340
-
341
- /**
342
- * 获取基础类型Schema
343
- * @private
344
- */
345
- _getBaseType(type) {
346
- // 🔴 优先查询自定义类型(插件注册的)
347
- if (DslBuilder._customTypes.has(type)) {
348
- const customSchema = DslBuilder._customTypes.get(type);
349
- // 如果是函数,调用它生成Schema
350
- if (typeof customSchema === 'function') {
351
- return customSchema();
352
- }
353
- // 否则返回Schema对象的深拷贝(避免污染)
354
- return JSON.parse(JSON.stringify(customSchema));
355
- }
356
-
357
- // 🔴 查询内置类型
358
- const typeMap = {
359
- 'string': { type: 'string' },
360
- 'number': { type: 'number' },
361
- 'integer': { type: 'integer' },
362
- 'boolean': { type: 'boolean' },
363
- 'object': { type: 'object' },
364
- 'array': { type: 'array' },
365
- 'null': { type: 'null' },
366
- 'email': { type: 'string', format: 'email' },
367
- 'url': { type: 'string', format: 'uri' },
368
- 'uuid': { type: 'string', format: 'uuid' },
369
- 'date': { type: 'string', format: 'date' },
370
- 'datetime': { type: 'string', format: 'date-time' },
371
- 'time': { type: 'string', format: 'time' },
372
- 'ipv4': { type: 'string', format: 'ipv4' },
373
- 'ipv6': { type: 'string', format: 'ipv6' },
374
- 'binary': { type: 'string', contentEncoding: 'base64' },
375
- 'objectId': { type: 'string', pattern: '^[0-9a-fA-F]{24}$', _customMessages: { 'pattern': 'pattern.objectId' } },
376
- 'hexColor': { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$', _customMessages: { 'pattern': 'pattern.hexColor' } },
377
- 'macAddress': { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', _customMessages: { 'pattern': 'pattern.macAddress' } },
378
- 'cron': { type: 'string', pattern: '^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) (\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) (\\*|([0-6])|\\*\\/([0-6]))$', _customMessages: { 'pattern': 'pattern.cron' } },
379
- 'slug': { type: 'string', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', _customMessages: { 'pattern': 'pattern.slug' } },
380
- 'any': {},
381
- // v1.0.2 新增类型
382
- 'alphanum': { type: 'string', alphanum: true },
383
- 'lower': { type: 'string', lowercase: true },
384
- 'upper': { type: 'string', uppercase: true },
385
- 'json': { type: 'string', jsonString: true },
386
- 'port': { type: 'integer', port: true }
387
- };
388
-
389
- return typeMap[type] || { type: 'string' };
390
- }
391
-
392
- /**
393
- * 解析约束
394
- * @private
395
- *
396
- * @example
397
- * // 比较运算符 (v1.2.0+)
398
- * _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
399
- * _parseConstraint('number', '>=18') // { minimum: 18 }
400
- * _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
401
- * _parseConstraint('number', '<=100') // { maximum: 100 }
402
- * _parseConstraint('number', '=100') // { enum: [100] }
403
- */
404
- _parseConstraint(type, constraint) {
405
- const result = {};
406
-
407
- if (type === 'string' || type === 'number' || type === 'integer') {
408
- // ========== 比较运算符(v1.1.2新增,仅number/integer,最高优先级)==========
409
- if (type === 'number' || type === 'integer') {
410
- // 1. 大于等于: >=18, >=-10 (支持负数)
411
- const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
412
- if (gteMatch) {
413
- result.minimum = parseFloat(gteMatch[1]);
414
- return result;
415
- }
416
-
417
- // 2. 小于等于: <=100, <=-10 (支持负数)
418
- const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
419
- if (lteMatch) {
420
- result.maximum = parseFloat(lteMatch[1]);
421
- return result;
422
- }
423
-
424
- // 3. 大于: >0, >-10 (不包括边界值,支持负数)
425
- const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
426
- if (gtMatch) {
427
- result.exclusiveMinimum = parseFloat(gtMatch[1]);
428
- return result;
429
- }
430
-
431
- // 4. 小于: <100, <-10 (不包括边界值,支持负数)
432
- const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
433
- if (ltMatch) {
434
- result.exclusiveMaximum = parseFloat(ltMatch[1]);
435
- return result;
436
- }
437
-
438
- // 5. 等于: =100, =-10 (支持负数)
439
- const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
440
- if (eqMatch) {
441
- result.enum = [parseFloat(eqMatch[1])];
442
- return result;
443
- }
444
- }
445
-
446
- // ========== 范围约束: min-max ==========
447
- if (constraint.includes('-')) {
448
- const [min, max] = constraint.split('-').map(v => v.trim());
449
-
450
- if (type === 'string') {
451
- if (min) result.minLength = parseInt(min);
452
- if (max) result.maxLength = parseInt(max);
453
- } else {
454
- if (min) result.minimum = parseFloat(min);
455
- if (max) result.maximum = parseFloat(max);
456
- }
457
- } else {
458
- // 单个值
459
- const value = constraint.trim();
460
- if (value) {
461
- if (type === 'string') {
462
- // 🔴 String单值 = 精确长度(常用于验证码、国家代码等)
463
- result.exactLength = parseInt(value);
464
- } else {
465
- // Number单值 = 最大值(符合直觉:不超过某值)
466
- result.maximum = parseFloat(value);
467
- }
468
- }
469
- }
470
- }
471
-
472
- return result;
473
- }
474
-
475
- /**
476
- * 检查是否为已知类型
477
- * @private
478
- */
479
- _isKnownType(type) {
480
- const knownTypes = [
481
- 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
482
- 'email', 'url', 'uuid', 'date', 'datetime', 'time', 'ipv4', 'ipv6',
483
- 'binary', 'objectId', 'hexColor', 'macAddress', 'cron', 'any',
484
- 'phone', 'idCard', 'creditCard', 'licensePlate', 'postalCode', 'passport',
485
- // v1.0.2 新增
486
- 'alphanum', 'lower', 'upper', 'json', 'port'
487
- ];
488
- return knownTypes.includes(type);
489
- }
490
-
491
- /**
492
- * 自动检测枚举类型
493
- * @private
494
- */
495
- _detectEnumType(enumValues) {
496
- const values = enumValues.split('|').map(v => v.trim());
497
-
498
- // 检查是否全部为布尔值
499
- const allBoolean = values.every(v => v === 'true' || v === 'false');
500
- if (allBoolean) return 'boolean';
501
-
502
- // 检查是否全部为数字
503
- const allNumber = values.every(v => !isNaN(parseFloat(v)) && isFinite(v));
504
- if (allNumber) return 'number';
505
-
506
- // 默认字符串
507
- return 'string';
508
- }
509
-
510
- /**
511
- * 解析枚举值
512
- * @private
513
- */
514
- _parseEnum(enumType, enumValues) {
515
- let values = enumValues.split('|').map(v => v.trim());
516
-
517
- // 类型转换
518
- if (enumType === 'boolean') {
519
- values = values.map(v => {
520
- if (v === 'true') return true;
521
- if (v === 'false') return false;
522
- throw new Error(`Invalid boolean enum value: ${v}. Must be 'true' or 'false'`);
523
- });
524
- return { type: 'boolean', enum: values };
525
- } else if (enumType === 'number') {
526
- values = values.map(v => {
527
- const num = parseFloat(v);
528
- if (isNaN(num)) throw new Error(`Invalid number enum value: ${v}`);
529
- return num;
530
- });
531
- return { type: 'number', enum: values };
532
- } else if (enumType === 'integer') {
533
- values = values.map(v => {
534
- const num = parseInt(v, 10);
535
- if (isNaN(num)) throw new Error(`Invalid integer enum value: ${v}`);
536
- return num;
537
- });
538
- return { type: 'integer', enum: values };
539
- } else {
540
- // 字符串枚举(默认)
541
- return { type: 'string', enum: values };
542
- }
543
- }
544
-
545
- /**
546
- * 添加正则表达式验证
547
- * @param {RegExp|string} regex - 正则表达式
548
- * @param {string} [message] - 自定义错误消息
549
- * @returns {DslBuilder}
550
- *
551
- * @example
552
- * dsl('string:3-32!')
553
- * .pattern(/^[a-zA-Z0-9_]+$/, '只能包含字母、数字和下划线')
554
- */
555
- pattern(regex, message) {
556
- this._baseSchema.pattern = regex instanceof RegExp ? regex.source : regex;
557
-
558
- if (message) {
559
- this._customMessages['string.pattern'] = message;
560
- }
561
-
562
- return this;
563
- }
564
-
565
- /**
566
- * 自定义错误消息
567
- * @param {Object} messages - 错误消息对象
568
- * @returns {DslBuilder}
569
- *
570
- * @example
571
- * dsl('string:3-32!')
572
- * .messages({
573
- * 'string.min': '至少{{#limit}}个字符',
574
- * 'string.max': '最多{{#limit}}个字符'
575
- * })
576
- */
577
- messages(messages) {
578
- Object.assign(this._customMessages, messages);
579
- return this;
580
- }
581
-
582
- /**
583
- * 设置字段标签(用于错误消息)
584
- * @param {string} labelText - 标签文本
585
- * @returns {DslBuilder}
586
- *
587
- * @example
588
- * dsl('email!').label('邮箱地址')
589
- */
590
- label(labelText) {
591
- this._label = labelText;
592
- return this;
593
- }
594
-
595
- /**
596
- * 添加自定义验证器
597
- * @param {Function} validatorFn - 验证函数
598
- * @returns {DslBuilder}
599
- *
600
- * 支持多种返回方式:
601
- * 1. 不返回/返回 undefined → 验证通过
602
- * 2. 返回 true → 验证通过
603
- * 3. 返回 false → 验证失败(使用默认消息)
604
- * 4. 返回字符串 → 验证失败(字符串作为错误消息)
605
- * 5. 返回对象 { error, message } → 验证失败(自定义错误)
606
- * 6. 抛出异常 → 验证失败(异常消息作为错误)
607
- *
608
- * @example
609
- * // 方式1: 不返回任何值(推荐)
610
- * .custom(async (value) => {
611
- * const exists = await checkEmailExists(value);
612
- * if (exists) return '邮箱已被占用';
613
- * })
614
- *
615
- * // 方式2: 返回错误消息字符串
616
- * .custom((value) => {
617
- * if (value.includes('admin')) return '不能包含敏感词';
618
- * })
619
- *
620
- * // 方式3: 返回错误对象
621
- * .custom(async (value) => {
622
- * const exists = await checkExists(value);
623
- * if (exists) {
624
- * return { error: 'email.exists', message: '邮箱已被占用' };
625
- * }
626
- * })
627
- *
628
- * // 方式4: 抛出异常
629
- * .custom(async (value) => {
630
- * const user = await findUser(value);
631
- * if (!user) throw new Error('用户不存在');
632
- * })
633
- */
634
- custom(validatorFn) {
635
- if (typeof validatorFn !== 'function') {
636
- throw new Error('Custom validator must be a function');
637
- }
638
- this._customValidators.push(validatorFn);
639
- return this;
640
- }
641
-
642
- /**
643
- * 设置描述
644
- * @param {string} text - 描述文本
645
- * @returns {DslBuilder}
646
- *
647
- * @example
648
- * dsl('string:3-32!').description('用户登录名')
649
- */
650
- description(text) {
651
- this._description = text;
652
- return this;
653
- }
654
-
655
-
656
- /**
657
- * 设置默认值
658
- * @param {*} value - 默认值
659
- * @returns {DslBuilder}
660
- */
661
- default(value) {
662
- this._baseSchema.default = value;
663
- return this;
664
- }
665
-
666
- /**
667
- * 转换为 JSON Schema
668
- * @returns {Object} JSON Schema对象
669
- */
670
- toSchema() {
671
- const schema = { ...this._baseSchema };
672
-
673
- // 添加描述
674
- if (this._description) {
675
- schema.description = this._description;
676
- }
677
-
678
- // 添加自定义消息
679
- if (Object.keys(this._customMessages).length > 0) {
680
- schema._customMessages = this._customMessages;
681
- }
682
-
683
- // 添加标签
684
- if (this._label) {
685
- schema._label = this._label;
686
- }
687
-
688
- // 添加自定义验证器
689
- if (this._customValidators.length > 0) {
690
- schema._customValidators = this._customValidators;
691
- }
692
-
693
- // 添加when条件
694
- if (this._whenConditions.length > 0) {
695
- schema._whenConditions = this._whenConditions;
696
- }
697
-
698
- // 添加必填标记
699
- schema._required = this._required;
700
-
701
- return schema;
702
- }
703
-
704
- /**
705
- * 验证数据
706
- * @param {*} data - 待验证数据
707
- * @param {Object} [context] - 验证上下文
708
- * @returns {Promise<Object>} 验证结果
709
- */
710
- async validate(data, context = {}) {
711
- const Validator = require('./Validator');
712
- const validator = new Validator();
713
- const schema = this.toSchema();
714
-
715
-
716
- return validator.validate(schema, data);
717
- }
718
-
719
- /**
720
- * 验证Schema嵌套深度
721
- * @static
722
- * @param {Object} schema - Schema对象
723
- * @param {number} maxDepth - 最大深度(默认3)
724
- * @returns {Object} { valid, depth, path, message }
725
- */
726
- static validateNestingDepth(schema, maxDepth = 3) {
727
- let maxFound = 0;
728
- let deepestPath = '';
729
-
730
- function traverse(obj, depth = 0, path = '', isRoot = false) {
731
- // 更新最大深度(仅当节点是容器时,即包含 properties 或 items)
732
- // 这样叶子节点(如 string 字段)不会增加嵌套深度
733
- if (!isRoot && (obj.properties || obj.items)) {
734
- if (depth > maxFound) {
735
- maxFound = depth;
736
- deepestPath = path;
737
- }
738
- }
739
-
740
- if (obj && typeof obj === 'object') {
741
- if (obj.properties) {
742
- const nextDepth = depth + 1;
743
- Object.keys(obj.properties).forEach(key => {
744
- traverse(obj.properties[key], nextDepth, `${path}.${key}`.replace(/^\./, ''), false);
745
- });
746
- }
747
- if (obj.items) {
748
- // 数组items不增加深度,或者根据需求增加
749
- // 这里保持原逻辑:数组本身算一层,items内部继续
750
- traverse(obj.items, depth, `${path}[]`, false);
751
- }
752
- }
753
- }
754
-
755
- traverse(schema, 0, '', true);
756
-
757
- return {
758
- valid: maxFound <= maxDepth,
759
- depth: maxFound,
760
- path: deepestPath,
761
- message: maxFound > maxDepth
762
- ? `嵌套深度${maxFound}超过限制${maxDepth},路径: ${deepestPath}`
763
- : `嵌套深度${maxFound}符合要求`
764
- };
765
- }
766
-
767
- // ========== 默认验证方法 ==========
768
-
769
- /**
770
- * 设置格式
771
- * @param {string} format - 格式名称 (email, url, uuid, etc.)
772
- * @returns {DslBuilder}
773
- */
774
- format(format) {
775
- this._baseSchema.format = format;
776
- return this;
777
- }
778
-
779
- /**
780
- * 手机号别名
781
- * @param {string} country
782
- * @returns {DslBuilder}
783
- */
784
- phoneNumber(country) {
785
- return this.phone(country);
786
- }
787
-
788
- /**
789
- * 身份证验证
790
- * @param {string} country - 国家代码 (目前仅支持 'cn')
791
- * @returns {DslBuilder}
792
- */
793
- idCard(country = 'cn') {
794
- if (country.toLowerCase() !== 'cn') {
795
- throw new Error(`Unsupported country for idCard: ${country}`);
796
- }
797
-
798
- // 中国身份证正则 (18位)
799
- const pattern = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
800
-
801
- // 自动设置长度
802
- if (!this._baseSchema.minLength) this._baseSchema.minLength = 18;
803
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 18;
804
-
805
- return this.pattern(pattern)
806
- .messages({
807
- 'pattern': 'pattern.idCard.cn'
808
- });
809
- }
810
-
811
- /**
812
- * URL Slug 验证
813
- * @returns {DslBuilder}
814
- */
815
- slug() {
816
- // 只能包含小写字母、数字和连字符,不能以连字符开头或结尾
817
- const pattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
818
-
819
- return this.pattern(pattern)
820
- .messages({
821
- 'pattern': 'pattern.slug'
822
- });
823
- }
824
-
825
- /**
826
- * 用户名验证(自动设置合理约束)
827
- * @param {string|Object} preset - 预设长度或选项
828
- * - '5-20' → 长度5-20
829
- * - 'short' → 3-16(短用户名)
830
- * - 'medium' → 3-32(中等,默认)
831
- * - 'long' → 3-64(长用户名)
832
- * - { minLength, maxLength, allowUnderscore, allowNumber }
833
- * @returns {DslBuilder}
834
- *
835
- * @example
836
- * // 简洁写法(推荐)
837
- * username: 'string!'.username() // 自动3-32
838
- * username: 'string!'.username('5-20') // 长度5-20
839
- * username: 'string!'.username('short') // 短用户名3-16
840
- * username: 'string!'.username('long') // 长用户名3-64
841
- */
842
- username(preset = 'medium') {
843
- let minLength, maxLength, allowUnderscore = true, allowNumber = true;
844
-
845
- // 解析预设
846
- if (typeof preset === 'string') {
847
- // 字符串范围格式:'5-20'
848
- const rangeMatch = preset.match(/^(\d+)-(\d+)$/);
849
- if (rangeMatch) {
850
- minLength = parseInt(rangeMatch[1], 10);
851
- maxLength = parseInt(rangeMatch[2], 10);
852
- }
853
- // 预设枚举
854
- else {
855
- const presets = {
856
- short: { min: 3, max: 16 },
857
- medium: { min: 3, max: 32 },
858
- long: { min: 3, max: 64 }
859
- };
860
- const p = presets[preset] || presets.medium;
861
- minLength = p.min;
862
- maxLength = p.max;
863
- }
864
- }
865
- // 对象参数
866
- else if (typeof preset === 'object') {
867
- minLength = preset.minLength || 3;
868
- maxLength = preset.maxLength || 32;
869
- allowUnderscore = preset.allowUnderscore !== false;
870
- allowNumber = preset.allowNumber !== false;
871
- }
872
-
873
- // 自动设置长度约束(如果未设置)
874
- if (!this._baseSchema.minLength) this._baseSchema.minLength = minLength;
875
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = maxLength;
876
-
877
- // 设置正则验证
878
- let pattern = '^[a-zA-Z]';
879
- if (allowUnderscore && allowNumber) {
880
- pattern += '[a-zA-Z0-9_]*$';
881
- } else if (allowNumber) {
882
- pattern += '[a-zA-Z0-9]*$';
883
- } else {
884
- pattern += '[a-zA-Z]*$';
885
- }
886
-
887
- return this.pattern(new RegExp(pattern))
888
- .messages({
889
- 'pattern': 'pattern.username'
890
- });
891
- }
892
-
893
- /**
894
- * 密码强度验证(自动设置合理约束)
895
- * @param {string} strength - 强度级别
896
- * @returns {DslBuilder}
897
- *
898
- * @example
899
- * password: 'string!'.password('strong') // 自动设置8-64长度
900
- */
901
- password(strength = 'medium') {
902
- const patterns = {
903
- weak: /.{6,}/,
904
- medium: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
905
- strong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
906
- veryStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{10,}$/
907
- };
908
-
909
- const minLengths = { weak: 6, medium: 8, strong: 8, veryStrong: 10 };
910
-
911
- const pattern = patterns[strength];
912
- if (!pattern) {
913
- throw new Error(`Invalid password strength: ${strength}`);
914
- }
915
-
916
- // 自动设置长度约束
917
- if (!this._baseSchema.minLength) this._baseSchema.minLength = minLengths[strength];
918
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 64;
919
-
920
- return this.pattern(pattern)
921
- .messages({
922
- 'pattern': `pattern.password.${strength}`
923
- });
924
- }
925
-
926
- /**
927
- * 手机号验证(自动设置合理约束)
928
- * @param {string} country - 国家代码: cn|us|uk|hk|tw|international
929
- * @returns {DslBuilder}
930
- *
931
- * @example
932
- * phone: 'string!'.phone('cn') // ✅ 推荐
933
- * phone: 'number!'.phone('cn') // ✅ 自动纠正为 string
934
- */
935
- phone(country = 'cn') {
936
- // ✨ 自动纠正类型为 string(手机号不应该是 number)
937
- if (this._baseSchema.type === 'number' || this._baseSchema.type === 'integer') {
938
- this._baseSchema.type = 'string';
939
- // 清理 number 类型的属性
940
- delete this._baseSchema.minimum;
941
- delete this._baseSchema.maximum;
942
- }
943
-
944
- const config = patterns.phone[country];
945
- if (!config) {
946
- throw new Error(`Unsupported country: ${country}`);
947
- }
948
-
949
- // 自动设置长度约束
950
- if (!this._baseSchema.minLength) this._baseSchema.minLength = config.min;
951
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = config.max;
952
-
953
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
954
- }
955
-
956
- /**
957
- * 信用卡验证
958
- * @param {string} type - 卡类型: visa|mastercard|amex|discover|jcb|unionpay
959
- * @returns {DslBuilder}
960
- */
961
- creditCard(type = 'visa') {
962
- const config = patterns.creditCard[type.toLowerCase()];
963
- if (!config) {
964
- throw new Error(`Unsupported credit card type: ${type}`);
965
- }
966
-
967
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
968
- }
969
-
970
- /**
971
- * 车牌号验证
972
- * @param {string} country - 国家代码
973
- * @returns {DslBuilder}
974
- */
975
- licensePlate(country = 'cn') {
976
- const config = patterns.licensePlate[country.toLowerCase()];
977
- if (!config) {
978
- throw new Error(`Unsupported country for licensePlate: ${country}`);
979
- }
980
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
981
- }
982
-
983
- /**
984
- * 邮政编码验证
985
- * @param {string} country - 国家代码
986
- * @returns {DslBuilder}
987
- */
988
- postalCode(country = 'cn') {
989
- const config = patterns.postalCode[country.toLowerCase()];
990
- if (!config) {
991
- throw new Error(`Unsupported country for postalCode: ${country}`);
992
- }
993
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
994
- }
995
-
996
- /**
997
- * 护照号码验证
998
- * @param {string} country - 国家代码
999
- * @returns {DslBuilder}
1000
- */
1001
- passport(country = 'cn') {
1002
- const config = patterns.passport[country.toLowerCase()];
1003
- if (!config) {
1004
- throw new Error(`Unsupported country for passport: ${country}`);
1005
- }
1006
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
1007
- }
1008
-
1009
- // ========== v1.0.2 新增验证器方法 ==========
1010
-
1011
- /**
1012
- * String 最小长度(使用AJV原生minLength)
1013
- * @param {number} n - 最小长度
1014
- * @returns {DslBuilder}
1015
- *
1016
- * @example
1017
- * dsl('string!').min(3) // 最少3个字符
1018
- */
1019
- min(n) {
1020
- if (this._baseSchema.type !== 'string') {
1021
- throw new Error('min() only applies to string type');
1022
- }
1023
- this._baseSchema.minLength = n;
1024
- return this;
1025
- }
1026
-
1027
- /**
1028
- * String 最大长度(使用AJV原生maxLength)
1029
- * @param {number} n - 最大长度
1030
- * @returns {DslBuilder}
1031
- *
1032
- * @example
1033
- * dsl('string!').max(32) // 最多32个字符
1034
- */
1035
- max(n) {
1036
- if (this._baseSchema.type !== 'string') {
1037
- throw new Error('max() only applies to string type');
1038
- }
1039
- this._baseSchema.maxLength = n;
1040
- return this;
1041
- }
1042
-
1043
- /**
1044
- * String 精确长度
1045
- * @param {number} n - 精确长度
1046
- * @returns {DslBuilder}
1047
- *
1048
- * @example
1049
- * dsl('string!').length(11) // 必须是11个字符
1050
- */
1051
- length(n) {
1052
- if (this._baseSchema.type !== 'string') {
1053
- throw new Error('length() only applies to string type');
1054
- }
1055
- this._baseSchema.exactLength = n;
1056
- return this;
1057
- }
1058
-
1059
- /**
1060
- * String 只能包含字母和数字
1061
- * @returns {DslBuilder}
1062
- *
1063
- * @example
1064
- * dsl('string!').alphanum() // 只能是字母和数字
1065
- */
1066
- alphanum() {
1067
- if (this._baseSchema.type !== 'string') {
1068
- throw new Error('alphanum() only applies to string type');
1069
- }
1070
- this._baseSchema.alphanum = true;
1071
- return this;
1072
- }
1073
-
1074
- /**
1075
- * String 不能包含前后空格
1076
- * @returns {DslBuilder}
1077
- *
1078
- * @example
1079
- * dsl('string!').trim() // 不能有前后空格
1080
- */
1081
- trim() {
1082
- if (this._baseSchema.type !== 'string') {
1083
- throw new Error('trim() only applies to string type');
1084
- }
1085
- this._baseSchema.trim = true;
1086
- return this;
1087
- }
1088
-
1089
- /**
1090
- * String 必须是小写
1091
- * @returns {DslBuilder}
1092
- *
1093
- * @example
1094
- * dsl('string!').lowercase() // 必须全小写
1095
- */
1096
- lowercase() {
1097
- if (this._baseSchema.type !== 'string') {
1098
- throw new Error('lowercase() only applies to string type');
1099
- }
1100
- this._baseSchema.lowercase = true;
1101
- return this;
1102
- }
1103
-
1104
- /**
1105
- * String 必须是大写
1106
- * @returns {DslBuilder}
1107
- *
1108
- * @example
1109
- * dsl('string!').uppercase() // 必须全大写
1110
- */
1111
- uppercase() {
1112
- if (this._baseSchema.type !== 'string') {
1113
- throw new Error('uppercase() only applies to string type');
1114
- }
1115
- this._baseSchema.uppercase = true;
1116
- return this;
1117
- }
1118
-
1119
- /**
1120
- * Number 小数位数限制
1121
- * @param {number} n - 最大小数位数
1122
- * @returns {DslBuilder}
1123
- *
1124
- * @example
1125
- * dsl('number!').precision(2) // 最多2位小数
1126
- */
1127
- precision(n) {
1128
- if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
1129
- throw new Error('precision() only applies to number type');
1130
- }
1131
- this._baseSchema.precision = n;
1132
- return this;
1133
- }
1134
-
1135
- /**
1136
- * Number 倍数验证(使用AJV原生multipleOf)
1137
- * @param {number} n - 必须是此数的倍数
1138
- * @returns {DslBuilder}
1139
- *
1140
- * @example
1141
- * dsl('number!').multiple(5) // 必须是5的倍数
1142
- */
1143
- multiple(n) {
1144
- if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
1145
- throw new Error('multiple() only applies to number type');
1146
- }
1147
- this._baseSchema.multipleOf = n;
1148
- return this;
1149
- }
1150
-
1151
- /**
1152
- * Number 端口号验证(1-65535)
1153
- * @returns {DslBuilder}
1154
- *
1155
- * @example
1156
- * dsl('integer!').port() // 必须是有效端口号
1157
- */
1158
- port() {
1159
- if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
1160
- throw new Error('port() only applies to number type');
1161
- }
1162
- this._baseSchema.port = true;
1163
- return this;
1164
- }
1165
-
1166
- /**
1167
- * Object 要求所有属性都必须存在
1168
- * @returns {DslBuilder}
1169
- *
1170
- * @example
1171
- * dsl({ name: 'string', age: 'number' }).requireAll()
1172
- */
1173
- requireAll() {
1174
- if (this._baseSchema.type !== 'object') {
1175
- throw new Error('requireAll() only applies to object type');
1176
- }
1177
- this._baseSchema.requiredAll = true;
1178
- return this;
1179
- }
1180
-
1181
- /**
1182
- * Object 严格模式,不允许额外属性
1183
- * @returns {DslBuilder}
1184
- *
1185
- * @example
1186
- * dsl({ name: 'string!' }).strict()
1187
- */
1188
- strict() {
1189
- if (this._baseSchema.type !== 'object') {
1190
- throw new Error('strict() only applies to object type');
1191
- }
1192
- this._baseSchema.strictSchema = true;
1193
- return this;
1194
- }
1195
-
1196
- /**
1197
- * Array 不允许稀疏数组
1198
- * @returns {DslBuilder}
1199
- *
1200
- * @example
1201
- * dsl('array<string>').noSparse()
1202
- */
1203
- noSparse() {
1204
- if (this._baseSchema.type !== 'array') {
1205
- throw new Error('noSparse() only applies to array type');
1206
- }
1207
- this._baseSchema.noSparse = true;
1208
- return this;
1209
- }
1210
-
1211
- /**
1212
- * Array 必须包含指定元素
1213
- * @param {Array} items - 必须包含的元素
1214
- * @returns {DslBuilder}
1215
- *
1216
- * @example
1217
- * dsl('array<string>').includesRequired(['admin', 'user'])
1218
- */
1219
- includesRequired(items) {
1220
- if (this._baseSchema.type !== 'array') {
1221
- throw new Error('includesRequired() only applies to array type');
1222
- }
1223
- if (!Array.isArray(items)) {
1224
- throw new Error('includesRequired() requires an array parameter');
1225
- }
1226
- this._baseSchema.includesRequired = items;
1227
- return this;
1228
- }
1229
-
1230
- /**
1231
- * Date 自定义日期格式验证
1232
- * @param {string} fmt - 日期格式(YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, DD/MM/YYYY, ISO8601)
1233
- * @returns {DslBuilder}
1234
- *
1235
- * @example
1236
- * dsl('string!').dateFormat('YYYY-MM-DD')
1237
- */
1238
- dateFormat(fmt) {
1239
- if (this._baseSchema.type !== 'string') {
1240
- throw new Error('dateFormat() only applies to string type');
1241
- }
1242
- this._baseSchema.dateFormat = fmt;
1243
- return this;
1244
- }
1245
-
1246
- /**
1247
- * Date 必须晚于指定日期
1248
- * @param {string} date - 比较日期
1249
- * @returns {DslBuilder}
1250
- *
1251
- * @example
1252
- * dsl('date!').after('2024-01-01')
1253
- */
1254
- after(date) {
1255
- if (this._baseSchema.type !== 'string') {
1256
- throw new Error('after() only applies to string type');
1257
- }
1258
- this._baseSchema.dateGreater = date;
1259
- return this;
1260
- }
1261
-
1262
- /**
1263
- * Date 必须早于指定日期
1264
- * @param {string} date - 比较日期
1265
- * @returns {DslBuilder}
1266
- *
1267
- * @example
1268
- * dsl('date!').before('2025-12-31')
1269
- */
1270
- before(date) {
1271
- if (this._baseSchema.type !== 'string') {
1272
- throw new Error('before() only applies to string type');
1273
- }
1274
- this._baseSchema.dateLess = date;
1275
- return this;
1276
- }
1277
-
1278
- /**
1279
- * Pattern 域名验证
1280
- * @returns {DslBuilder}
1281
- *
1282
- * @example
1283
- * dsl('string!').domain()
1284
- */
1285
- domain() {
1286
- if (this._baseSchema.type !== 'string') {
1287
- throw new Error('domain() only applies to string type');
1288
- }
1289
- const config = patterns.common.domain;
1290
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
1291
- }
1292
-
1293
- /**
1294
- * Pattern IP地址验证(IPv4或IPv6)
1295
- * @returns {DslBuilder}
1296
- *
1297
- * @example
1298
- * dsl('string!').ip()
1299
- */
1300
- ip() {
1301
- if (this._baseSchema.type !== 'string') {
1302
- throw new Error('ip() only applies to string type');
1303
- }
1304
- const config = patterns.common.ip;
1305
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
1306
- }
1307
-
1308
- /**
1309
- * Pattern Base64编码验证
1310
- * @returns {DslBuilder}
1311
- *
1312
- * @example
1313
- * dsl('string!').base64()
1314
- */
1315
- base64() {
1316
- if (this._baseSchema.type !== 'string') {
1317
- throw new Error('base64() only applies to string type');
1318
- }
1319
- const config = patterns.common.base64;
1320
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
1321
- }
1322
-
1323
- /**
1324
- * Pattern JWT令牌验证
1325
- * @returns {DslBuilder}
1326
- *
1327
- * @example
1328
- * dsl('string!').jwt()
1329
- */
1330
- jwt() {
1331
- if (this._baseSchema.type !== 'string') {
1332
- throw new Error('jwt() only applies to string type');
1333
- }
1334
- const config = patterns.common.jwt;
1335
- return this.pattern(config.pattern).messages({ 'pattern': config.key });
1336
- }
1337
-
1338
- /**
1339
- * Pattern JSON字符串验证
1340
- * @returns {DslBuilder}
1341
- *
1342
- * @example
1343
- * dsl('string!').json()
1344
- */
1345
- json() {
1346
- if (this._baseSchema.type !== 'string') {
1347
- throw new Error('json() only applies to string type');
1348
- }
1349
- this._baseSchema.jsonString = true;
1350
- return this;
1351
- }
1352
-
1353
- /**
1354
- * Pattern URL slug验证 (v1.0.3)
1355
- * URL slug只能包含小写字母、数字和连字符
1356
- * @returns {DslBuilder}
1357
- *
1358
- * @example
1359
- * dsl('string!').slug() // my-blog-post, hello-world-123
1360
- */
1361
- slug() {
1362
- if (this._baseSchema.type !== 'string') {
1363
- throw new Error('slug() only applies to string type');
1364
- }
1365
- this._baseSchema.pattern = '^[a-z0-9]+(?:-[a-z0-9]+)*$';
1366
- this._baseSchema._customMessages = this._baseSchema._customMessages || {};
1367
- this._baseSchema._customMessages['pattern'] = 'pattern.slug';
1368
- return this;
1369
- }
1370
-
1371
-
1372
- /**
1373
- * 日期大于验证 (v1.0.2)
1374
- * @param {string} date - 对比日期
1375
- * @returns {DslBuilder}
1376
- *
1377
- * @example
1378
- * dsl('string!').dateGreater('2025-01-01')
1379
- */
1380
- dateGreater(date) {
1381
- this._baseSchema.dateGreater = date;
1382
- return this;
1383
- }
1384
-
1385
- /**
1386
- * 日期小于验证 (v1.0.2)
1387
- * @param {string} date - 对比日期
1388
- * @returns {DslBuilder}
1389
- *
1390
- * @example
1391
- * dsl('string!').dateLess('2025-12-31')
1392
- */
1393
- dateLess(date) {
1394
- this._baseSchema.dateLess = date;
1395
- return this;
1396
- }
1397
- }
1398
-
1399
- module.exports = DslBuilder;
1400
-
1
+ /**
2
+ * DSL Builder - 统一的Schema构建器
3
+ *
4
+ * 支持链式调用扩展DSL功能
5
+ *
6
+ * @module lib/core/DslBuilder
7
+ * @version 2.0.0
8
+ *
9
+ * @example
10
+ * // 简单使用
11
+ * const schema = dsl('email!');
12
+ *
13
+ * // 链式扩展
14
+ * const schema = dsl('email!')
15
+ * .pattern(/custom/)
16
+ * .messages({ 'string.pattern': '格式不正确' })
17
+ * .label('邮箱地址');
18
+ */
19
+
20
+ const ErrorCodes = require("./ErrorCodes");
21
+ const MessageTemplate = require("./MessageTemplate");
22
+ const Locale = require("./Locale");
23
+ const patterns = require("../config/patterns");
24
+
25
+ class DslBuilder {
26
+ /**
27
+ * schema-dsl 自定义验证关键字集合(非 JSON Schema 标准字段)
28
+ *
29
+ * toJsonSchema() 使用此集合过滤非标准字段,确保输出纯净的 JSON Schema。
30
+ * @private
31
+ * @type {Set<string>}
32
+ */
33
+ static _internalKeys = new Set([
34
+ "exactLength",
35
+ "alphanum",
36
+ "lowercase",
37
+ "uppercase",
38
+ "trim",
39
+ "jsonString",
40
+ "port",
41
+ "requiredAll",
42
+ "strictSchema",
43
+ "noSparse",
44
+ "includesRequired",
45
+ "dateFormat",
46
+ "dateGreater",
47
+ "dateLess",
48
+ "precision",
49
+ "multipleOf",
50
+ ]);
51
+
52
+ /**
53
+ * 静态属性:存储用户自定义类型(插件注册)
54
+ * @private
55
+ * @type {Map<string, Object|Function>}
56
+ */
57
+ static _customTypes = new Map();
58
+
59
+ /**
60
+ * 注册自定义类型(供插件使用)
61
+ * @param {string} name - 类型名称
62
+ * @param {Object|Function} schema - JSON Schema对象 或 生成函数
63
+ * @throws {Error} 类型名称无效时抛出错误
64
+ *
65
+ * @example
66
+ * // 插件中注册自定义类型
67
+ * DslBuilder.registerType('phone-cn', {
68
+ * type: 'string',
69
+ * pattern: '^1[3-9]\\d{9}$'
70
+ * });
71
+ *
72
+ * // 在DSL中使用
73
+ * dsl('phone-cn!') // ✅ 可用
74
+ * dsl('types:string|phone-cn') // ✅ 可用
75
+ */
76
+ static registerType(name, schema) {
77
+ if (!name || typeof name !== "string") {
78
+ throw new Error("Type name must be a non-empty string");
79
+ }
80
+
81
+ if (
82
+ !schema ||
83
+ (typeof schema !== "object" && typeof schema !== "function")
84
+ ) {
85
+ throw new Error("Schema must be an object or function");
86
+ }
87
+
88
+ this._customTypes.set(name, schema);
89
+ }
90
+
91
+ /**
92
+ * 检查类型是否已注册(内置或自定义)
93
+ * @param {string} type - 类型名称
94
+ * @returns {boolean}
95
+ *
96
+ * @example
97
+ * DslBuilder.hasType('string') // true (内置)
98
+ * DslBuilder.hasType('phone-cn') // false (未注册)
99
+ *
100
+ * DslBuilder.registerType('phone-cn', { ... });
101
+ * DslBuilder.hasType('phone-cn') // true (已注册)
102
+ */
103
+ static hasType(type) {
104
+ // 检查自定义类型
105
+ if (this._customTypes.has(type)) {
106
+ return true;
107
+ }
108
+
109
+ // 检查内置类型
110
+ const builtInTypes = [
111
+ "string",
112
+ "number",
113
+ "integer",
114
+ "boolean",
115
+ "object",
116
+ "array",
117
+ "null",
118
+ "email",
119
+ "url",
120
+ "uuid",
121
+ "date",
122
+ "datetime",
123
+ "time",
124
+ "ipv4",
125
+ "ipv6",
126
+ "binary",
127
+ "objectId",
128
+ "hexColor",
129
+ "macAddress",
130
+ "cron",
131
+ "slug",
132
+ "alphanum",
133
+ "lower",
134
+ "upper",
135
+ "json",
136
+ "port",
137
+ "phone",
138
+ "idCard",
139
+ "creditCard",
140
+ "licensePlate",
141
+ "postalCode",
142
+ "passport",
143
+ "any",
144
+ ];
145
+
146
+ return builtInTypes.includes(type);
147
+ }
148
+
149
+ /**
150
+ * 获取所有已注册的自定义类型
151
+ * @returns {Array<string>}
152
+ */
153
+ static getCustomTypes() {
154
+ return Array.from(this._customTypes.keys());
155
+ }
156
+
157
+ /**
158
+ * 清除所有自定义类型(主要用于测试)
159
+ */
160
+ static clearCustomTypes() {
161
+ this._customTypes.clear();
162
+ }
163
+
164
+ /**
165
+ * 创建 DslBuilder 实例
166
+ * @param {string} dslString - DSL字符串,如 'string:3-32!' 或 'email!'
167
+ */
168
+ constructor(dslString) {
169
+ if (!dslString || typeof dslString !== "string") {
170
+ throw new Error("DSL string is required");
171
+ }
172
+
173
+ // 解析DSL字符串
174
+ const trimmed = dslString.trim();
175
+
176
+ // 特殊处理:array!数字 array:数字 + 必填
177
+ // 例如:array!1-10 → array:1-10!
178
+ let processedDsl = trimmed;
179
+ if (/^array![\d-]/.test(trimmed)) {
180
+ processedDsl = trimmed.replace(/^array!/, "array:") + "!";
181
+ }
182
+
183
+ // 🔴 处理必填标记 ! 和可选标记 ?
184
+ // 优先级:! > ?(如果同时存在,! 优先)
185
+ this._required = processedDsl.endsWith("!");
186
+ this._optional = processedDsl.endsWith("?") && !this._required;
187
+
188
+ let dslWithoutMarker = processedDsl;
189
+ if (this._required) {
190
+ dslWithoutMarker = processedDsl.slice(0, -1);
191
+ } else if (this._optional) {
192
+ dslWithoutMarker = processedDsl.slice(0, -1);
193
+ }
194
+
195
+ // 简单解析为基础Schema(避免循环依赖)
196
+ this._baseSchema = this._parseSimple(dslWithoutMarker);
197
+
198
+ // 扩展属性
199
+ this._customMessages = {};
200
+ this._label = null;
201
+ this._customValidators = [];
202
+ this._description = null;
203
+ this._whenConditions = [];
204
+ }
205
+
206
+ /**
207
+ * 简单解析DSL字符串(避免循环依赖)
208
+ * @private
209
+ * @param {string} dsl - DSL字符串(不含!)
210
+ * @returns {Object} JSON Schema对象
211
+ */
212
+ _parseSimple(dsl) {
213
+ // 🔴 处理跨类型联合:types:type1|type2|type3
214
+ if (dsl.startsWith("types:")) {
215
+ const typesStr = dsl.substring(6); // 去掉 'types:' 前缀
216
+ const types = typesStr
217
+ .split("|")
218
+ .map((t) => t.trim())
219
+ .filter((t) => t);
220
+
221
+ if (types.length === 0) {
222
+ throw new Error("types: requires at least one type");
223
+ }
224
+
225
+ if (types.length === 1) {
226
+ // 只有一个类型,直接解析(避免不必要的oneOf)
227
+ return this._parseSimple(types[0]);
228
+ }
229
+
230
+ // 多个类型,生成oneOf结构
231
+ return {
232
+ oneOf: types.map((type) => this._parseSimple(type)),
233
+ };
234
+ }
235
+
236
+ // 处理数组类型:array:1-10 或 array<string>
237
+ if (dsl.startsWith("array")) {
238
+ const schema = { type: "array" };
239
+
240
+ // 匹配:array:min-max<itemType> 或 array:constraint<itemType> 或 array<itemType>
241
+ const arrayMatch = dsl.match(/^array(?::([^<]+?))?(?:<(.+)>)?$/);
242
+
243
+ if (arrayMatch) {
244
+ const [, constraint, itemType] = arrayMatch;
245
+
246
+ // 解析约束
247
+ if (constraint) {
248
+ const trimmedConstraint = constraint.trim();
249
+
250
+ if (trimmedConstraint.includes("-")) {
251
+ // 范围约束: min-max, min-, -max
252
+ const [min, max] = trimmedConstraint
253
+ .split("-")
254
+ .map((v) => v.trim());
255
+ if (min) schema.minItems = parseInt(min, 10);
256
+ if (max) schema.maxItems = parseInt(max, 10);
257
+ } else {
258
+ // 单个值 = 最大值
259
+ schema.maxItems = parseInt(trimmedConstraint, 10);
260
+ }
261
+ }
262
+
263
+ // 解析元素类型
264
+ if (itemType) {
265
+ schema.items = this._parseSimple(itemType.trim());
266
+ }
267
+
268
+ return schema;
269
+ }
270
+ }
271
+
272
+ // 处理 enum: 前缀的枚举(支持逗号分隔和管道分隔)
273
+ // 例如: enum:a,b,c / enum:admin,user,guest / enum:number:1,2,3 / enum:a|b|c
274
+ if (dsl.startsWith("enum:")) {
275
+ const enumBody = dsl.slice(5); // 'a,b,c' 或 'number:1,2,3' 或 'a|b|c'
276
+
277
+ // 检查是否有类型前缀:enum:number:1,2,3
278
+ const colonIdx = enumBody.indexOf(":");
279
+ let enumType = "string";
280
+ let enumValues;
281
+
282
+ if (colonIdx !== -1) {
283
+ // enum:type:values
284
+ enumType = enumBody.slice(0, colonIdx);
285
+ enumValues = enumBody.slice(colonIdx + 1);
286
+ } else {
287
+ // enum:values (默认 string)
288
+ enumValues = enumBody;
289
+ }
290
+
291
+ // 统一分隔符:逗号 → 管道(_parseEnum 使用管道分隔)
292
+ const normalized = enumValues.includes("|")
293
+ ? enumValues
294
+ : enumValues.replace(/,/g, "|");
295
+ return this._parseEnum(enumType, normalized);
296
+ }
297
+
298
+ // 处理简写枚举(管道分隔,无 enum: 前缀)
299
+ // 例如: admin|user|guest / 1|2|3
300
+ if (dsl.includes("|")) {
301
+ let enumType = "string"; // 默认字符串
302
+ let enumValues = dsl;
303
+
304
+ if (dsl.includes(":") && !this._isKnownType(dsl.split(":")[0])) {
305
+ // 如果有冒号但不是已知类型(如 string:3-32),不作为枚举
306
+ // 让后续逻辑处理
307
+ } else {
308
+ // 简写形式:value1|value2
309
+ // 自动识别类型
310
+ enumType = this._detectEnumType(enumValues);
311
+ }
312
+
313
+ // 如果是枚举,解析值
314
+ if (enumValues.includes("|")) {
315
+ return this._parseEnum(enumType, enumValues);
316
+ }
317
+ }
318
+
319
+ // 处理类型:约束格式
320
+ const colonIndex = dsl.indexOf(":");
321
+ let type, constraint;
322
+
323
+ if (colonIndex === -1) {
324
+ type = dsl;
325
+ constraint = "";
326
+ } else {
327
+ type = dsl.substring(0, colonIndex);
328
+ constraint = dsl.substring(colonIndex + 1);
329
+ }
330
+
331
+ // 特殊处理 phone:country
332
+ if (type === "phone") {
333
+ const country = constraint || "cn";
334
+ const config = patterns.phone[country];
335
+ if (!config) throw new Error(`Unsupported country: ${country}`);
336
+ return {
337
+ type: "string",
338
+ pattern: config.pattern.source,
339
+ minLength: config.min,
340
+ maxLength: config.max,
341
+ _customMessages: { pattern: config.key || config.msg },
342
+ };
343
+ }
344
+
345
+ // 特殊处理 idCard:country
346
+ if (type === "idCard") {
347
+ const country = constraint || "cn";
348
+ const config = patterns.idCard[country.toLowerCase()];
349
+ if (!config)
350
+ throw new Error(`Unsupported country for idCard: ${country}`);
351
+ return {
352
+ type: "string",
353
+ pattern: config.pattern.source,
354
+ minLength: config.min,
355
+ maxLength: config.max,
356
+ _customMessages: { pattern: config.key || config.msg },
357
+ };
358
+ }
359
+
360
+ // 特殊处理 creditCard:type
361
+ if (type === "creditCard") {
362
+ const cardType = constraint || "visa";
363
+ const config = patterns.creditCard[cardType.toLowerCase()];
364
+ if (!config) throw new Error(`Unsupported credit card type: ${cardType}`);
365
+ return {
366
+ type: "string",
367
+ pattern: config.pattern.source,
368
+ _customMessages: { pattern: config.key || config.msg },
369
+ };
370
+ }
371
+
372
+ // 特殊处理 licensePlate:country
373
+ if (type === "licensePlate") {
374
+ const country = constraint || "cn";
375
+ const config = patterns.licensePlate[country.toLowerCase()];
376
+ if (!config)
377
+ throw new Error(`Unsupported country for licensePlate: ${country}`);
378
+ return {
379
+ type: "string",
380
+ pattern: config.pattern.source,
381
+ _customMessages: { pattern: config.key || config.msg },
382
+ };
383
+ }
384
+
385
+ // 特殊处理 postalCode:country
386
+ if (type === "postalCode") {
387
+ const country = constraint || "cn";
388
+ const config = patterns.postalCode[country.toLowerCase()];
389
+ if (!config)
390
+ throw new Error(`Unsupported country for postalCode: ${country}`);
391
+ return {
392
+ type: "string",
393
+ pattern: config.pattern.source,
394
+ _customMessages: { pattern: config.key || config.msg },
395
+ };
396
+ }
397
+
398
+ // 特殊处理 passport:country
399
+ if (type === "passport") {
400
+ const country = constraint || "cn";
401
+ const config = patterns.passport[country.toLowerCase()];
402
+ if (!config)
403
+ throw new Error(`Unsupported country for passport: ${country}`);
404
+ return {
405
+ type: "string",
406
+ pattern: config.pattern.source,
407
+ _customMessages: { pattern: config.key || config.msg },
408
+ };
409
+ }
410
+
411
+ // 获取基础类型
412
+ const schema = this._getBaseType(type);
413
+
414
+ // 处理约束
415
+ if (constraint) {
416
+ Object.assign(schema, this._parseConstraint(schema.type, constraint));
417
+ }
418
+
419
+ return schema;
420
+ }
421
+
422
+ /**
423
+ * 获取基础类型Schema
424
+ * @private
425
+ */
426
+ _getBaseType(type) {
427
+ // 🔴 优先查询自定义类型(插件注册的)
428
+ if (DslBuilder._customTypes.has(type)) {
429
+ const customSchema = DslBuilder._customTypes.get(type);
430
+ // 如果是函数,调用它生成Schema
431
+ if (typeof customSchema === "function") {
432
+ return customSchema();
433
+ }
434
+ // 否则返回Schema对象的深拷贝(避免污染)
435
+ return JSON.parse(JSON.stringify(customSchema));
436
+ }
437
+
438
+ // 🔴 查询内置类型
439
+ const typeMap = {
440
+ string: { type: "string" },
441
+ number: { type: "number" },
442
+ integer: { type: "integer" },
443
+ boolean: { type: "boolean" },
444
+ object: { type: "object" },
445
+ array: { type: "array" },
446
+ null: { type: "null" },
447
+ email: { type: "string", format: "email" },
448
+ url: { type: "string", format: "uri" },
449
+ uuid: { type: "string", format: "uuid" },
450
+ date: { type: "string", format: "date" },
451
+ datetime: { type: "string", format: "date-time" },
452
+ time: { type: "string", format: "time" },
453
+ ipv4: { type: "string", format: "ipv4" },
454
+ ipv6: { type: "string", format: "ipv6" },
455
+ binary: { type: "string", contentEncoding: "base64" },
456
+ objectId: {
457
+ type: "string",
458
+ pattern: "^[0-9a-fA-F]{24}$",
459
+ _customMessages: { pattern: "pattern.objectId" },
460
+ },
461
+ hexColor: {
462
+ type: "string",
463
+ pattern: "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",
464
+ _customMessages: { pattern: "pattern.hexColor" },
465
+ },
466
+ macAddress: {
467
+ type: "string",
468
+ pattern: "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$",
469
+ _customMessages: { pattern: "pattern.macAddress" },
470
+ },
471
+ cron: {
472
+ type: "string",
473
+ pattern:
474
+ "^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) (\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) (\\*|([0-6])|\\*\\/([0-6]))$",
475
+ _customMessages: { pattern: "pattern.cron" },
476
+ },
477
+ slug: {
478
+ type: "string",
479
+ pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
480
+ _customMessages: { pattern: "pattern.slug" },
481
+ },
482
+ any: {},
483
+ // v1.0.2 新增类型
484
+ alphanum: { type: "string", alphanum: true },
485
+ lower: { type: "string", lowercase: true },
486
+ upper: { type: "string", uppercase: true },
487
+ json: { type: "string", jsonString: true },
488
+ port: { type: "integer", port: true },
489
+ };
490
+
491
+ return typeMap[type] || { type: "string" };
492
+ }
493
+
494
+ /**
495
+ * 解析约束
496
+ * @private
497
+ *
498
+ * @example
499
+ * // 比较运算符 (v1.2.0+)
500
+ * _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
501
+ * _parseConstraint('number', '>=18') // { minimum: 18 }
502
+ * _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
503
+ * _parseConstraint('number', '<=100') // { maximum: 100 }
504
+ * _parseConstraint('number', '=100') // { enum: [100] }
505
+ */
506
+ _parseConstraint(type, constraint) {
507
+ const result = {};
508
+
509
+ if (type === "string" || type === "number" || type === "integer") {
510
+ // ========== 比较运算符(v1.1.2新增,仅number/integer,最高优先级)==========
511
+ if (type === "number" || type === "integer") {
512
+ // 1. 大于等于: >=18, >=-10 (支持负数)
513
+ const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
514
+ if (gteMatch) {
515
+ result.minimum = parseFloat(gteMatch[1]);
516
+ return result;
517
+ }
518
+
519
+ // 2. 小于等于: <=100, <=-10 (支持负数)
520
+ const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
521
+ if (lteMatch) {
522
+ result.maximum = parseFloat(lteMatch[1]);
523
+ return result;
524
+ }
525
+
526
+ // 3. 大于: >0, >-10 (不包括边界值,支持负数)
527
+ const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
528
+ if (gtMatch) {
529
+ result.exclusiveMinimum = parseFloat(gtMatch[1]);
530
+ return result;
531
+ }
532
+
533
+ // 4. 小于: <100, <-10 (不包括边界值,支持负数)
534
+ const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
535
+ if (ltMatch) {
536
+ result.exclusiveMaximum = parseFloat(ltMatch[1]);
537
+ return result;
538
+ }
539
+
540
+ // 5. 等于: =100, =-10 (支持负数)
541
+ const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
542
+ if (eqMatch) {
543
+ result.enum = [parseFloat(eqMatch[1])];
544
+ return result;
545
+ }
546
+ }
547
+
548
+ // ========== 范围约束: min-max ==========
549
+ if (constraint.includes("-")) {
550
+ const [min, max] = constraint.split("-").map((v) => v.trim());
551
+
552
+ if (type === "string") {
553
+ if (min) result.minLength = parseInt(min);
554
+ if (max) result.maxLength = parseInt(max);
555
+ } else {
556
+ if (min) result.minimum = parseFloat(min);
557
+ if (max) result.maximum = parseFloat(max);
558
+ }
559
+ } else {
560
+ // 单个值
561
+ const value = constraint.trim();
562
+ if (value) {
563
+ if (type === "string") {
564
+ // 🔴 String单值 = 精确长度(常用于验证码、国家代码等)
565
+ result.exactLength = parseInt(value);
566
+ } else {
567
+ // Number单值 = 最大值(符合直觉:不超过某值)
568
+ result.maximum = parseFloat(value);
569
+ }
570
+ }
571
+ }
572
+ }
573
+
574
+ return result;
575
+ }
576
+
577
+ /**
578
+ * 检查是否为已知类型
579
+ * @private
580
+ */
581
+ _isKnownType(type) {
582
+ const knownTypes = [
583
+ "string",
584
+ "number",
585
+ "integer",
586
+ "boolean",
587
+ "object",
588
+ "array",
589
+ "null",
590
+ "email",
591
+ "url",
592
+ "uuid",
593
+ "date",
594
+ "datetime",
595
+ "time",
596
+ "ipv4",
597
+ "ipv6",
598
+ "binary",
599
+ "objectId",
600
+ "hexColor",
601
+ "macAddress",
602
+ "cron",
603
+ "any",
604
+ "phone",
605
+ "idCard",
606
+ "creditCard",
607
+ "licensePlate",
608
+ "postalCode",
609
+ "passport",
610
+ // v1.0.2 新增
611
+ "alphanum",
612
+ "lower",
613
+ "upper",
614
+ "json",
615
+ "port",
616
+ ];
617
+ return knownTypes.includes(type);
618
+ }
619
+
620
+ /**
621
+ * 自动检测枚举类型
622
+ * @private
623
+ */
624
+ _detectEnumType(enumValues) {
625
+ const values = enumValues.split("|").map((v) => v.trim());
626
+
627
+ // 检查是否全部为布尔值
628
+ const allBoolean = values.every((v) => v === "true" || v === "false");
629
+ if (allBoolean) return "boolean";
630
+
631
+ // 检查是否全部为数字
632
+ const allNumber = values.every((v) => !isNaN(parseFloat(v)) && isFinite(v));
633
+ if (allNumber) return "number";
634
+
635
+ // 默认字符串
636
+ return "string";
637
+ }
638
+
639
+ /**
640
+ * 解析枚举值
641
+ * @private
642
+ */
643
+ _parseEnum(enumType, enumValues) {
644
+ let values = enumValues.split("|").map((v) => v.trim());
645
+
646
+ // 类型转换
647
+ if (enumType === "boolean") {
648
+ values = values.map((v) => {
649
+ if (v === "true") return true;
650
+ if (v === "false") return false;
651
+ throw new Error(
652
+ `Invalid boolean enum value: ${v}. Must be 'true' or 'false'`,
653
+ );
654
+ });
655
+ return { type: "boolean", enum: values };
656
+ } else if (enumType === "number") {
657
+ values = values.map((v) => {
658
+ const num = parseFloat(v);
659
+ if (isNaN(num)) throw new Error(`Invalid number enum value: ${v}`);
660
+ return num;
661
+ });
662
+ return { type: "number", enum: values };
663
+ } else if (enumType === "integer") {
664
+ values = values.map((v) => {
665
+ const num = parseInt(v, 10);
666
+ if (isNaN(num)) throw new Error(`Invalid integer enum value: ${v}`);
667
+ return num;
668
+ });
669
+ return { type: "integer", enum: values };
670
+ } else {
671
+ // 字符串枚举(默认)
672
+ return { type: "string", enum: values };
673
+ }
674
+ }
675
+
676
+ /**
677
+ * 添加正则表达式验证
678
+ * @param {RegExp|string} regex - 正则表达式
679
+ * @param {string} [message] - 自定义错误消息
680
+ * @returns {DslBuilder}
681
+ *
682
+ * @example
683
+ * dsl('string:3-32!')
684
+ * .pattern(/^[a-zA-Z0-9_]+$/, '只能包含字母、数字和下划线')
685
+ */
686
+ pattern(regex, message) {
687
+ this._baseSchema.pattern = regex instanceof RegExp ? regex.source : regex;
688
+
689
+ if (message) {
690
+ this._customMessages["string.pattern"] = message;
691
+ }
692
+
693
+ return this;
694
+ }
695
+
696
+ /**
697
+ * 自定义错误消息
698
+ * @param {Object} messages - 错误消息对象
699
+ * @returns {DslBuilder}
700
+ *
701
+ * @example
702
+ * dsl('string:3-32!')
703
+ * .messages({
704
+ * 'string.min': '至少{{#limit}}个字符',
705
+ * 'string.max': '最多{{#limit}}个字符'
706
+ * })
707
+ */
708
+ messages(messages) {
709
+ Object.assign(this._customMessages, messages);
710
+ return this;
711
+ }
712
+
713
+ /**
714
+ * 设置字段标签(用于错误消息)
715
+ * @param {string} labelText - 标签文本
716
+ * @returns {DslBuilder}
717
+ *
718
+ * @example
719
+ * dsl('email!').label('邮箱地址')
720
+ */
721
+ label(labelText) {
722
+ this._label = labelText;
723
+ return this;
724
+ }
725
+
726
+ /**
727
+ * 添加自定义验证器
728
+ * @param {Function} validatorFn - 验证函数
729
+ * @returns {DslBuilder}
730
+ *
731
+ * 支持多种返回方式:
732
+ * 1. 不返回/返回 undefined → 验证通过
733
+ * 2. 返回 true 验证通过
734
+ * 3. 返回 false → 验证失败(使用默认消息)
735
+ * 4. 返回字符串 → 验证失败(字符串作为错误消息)
736
+ * 5. 返回对象 { error, message } → 验证失败(自定义错误)
737
+ * 6. 抛出异常 → 验证失败(异常消息作为错误)
738
+ *
739
+ * @example
740
+ * // 方式1: 不返回任何值(推荐)
741
+ * .custom(async (value) => {
742
+ * const exists = await checkEmailExists(value);
743
+ * if (exists) return '邮箱已被占用';
744
+ * })
745
+ *
746
+ * // 方式2: 返回错误消息字符串
747
+ * .custom((value) => {
748
+ * if (value.includes('admin')) return '不能包含敏感词';
749
+ * })
750
+ *
751
+ * // 方式3: 返回错误对象
752
+ * .custom(async (value) => {
753
+ * const exists = await checkExists(value);
754
+ * if (exists) {
755
+ * return { error: 'email.exists', message: '邮箱已被占用' };
756
+ * }
757
+ * })
758
+ *
759
+ * // 方式4: 抛出异常
760
+ * .custom(async (value) => {
761
+ * const user = await findUser(value);
762
+ * if (!user) throw new Error('用户不存在');
763
+ * })
764
+ */
765
+ custom(validatorFn) {
766
+ if (typeof validatorFn !== "function") {
767
+ throw new Error("Custom validator must be a function");
768
+ }
769
+ this._customValidators.push(validatorFn);
770
+ return this;
771
+ }
772
+
773
+ /**
774
+ * 设置描述
775
+ * @param {string} text - 描述文本
776
+ * @returns {DslBuilder}
777
+ *
778
+ * @example
779
+ * dsl('string:3-32!').description('用户登录名')
780
+ */
781
+ description(text) {
782
+ this._description = text;
783
+ return this;
784
+ }
785
+
786
+ /**
787
+ * 设置默认值
788
+ * @param {*} value - 默认值
789
+ * @returns {DslBuilder}
790
+ */
791
+ default(value) {
792
+ this._baseSchema.default = value;
793
+ return this;
794
+ }
795
+
796
+ /**
797
+ * 转换为 JSON Schema
798
+ * @returns {Object} JSON Schema对象
799
+ */
800
+ toSchema() {
801
+ const schema = { ...this._baseSchema };
802
+
803
+ // 添加描述
804
+ if (this._description) {
805
+ schema.description = this._description;
806
+ }
807
+
808
+ // 添加自定义消息
809
+ if (Object.keys(this._customMessages).length > 0) {
810
+ schema._customMessages = this._customMessages;
811
+ }
812
+
813
+ // 添加标签
814
+ if (this._label) {
815
+ schema._label = this._label;
816
+ }
817
+
818
+ // 添加自定义验证器
819
+ if (this._customValidators.length > 0) {
820
+ schema._customValidators = this._customValidators;
821
+ }
822
+
823
+ // 添加when条件
824
+ if (this._whenConditions.length > 0) {
825
+ schema._whenConditions = this._whenConditions;
826
+ }
827
+
828
+ // 添加必填标记
829
+ schema._required = this._required;
830
+
831
+ return schema;
832
+ }
833
+
834
+ /**
835
+ * 输出纯净的 JSON Schema(无内部标记字段)
836
+ *
837
+ * toSchema() 不同,toJsonSchema() 会自动清理所有 schema-dsl 内部标记:
838
+ * - 下划线前缀字段:_required / _customMessages / _label / _customValidators / _whenConditions
839
+ * - 自定义验证关键字:exactLength / alphanum / lowercase / uppercase / trim / jsonString /
840
+ * port / requiredAll / strictSchema / noSparse / includesRequired / dateFormat /
841
+ * dateGreater / dateLess / precision / multipleOf
842
+ *
843
+ * 返回的对象可直接嵌入 OpenAPI / JSON Schema 等标准文档中,无需下游再做清理。
844
+ *
845
+ * @returns {Object} 纯净的 JSON Schema 对象(符合 JSON Schema 标准)
846
+ *
847
+ * @example
848
+ * const builder = new DslBuilder('string:3-32!');
849
+ * builder.toSchema(); // { type: 'string', minLength: 3, maxLength: 32, _required: true }
850
+ * builder.toJsonSchema(); // { type: 'string', minLength: 3, maxLength: 32 }
851
+ *
852
+ * @example
853
+ * const builder = new DslBuilder('email!');
854
+ * builder.messages({ format: '邮箱格式不正确' });
855
+ * builder.toSchema(); // { type: 'string', format: 'email', _required: true, _customMessages: { format: '...' } }
856
+ * builder.toJsonSchema(); // { type: 'string', format: 'email' }
857
+ *
858
+ * @since v1.2.5
859
+ */
860
+ toJsonSchema() {
861
+ const raw = this.toSchema();
862
+ const cleaned = {};
863
+
864
+ for (const key of Object.keys(raw)) {
865
+ // 跳过下划线前缀的内部字段
866
+ if (key.startsWith("_")) continue;
867
+
868
+ // 跳过 schema-dsl 自定义验证关键字(非 JSON Schema 标准)
869
+ if (DslBuilder._internalKeys.has(key)) continue;
870
+
871
+ cleaned[key] = raw[key];
872
+ }
873
+
874
+ return cleaned;
875
+ }
876
+
877
+ /**
878
+ * 验证数据
879
+ * @param {*} data - 待验证数据
880
+ * @param {Object} [context] - 验证上下文
881
+ * @returns {Promise<Object>} 验证结果
882
+ */
883
+ async validate(data, context = {}) {
884
+ const Validator = require("./Validator");
885
+ const validator = new Validator();
886
+ const schema = this.toSchema();
887
+
888
+ return validator.validate(schema, data);
889
+ }
890
+
891
+ /**
892
+ * 验证Schema嵌套深度
893
+ * @static
894
+ * @param {Object} schema - Schema对象
895
+ * @param {number} maxDepth - 最大深度(默认3)
896
+ * @returns {Object} { valid, depth, path, message }
897
+ */
898
+ static validateNestingDepth(schema, maxDepth = 3) {
899
+ let maxFound = 0;
900
+ let deepestPath = "";
901
+
902
+ function traverse(obj, depth = 0, path = "", isRoot = false) {
903
+ // 更新最大深度(仅当节点是容器时,即包含 properties 或 items)
904
+ // 这样叶子节点(如 string 字段)不会增加嵌套深度
905
+ if (!isRoot && (obj.properties || obj.items)) {
906
+ if (depth > maxFound) {
907
+ maxFound = depth;
908
+ deepestPath = path;
909
+ }
910
+ }
911
+
912
+ if (obj && typeof obj === "object") {
913
+ if (obj.properties) {
914
+ const nextDepth = depth + 1;
915
+ Object.keys(obj.properties).forEach((key) => {
916
+ traverse(
917
+ obj.properties[key],
918
+ nextDepth,
919
+ `${path}.${key}`.replace(/^\./, ""),
920
+ false,
921
+ );
922
+ });
923
+ }
924
+ if (obj.items) {
925
+ // 数组items不增加深度,或者根据需求增加
926
+ // 这里保持原逻辑:数组本身算一层,items内部继续
927
+ traverse(obj.items, depth, `${path}[]`, false);
928
+ }
929
+ }
930
+ }
931
+
932
+ traverse(schema, 0, "", true);
933
+
934
+ return {
935
+ valid: maxFound <= maxDepth,
936
+ depth: maxFound,
937
+ path: deepestPath,
938
+ message:
939
+ maxFound > maxDepth
940
+ ? `嵌套深度${maxFound}超过限制${maxDepth},路径: ${deepestPath}`
941
+ : `嵌套深度${maxFound}符合要求`,
942
+ };
943
+ }
944
+
945
+ // ========== 默认验证方法 ==========
946
+
947
+ /**
948
+ * 设置格式
949
+ * @param {string} format - 格式名称 (email, url, uuid, etc.)
950
+ * @returns {DslBuilder}
951
+ */
952
+ format(format) {
953
+ this._baseSchema.format = format;
954
+ return this;
955
+ }
956
+
957
+ /**
958
+ * 手机号别名
959
+ * @param {string} country
960
+ * @returns {DslBuilder}
961
+ */
962
+ phoneNumber(country) {
963
+ return this.phone(country);
964
+ }
965
+
966
+ /**
967
+ * 身份证验证
968
+ * @param {string} country - 国家代码 (目前仅支持 'cn')
969
+ * @returns {DslBuilder}
970
+ */
971
+ idCard(country = "cn") {
972
+ if (country.toLowerCase() !== "cn") {
973
+ throw new Error(`Unsupported country for idCard: ${country}`);
974
+ }
975
+
976
+ // 中国身份证正则 (18位)
977
+ const pattern =
978
+ /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
979
+
980
+ // 自动设置长度
981
+ if (!this._baseSchema.minLength) this._baseSchema.minLength = 18;
982
+ if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 18;
983
+
984
+ return this.pattern(pattern).messages({
985
+ pattern: "pattern.idCard.cn",
986
+ });
987
+ }
988
+
989
+ /**
990
+ * URL Slug 验证
991
+ * @returns {DslBuilder}
992
+ */
993
+ slug() {
994
+ // 只能包含小写字母、数字和连字符,不能以连字符开头或结尾
995
+ const pattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
996
+
997
+ return this.pattern(pattern).messages({
998
+ pattern: "pattern.slug",
999
+ });
1000
+ }
1001
+
1002
+ /**
1003
+ * 用户名验证(自动设置合理约束)
1004
+ * @param {string|Object} preset - 预设长度或选项
1005
+ * - '5-20' → 长度5-20
1006
+ * - 'short' 3-16(短用户名)
1007
+ * - 'medium' → 3-32(中等,默认)
1008
+ * - 'long' → 3-64(长用户名)
1009
+ * - { minLength, maxLength, allowUnderscore, allowNumber }
1010
+ * @returns {DslBuilder}
1011
+ *
1012
+ * @example
1013
+ * // 简洁写法(推荐)
1014
+ * username: 'string!'.username() // 自动3-32
1015
+ * username: 'string!'.username('5-20') // 长度5-20
1016
+ * username: 'string!'.username('short') // 短用户名3-16
1017
+ * username: 'string!'.username('long') // 长用户名3-64
1018
+ */
1019
+ username(preset = "medium") {
1020
+ let minLength,
1021
+ maxLength,
1022
+ allowUnderscore = true,
1023
+ allowNumber = true;
1024
+
1025
+ // 解析预设
1026
+ if (typeof preset === "string") {
1027
+ // 字符串范围格式:'5-20'
1028
+ const rangeMatch = preset.match(/^(\d+)-(\d+)$/);
1029
+ if (rangeMatch) {
1030
+ minLength = parseInt(rangeMatch[1], 10);
1031
+ maxLength = parseInt(rangeMatch[2], 10);
1032
+ }
1033
+ // 预设枚举
1034
+ else {
1035
+ const presets = {
1036
+ short: { min: 3, max: 16 },
1037
+ medium: { min: 3, max: 32 },
1038
+ long: { min: 3, max: 64 },
1039
+ };
1040
+ const p = presets[preset] || presets.medium;
1041
+ minLength = p.min;
1042
+ maxLength = p.max;
1043
+ }
1044
+ }
1045
+ // 对象参数
1046
+ else if (typeof preset === "object") {
1047
+ minLength = preset.minLength || 3;
1048
+ maxLength = preset.maxLength || 32;
1049
+ allowUnderscore = preset.allowUnderscore !== false;
1050
+ allowNumber = preset.allowNumber !== false;
1051
+ }
1052
+
1053
+ // 自动设置长度约束(如果未设置)
1054
+ if (!this._baseSchema.minLength) this._baseSchema.minLength = minLength;
1055
+ if (!this._baseSchema.maxLength) this._baseSchema.maxLength = maxLength;
1056
+
1057
+ // 设置正则验证
1058
+ let pattern = "^[a-zA-Z]";
1059
+ if (allowUnderscore && allowNumber) {
1060
+ pattern += "[a-zA-Z0-9_]*$";
1061
+ } else if (allowNumber) {
1062
+ pattern += "[a-zA-Z0-9]*$";
1063
+ } else {
1064
+ pattern += "[a-zA-Z]*$";
1065
+ }
1066
+
1067
+ return this.pattern(new RegExp(pattern)).messages({
1068
+ pattern: "pattern.username",
1069
+ });
1070
+ }
1071
+
1072
+ /**
1073
+ * 密码强度验证(自动设置合理约束)
1074
+ * @param {string} strength - 强度级别
1075
+ * @returns {DslBuilder}
1076
+ *
1077
+ * @example
1078
+ * password: 'string!'.password('strong') // 自动设置8-64长度
1079
+ */
1080
+ password(strength = "medium") {
1081
+ const patterns = {
1082
+ weak: /.{6,}/,
1083
+ medium: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
1084
+ strong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
1085
+ veryStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{10,}$/,
1086
+ };
1087
+
1088
+ const minLengths = { weak: 6, medium: 8, strong: 8, veryStrong: 10 };
1089
+
1090
+ const pattern = patterns[strength];
1091
+ if (!pattern) {
1092
+ throw new Error(`Invalid password strength: ${strength}`);
1093
+ }
1094
+
1095
+ // 自动设置长度约束
1096
+ if (!this._baseSchema.minLength)
1097
+ this._baseSchema.minLength = minLengths[strength];
1098
+ if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 64;
1099
+
1100
+ return this.pattern(pattern).messages({
1101
+ pattern: `pattern.password.${strength}`,
1102
+ });
1103
+ }
1104
+
1105
+ /**
1106
+ * 手机号验证(自动设置合理约束)
1107
+ * @param {string} country - 国家代码: cn|us|uk|hk|tw|international
1108
+ * @returns {DslBuilder}
1109
+ *
1110
+ * @example
1111
+ * phone: 'string!'.phone('cn') // ✅ 推荐
1112
+ * phone: 'number!'.phone('cn') // ✅ 自动纠正为 string
1113
+ */
1114
+ phone(country = "cn") {
1115
+ // 自动纠正类型为 string(手机号不应该是 number)
1116
+ if (
1117
+ this._baseSchema.type === "number" ||
1118
+ this._baseSchema.type === "integer"
1119
+ ) {
1120
+ this._baseSchema.type = "string";
1121
+ // 清理 number 类型的属性
1122
+ delete this._baseSchema.minimum;
1123
+ delete this._baseSchema.maximum;
1124
+ }
1125
+
1126
+ const config = patterns.phone[country];
1127
+ if (!config) {
1128
+ throw new Error(`Unsupported country: ${country}`);
1129
+ }
1130
+
1131
+ // 自动设置长度约束
1132
+ if (!this._baseSchema.minLength) this._baseSchema.minLength = config.min;
1133
+ if (!this._baseSchema.maxLength) this._baseSchema.maxLength = config.max;
1134
+
1135
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1136
+ }
1137
+
1138
+ /**
1139
+ * 信用卡验证
1140
+ * @param {string} type - 卡类型: visa|mastercard|amex|discover|jcb|unionpay
1141
+ * @returns {DslBuilder}
1142
+ */
1143
+ creditCard(type = "visa") {
1144
+ const config = patterns.creditCard[type.toLowerCase()];
1145
+ if (!config) {
1146
+ throw new Error(`Unsupported credit card type: ${type}`);
1147
+ }
1148
+
1149
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1150
+ }
1151
+
1152
+ /**
1153
+ * 车牌号验证
1154
+ * @param {string} country - 国家代码
1155
+ * @returns {DslBuilder}
1156
+ */
1157
+ licensePlate(country = "cn") {
1158
+ const config = patterns.licensePlate[country.toLowerCase()];
1159
+ if (!config) {
1160
+ throw new Error(`Unsupported country for licensePlate: ${country}`);
1161
+ }
1162
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1163
+ }
1164
+
1165
+ /**
1166
+ * 邮政编码验证
1167
+ * @param {string} country - 国家代码
1168
+ * @returns {DslBuilder}
1169
+ */
1170
+ postalCode(country = "cn") {
1171
+ const config = patterns.postalCode[country.toLowerCase()];
1172
+ if (!config) {
1173
+ throw new Error(`Unsupported country for postalCode: ${country}`);
1174
+ }
1175
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1176
+ }
1177
+
1178
+ /**
1179
+ * 护照号码验证
1180
+ * @param {string} country - 国家代码
1181
+ * @returns {DslBuilder}
1182
+ */
1183
+ passport(country = "cn") {
1184
+ const config = patterns.passport[country.toLowerCase()];
1185
+ if (!config) {
1186
+ throw new Error(`Unsupported country for passport: ${country}`);
1187
+ }
1188
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1189
+ }
1190
+
1191
+ // ========== v1.0.2 新增验证器方法 ==========
1192
+
1193
+ /**
1194
+ * String 最小长度(使用AJV原生minLength)
1195
+ * @param {number} n - 最小长度
1196
+ * @returns {DslBuilder}
1197
+ *
1198
+ * @example
1199
+ * dsl('string!').min(3) // 最少3个字符
1200
+ */
1201
+ min(n) {
1202
+ if (this._baseSchema.type !== "string") {
1203
+ throw new Error("min() only applies to string type");
1204
+ }
1205
+ this._baseSchema.minLength = n;
1206
+ return this;
1207
+ }
1208
+
1209
+ /**
1210
+ * String 最大长度(使用AJV原生maxLength)
1211
+ * @param {number} n - 最大长度
1212
+ * @returns {DslBuilder}
1213
+ *
1214
+ * @example
1215
+ * dsl('string!').max(32) // 最多32个字符
1216
+ */
1217
+ max(n) {
1218
+ if (this._baseSchema.type !== "string") {
1219
+ throw new Error("max() only applies to string type");
1220
+ }
1221
+ this._baseSchema.maxLength = n;
1222
+ return this;
1223
+ }
1224
+
1225
+ /**
1226
+ * String 精确长度
1227
+ * @param {number} n - 精确长度
1228
+ * @returns {DslBuilder}
1229
+ *
1230
+ * @example
1231
+ * dsl('string!').length(11) // 必须是11个字符
1232
+ */
1233
+ length(n) {
1234
+ if (this._baseSchema.type !== "string") {
1235
+ throw new Error("length() only applies to string type");
1236
+ }
1237
+ this._baseSchema.exactLength = n;
1238
+ return this;
1239
+ }
1240
+
1241
+ /**
1242
+ * String 只能包含字母和数字
1243
+ * @returns {DslBuilder}
1244
+ *
1245
+ * @example
1246
+ * dsl('string!').alphanum() // 只能是字母和数字
1247
+ */
1248
+ alphanum() {
1249
+ if (this._baseSchema.type !== "string") {
1250
+ throw new Error("alphanum() only applies to string type");
1251
+ }
1252
+ this._baseSchema.alphanum = true;
1253
+ return this;
1254
+ }
1255
+
1256
+ /**
1257
+ * String 不能包含前后空格
1258
+ * @returns {DslBuilder}
1259
+ *
1260
+ * @example
1261
+ * dsl('string!').trim() // 不能有前后空格
1262
+ */
1263
+ trim() {
1264
+ if (this._baseSchema.type !== "string") {
1265
+ throw new Error("trim() only applies to string type");
1266
+ }
1267
+ this._baseSchema.trim = true;
1268
+ return this;
1269
+ }
1270
+
1271
+ /**
1272
+ * String 必须是小写
1273
+ * @returns {DslBuilder}
1274
+ *
1275
+ * @example
1276
+ * dsl('string!').lowercase() // 必须全小写
1277
+ */
1278
+ lowercase() {
1279
+ if (this._baseSchema.type !== "string") {
1280
+ throw new Error("lowercase() only applies to string type");
1281
+ }
1282
+ this._baseSchema.lowercase = true;
1283
+ return this;
1284
+ }
1285
+
1286
+ /**
1287
+ * String 必须是大写
1288
+ * @returns {DslBuilder}
1289
+ *
1290
+ * @example
1291
+ * dsl('string!').uppercase() // 必须全大写
1292
+ */
1293
+ uppercase() {
1294
+ if (this._baseSchema.type !== "string") {
1295
+ throw new Error("uppercase() only applies to string type");
1296
+ }
1297
+ this._baseSchema.uppercase = true;
1298
+ return this;
1299
+ }
1300
+
1301
+ /**
1302
+ * Number 小数位数限制
1303
+ * @param {number} n - 最大小数位数
1304
+ * @returns {DslBuilder}
1305
+ *
1306
+ * @example
1307
+ * dsl('number!').precision(2) // 最多2位小数
1308
+ */
1309
+ precision(n) {
1310
+ if (
1311
+ this._baseSchema.type !== "number" &&
1312
+ this._baseSchema.type !== "integer"
1313
+ ) {
1314
+ throw new Error("precision() only applies to number type");
1315
+ }
1316
+ this._baseSchema.precision = n;
1317
+ return this;
1318
+ }
1319
+
1320
+ /**
1321
+ * Number 倍数验证(使用AJV原生multipleOf)
1322
+ * @param {number} n - 必须是此数的倍数
1323
+ * @returns {DslBuilder}
1324
+ *
1325
+ * @example
1326
+ * dsl('number!').multiple(5) // 必须是5的倍数
1327
+ */
1328
+ multiple(n) {
1329
+ if (
1330
+ this._baseSchema.type !== "number" &&
1331
+ this._baseSchema.type !== "integer"
1332
+ ) {
1333
+ throw new Error("multiple() only applies to number type");
1334
+ }
1335
+ this._baseSchema.multipleOf = n;
1336
+ return this;
1337
+ }
1338
+
1339
+ /**
1340
+ * Number 端口号验证(1-65535)
1341
+ * @returns {DslBuilder}
1342
+ *
1343
+ * @example
1344
+ * dsl('integer!').port() // 必须是有效端口号
1345
+ */
1346
+ port() {
1347
+ if (
1348
+ this._baseSchema.type !== "number" &&
1349
+ this._baseSchema.type !== "integer"
1350
+ ) {
1351
+ throw new Error("port() only applies to number type");
1352
+ }
1353
+ this._baseSchema.port = true;
1354
+ return this;
1355
+ }
1356
+
1357
+ /**
1358
+ * Object 要求所有属性都必须存在
1359
+ * @returns {DslBuilder}
1360
+ *
1361
+ * @example
1362
+ * dsl({ name: 'string', age: 'number' }).requireAll()
1363
+ */
1364
+ requireAll() {
1365
+ if (this._baseSchema.type !== "object") {
1366
+ throw new Error("requireAll() only applies to object type");
1367
+ }
1368
+ this._baseSchema.requiredAll = true;
1369
+ return this;
1370
+ }
1371
+
1372
+ /**
1373
+ * Object 严格模式,不允许额外属性
1374
+ * @returns {DslBuilder}
1375
+ *
1376
+ * @example
1377
+ * dsl({ name: 'string!' }).strict()
1378
+ */
1379
+ strict() {
1380
+ if (this._baseSchema.type !== "object") {
1381
+ throw new Error("strict() only applies to object type");
1382
+ }
1383
+ this._baseSchema.strictSchema = true;
1384
+ return this;
1385
+ }
1386
+
1387
+ /**
1388
+ * Array 不允许稀疏数组
1389
+ * @returns {DslBuilder}
1390
+ *
1391
+ * @example
1392
+ * dsl('array<string>').noSparse()
1393
+ */
1394
+ noSparse() {
1395
+ if (this._baseSchema.type !== "array") {
1396
+ throw new Error("noSparse() only applies to array type");
1397
+ }
1398
+ this._baseSchema.noSparse = true;
1399
+ return this;
1400
+ }
1401
+
1402
+ /**
1403
+ * Array 必须包含指定元素
1404
+ * @param {Array} items - 必须包含的元素
1405
+ * @returns {DslBuilder}
1406
+ *
1407
+ * @example
1408
+ * dsl('array<string>').includesRequired(['admin', 'user'])
1409
+ */
1410
+ includesRequired(items) {
1411
+ if (this._baseSchema.type !== "array") {
1412
+ throw new Error("includesRequired() only applies to array type");
1413
+ }
1414
+ if (!Array.isArray(items)) {
1415
+ throw new Error("includesRequired() requires an array parameter");
1416
+ }
1417
+ this._baseSchema.includesRequired = items;
1418
+ return this;
1419
+ }
1420
+
1421
+ /**
1422
+ * Date 自定义日期格式验证
1423
+ * @param {string} fmt - 日期格式(YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, DD/MM/YYYY, ISO8601)
1424
+ * @returns {DslBuilder}
1425
+ *
1426
+ * @example
1427
+ * dsl('string!').dateFormat('YYYY-MM-DD')
1428
+ */
1429
+ dateFormat(fmt) {
1430
+ if (this._baseSchema.type !== "string") {
1431
+ throw new Error("dateFormat() only applies to string type");
1432
+ }
1433
+ this._baseSchema.dateFormat = fmt;
1434
+ return this;
1435
+ }
1436
+
1437
+ /**
1438
+ * Date 必须晚于指定日期
1439
+ * @param {string} date - 比较日期
1440
+ * @returns {DslBuilder}
1441
+ *
1442
+ * @example
1443
+ * dsl('date!').after('2024-01-01')
1444
+ */
1445
+ after(date) {
1446
+ if (this._baseSchema.type !== "string") {
1447
+ throw new Error("after() only applies to string type");
1448
+ }
1449
+ this._baseSchema.dateGreater = date;
1450
+ return this;
1451
+ }
1452
+
1453
+ /**
1454
+ * Date 必须早于指定日期
1455
+ * @param {string} date - 比较日期
1456
+ * @returns {DslBuilder}
1457
+ *
1458
+ * @example
1459
+ * dsl('date!').before('2025-12-31')
1460
+ */
1461
+ before(date) {
1462
+ if (this._baseSchema.type !== "string") {
1463
+ throw new Error("before() only applies to string type");
1464
+ }
1465
+ this._baseSchema.dateLess = date;
1466
+ return this;
1467
+ }
1468
+
1469
+ /**
1470
+ * Pattern 域名验证
1471
+ * @returns {DslBuilder}
1472
+ *
1473
+ * @example
1474
+ * dsl('string!').domain()
1475
+ */
1476
+ domain() {
1477
+ if (this._baseSchema.type !== "string") {
1478
+ throw new Error("domain() only applies to string type");
1479
+ }
1480
+ const config = patterns.common.domain;
1481
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1482
+ }
1483
+
1484
+ /**
1485
+ * Pattern IP地址验证(IPv4或IPv6)
1486
+ * @returns {DslBuilder}
1487
+ *
1488
+ * @example
1489
+ * dsl('string!').ip()
1490
+ */
1491
+ ip() {
1492
+ if (this._baseSchema.type !== "string") {
1493
+ throw new Error("ip() only applies to string type");
1494
+ }
1495
+ const config = patterns.common.ip;
1496
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1497
+ }
1498
+
1499
+ /**
1500
+ * Pattern Base64编码验证
1501
+ * @returns {DslBuilder}
1502
+ *
1503
+ * @example
1504
+ * dsl('string!').base64()
1505
+ */
1506
+ base64() {
1507
+ if (this._baseSchema.type !== "string") {
1508
+ throw new Error("base64() only applies to string type");
1509
+ }
1510
+ const config = patterns.common.base64;
1511
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1512
+ }
1513
+
1514
+ /**
1515
+ * Pattern JWT令牌验证
1516
+ * @returns {DslBuilder}
1517
+ *
1518
+ * @example
1519
+ * dsl('string!').jwt()
1520
+ */
1521
+ jwt() {
1522
+ if (this._baseSchema.type !== "string") {
1523
+ throw new Error("jwt() only applies to string type");
1524
+ }
1525
+ const config = patterns.common.jwt;
1526
+ return this.pattern(config.pattern).messages({ pattern: config.key });
1527
+ }
1528
+
1529
+ /**
1530
+ * Pattern JSON字符串验证
1531
+ * @returns {DslBuilder}
1532
+ *
1533
+ * @example
1534
+ * dsl('string!').json()
1535
+ */
1536
+ json() {
1537
+ if (this._baseSchema.type !== "string") {
1538
+ throw new Error("json() only applies to string type");
1539
+ }
1540
+ this._baseSchema.jsonString = true;
1541
+ return this;
1542
+ }
1543
+
1544
+ /**
1545
+ * Pattern URL slug验证 (v1.0.3)
1546
+ * URL slug只能包含小写字母、数字和连字符
1547
+ * @returns {DslBuilder}
1548
+ *
1549
+ * @example
1550
+ * dsl('string!').slug() // my-blog-post, hello-world-123
1551
+ */
1552
+ slug() {
1553
+ if (this._baseSchema.type !== "string") {
1554
+ throw new Error("slug() only applies to string type");
1555
+ }
1556
+ this._baseSchema.pattern = "^[a-z0-9]+(?:-[a-z0-9]+)*$";
1557
+ this._baseSchema._customMessages = this._baseSchema._customMessages || {};
1558
+ this._baseSchema._customMessages["pattern"] = "pattern.slug";
1559
+ return this;
1560
+ }
1561
+
1562
+ /**
1563
+ * 日期大于验证 (v1.0.2)
1564
+ * @param {string} date - 对比日期
1565
+ * @returns {DslBuilder}
1566
+ *
1567
+ * @example
1568
+ * dsl('string!').dateGreater('2025-01-01')
1569
+ */
1570
+ dateGreater(date) {
1571
+ this._baseSchema.dateGreater = date;
1572
+ return this;
1573
+ }
1574
+
1575
+ /**
1576
+ * 日期小于验证 (v1.0.2)
1577
+ * @param {string} date - 对比日期
1578
+ * @returns {DslBuilder}
1579
+ *
1580
+ * @example
1581
+ * dsl('string!').dateLess('2025-12-31')
1582
+ */
1583
+ dateLess(date) {
1584
+ this._baseSchema.dateLess = date;
1585
+ return this;
1586
+ }
1587
+ }
1588
+
1589
+ module.exports = DslBuilder;