schema-dsl 2.3.0
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/.eslintignore +10 -0
- package/.eslintrc.json +27 -0
- package/.github/CODE_OF_CONDUCT.md +45 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
- package/.github/ISSUE_TEMPLATE/question.md +31 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
- package/.github/SECURITY.md +184 -0
- package/.github/workflows/ci.yml +35 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1122 -0
- package/STATUS.md +273 -0
- package/docs/FEATURE-INDEX.md +521 -0
- package/docs/INDEX.md +224 -0
- package/docs/api-reference.md +1098 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +602 -0
- package/docs/dsl-syntax.md +654 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +459 -0
- package/docs/faq.md +576 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +488 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +398 -0
- package/docs/mongodb-exporter.md +279 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +257 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +290 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils.md +492 -0
- package/docs/string-extensions.md +480 -0
- package/docs/troubleshooting.md +471 -0
- package/docs/type-converter.md +319 -0
- package/docs/type-reference.md +219 -0
- package/docs/validate.md +486 -0
- package/docs/validation-guide.md +484 -0
- package/examples/array-dsl-example.js +227 -0
- package/examples/custom-extension.js +85 -0
- package/examples/dsl-match-example.js +74 -0
- package/examples/dsl-style.js +118 -0
- package/examples/dynamic-locale-configuration.js +348 -0
- package/examples/dynamic-locale-example.js +287 -0
- package/examples/export-demo.js +130 -0
- package/examples/i18n-full-demo.js +310 -0
- package/examples/i18n-memory-safety.examples.js +268 -0
- package/examples/markdown-export.js +71 -0
- package/examples/middleware-usage.js +93 -0
- package/examples/password-reset/README.md +153 -0
- package/examples/password-reset/schema.js +26 -0
- package/examples/password-reset/test.js +101 -0
- package/examples/plugin-system.examples.js +205 -0
- package/examples/simple-example.js +122 -0
- package/examples/string-extensions.js +297 -0
- package/examples/user-registration/README.md +156 -0
- package/examples/user-registration/routes.js +92 -0
- package/examples/user-registration/schema.js +150 -0
- package/examples/user-registration/server.js +74 -0
- package/index.d.ts +1999 -0
- package/index.js +270 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +653 -0
- package/lib/adapters/index.js +20 -0
- package/lib/config/constants.js +286 -0
- package/lib/config/patterns/creditCard.js +9 -0
- package/lib/config/patterns/idCard.js +9 -0
- package/lib/config/patterns/index.js +8 -0
- package/lib/config/patterns/licensePlate.js +4 -0
- package/lib/config/patterns/passport.js +4 -0
- package/lib/config/patterns/phone.js +9 -0
- package/lib/config/patterns/postalCode.js +5 -0
- package/lib/core/CacheManager.js +376 -0
- package/lib/core/DslBuilder.js +740 -0
- package/lib/core/ErrorCodes.js +233 -0
- package/lib/core/ErrorFormatter.js +342 -0
- package/lib/core/JSONSchemaCore.js +347 -0
- package/lib/core/Locale.js +119 -0
- package/lib/core/MessageTemplate.js +89 -0
- package/lib/core/PluginManager.js +448 -0
- package/lib/core/StringExtensions.js +209 -0
- package/lib/core/Validator.js +316 -0
- package/lib/exporters/MarkdownExporter.js +420 -0
- package/lib/exporters/MongoDBExporter.js +162 -0
- package/lib/exporters/MySQLExporter.js +212 -0
- package/lib/exporters/PostgreSQLExporter.js +289 -0
- package/lib/exporters/index.js +24 -0
- package/lib/locales/en-US.js +65 -0
- package/lib/locales/es-ES.js +66 -0
- package/lib/locales/fr-FR.js +66 -0
- package/lib/locales/index.js +8 -0
- package/lib/locales/ja-JP.js +66 -0
- package/lib/locales/zh-CN.js +93 -0
- package/lib/utils/LRUCache.js +174 -0
- package/lib/utils/SchemaHelper.js +240 -0
- package/lib/utils/SchemaUtils.js +313 -0
- package/lib/utils/TypeConverter.js +245 -0
- package/lib/utils/index.js +13 -0
- package/lib/validators/CustomKeywords.js +203 -0
- package/lib/validators/index.js +11 -0
- package/package.json +70 -0
- package/plugins/custom-format.js +101 -0
- package/plugins/custom-validator.js +200 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL风格适配器 v2.0
|
|
3
|
+
*
|
|
4
|
+
* 统一的DSL Builder Pattern
|
|
5
|
+
* 支持:纯字符串DSL + 链式调用扩展
|
|
6
|
+
*
|
|
7
|
+
* @module lib/adapters/DslAdapter
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const JSONSchemaCore = require('../core/JSONSchemaCore');
|
|
12
|
+
const DslBuilder = require('../core/DslBuilder');
|
|
13
|
+
|
|
14
|
+
// throw new Error('DEBUG FILE LOADED');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* DSL适配器类
|
|
18
|
+
* @class DslAdapter
|
|
19
|
+
*/
|
|
20
|
+
class DslAdapter {
|
|
21
|
+
/**
|
|
22
|
+
* 解析DSL字符串为JSON Schema(内部使用)
|
|
23
|
+
* @static
|
|
24
|
+
* @param {string} dslString - DSL字符串
|
|
25
|
+
* @returns {Object} JSON Schema对象
|
|
26
|
+
*/
|
|
27
|
+
static parseString(dslString) {
|
|
28
|
+
if (!dslString || typeof dslString !== 'string') {
|
|
29
|
+
throw new Error('DSL must be a string');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const trimmed = dslString.trim();
|
|
33
|
+
let required = trimmed.endsWith('!');
|
|
34
|
+
let dslWithoutRequired = required ? trimmed.slice(0, -1) : trimmed;
|
|
35
|
+
|
|
36
|
+
// 特殊处理:array!数字范围 → array:数字范围 + 必填
|
|
37
|
+
// 支持: array!1-10, array!1-, array!-10
|
|
38
|
+
if (/^array![\d-]/.test(trimmed)) {
|
|
39
|
+
dslWithoutRequired = trimmed.replace(/^array!/, 'array:');
|
|
40
|
+
required = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 解析基本类型和约束
|
|
44
|
+
const schema = this._parseType(dslWithoutRequired);
|
|
45
|
+
|
|
46
|
+
return schema;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* parse方法别名(向后兼容)
|
|
51
|
+
* @static
|
|
52
|
+
* @param {string} dslString - DSL字符串
|
|
53
|
+
* @returns {Object} JSON Schema对象
|
|
54
|
+
*/
|
|
55
|
+
static parse(dslString) {
|
|
56
|
+
const schema = this.parseString(dslString);
|
|
57
|
+
schema._required = dslString.trim().endsWith('!');
|
|
58
|
+
return schema;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 解析类型和约束
|
|
63
|
+
* @private
|
|
64
|
+
* @static
|
|
65
|
+
* @param {string} dsl - DSL字符串(不含!)
|
|
66
|
+
* @returns {Object} JSON Schema对象
|
|
67
|
+
*/
|
|
68
|
+
static _parseType(dsl) {
|
|
69
|
+
// 处理数组DSL语法
|
|
70
|
+
if (dsl.startsWith('array')) {
|
|
71
|
+
const schema = { type: 'array' };
|
|
72
|
+
|
|
73
|
+
// 匹配模式: array:min-max<itemType> 或 array:constraint<itemType> 或 array<itemType>
|
|
74
|
+
// 支持: array:1-10, array:1-, array:-10, array:10, array<string>, array:1-10<string>
|
|
75
|
+
const arrayMatch = dsl.match(/^array(?::([^<]+?))?(?:<(.+)>)?$/);
|
|
76
|
+
|
|
77
|
+
if (arrayMatch) {
|
|
78
|
+
const [, constraint, itemType] = arrayMatch;
|
|
79
|
+
|
|
80
|
+
// 解析约束
|
|
81
|
+
if (constraint) {
|
|
82
|
+
const trimmedConstraint = constraint.trim();
|
|
83
|
+
|
|
84
|
+
if (trimmedConstraint.includes('-')) {
|
|
85
|
+
// 范围约束: min-max, min-, -max
|
|
86
|
+
const [min, max] = trimmedConstraint.split('-').map(v => v.trim());
|
|
87
|
+
if (min) schema.minItems = parseInt(min, 10);
|
|
88
|
+
if (max) schema.maxItems = parseInt(max, 10);
|
|
89
|
+
} else {
|
|
90
|
+
// 单个值 = 最大值
|
|
91
|
+
schema.maxItems = parseInt(trimmedConstraint, 10);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 解析元素类型
|
|
96
|
+
if (itemType) {
|
|
97
|
+
schema.items = this._parseType(itemType.trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return schema;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// 检查是否为纯枚举(包含|但没有:且没有数字范围)
|
|
106
|
+
if (dsl.includes('|') && !dsl.includes(':') && !/^\w+\d+-\d+$/.test(dsl)) {
|
|
107
|
+
return {
|
|
108
|
+
type: 'string',
|
|
109
|
+
enum: dsl.split('|').map(v => v.trim())
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 格式: type:constraint 或 type数字范围
|
|
114
|
+
// 例如: string:3-32, number:0-100
|
|
115
|
+
let type, constraint;
|
|
116
|
+
|
|
117
|
+
// 检查是否有冒号
|
|
118
|
+
const colonIndex = dsl.indexOf(':');
|
|
119
|
+
|
|
120
|
+
if (colonIndex !== -1) {
|
|
121
|
+
// 有冒号:string:3-32
|
|
122
|
+
type = dsl.substring(0, colonIndex);
|
|
123
|
+
constraint = dsl.substring(colonIndex + 1);
|
|
124
|
+
} else {
|
|
125
|
+
// 无冒号:检查是否有数字约束(string3-32)
|
|
126
|
+
const match = dsl.match(/^([a-z]+)(\d.*)$/i);
|
|
127
|
+
if (match) {
|
|
128
|
+
type = match[1];
|
|
129
|
+
constraint = match[2];
|
|
130
|
+
} else {
|
|
131
|
+
type = dsl;
|
|
132
|
+
constraint = '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 特殊处理 phone:country
|
|
137
|
+
if (type === 'phone') {
|
|
138
|
+
const country = constraint || 'cn';
|
|
139
|
+
const config = patterns.phone[country];
|
|
140
|
+
if (!config) throw new Error(`Unsupported country for phone: ${country}`);
|
|
141
|
+
return {
|
|
142
|
+
type: 'string',
|
|
143
|
+
pattern: config.pattern.source,
|
|
144
|
+
minLength: config.min,
|
|
145
|
+
maxLength: config.max,
|
|
146
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 特殊处理 idCard:country
|
|
151
|
+
if (type === 'idCard') {
|
|
152
|
+
const country = constraint || 'cn';
|
|
153
|
+
const config = patterns.idCard[country.toLowerCase()];
|
|
154
|
+
if (!config) throw new Error(`Unsupported country for idCard: ${country}`);
|
|
155
|
+
return {
|
|
156
|
+
type: 'string',
|
|
157
|
+
pattern: config.pattern.source,
|
|
158
|
+
minLength: config.min,
|
|
159
|
+
maxLength: config.max,
|
|
160
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 特殊处理 creditCard:type
|
|
165
|
+
if (type === 'creditCard') {
|
|
166
|
+
const cardType = constraint || 'visa';
|
|
167
|
+
const config = patterns.creditCard[cardType.toLowerCase()];
|
|
168
|
+
if (!config) throw new Error(`Unsupported credit card type: ${cardType}`);
|
|
169
|
+
return {
|
|
170
|
+
type: 'string',
|
|
171
|
+
pattern: config.pattern.source,
|
|
172
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 特殊处理 licensePlate:country
|
|
177
|
+
if (type === 'licensePlate') {
|
|
178
|
+
const country = constraint || 'cn';
|
|
179
|
+
const config = patterns.licensePlate[country.toLowerCase()];
|
|
180
|
+
if (!config) throw new Error(`Unsupported country for licensePlate: ${country}`);
|
|
181
|
+
return {
|
|
182
|
+
type: 'string',
|
|
183
|
+
pattern: config.pattern.source,
|
|
184
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 特殊处理 postalCode:country
|
|
189
|
+
if (type === 'postalCode') {
|
|
190
|
+
const country = constraint || 'cn';
|
|
191
|
+
const config = patterns.postalCode[country.toLowerCase()];
|
|
192
|
+
if (!config) throw new Error(`Unsupported country for postalCode: ${country}`);
|
|
193
|
+
return {
|
|
194
|
+
type: 'string',
|
|
195
|
+
pattern: config.pattern.source,
|
|
196
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 特殊处理 passport:country
|
|
201
|
+
if (type === 'passport') {
|
|
202
|
+
const country = constraint || 'cn';
|
|
203
|
+
const config = patterns.passport[country.toLowerCase()];
|
|
204
|
+
if (!config) throw new Error(`Unsupported country for passport: ${country}`);
|
|
205
|
+
return {
|
|
206
|
+
type: 'string',
|
|
207
|
+
pattern: config.pattern.source,
|
|
208
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 获取基础类型Schema
|
|
213
|
+
const baseSchema = this._getBaseType(type);
|
|
214
|
+
|
|
215
|
+
// 应用约束
|
|
216
|
+
if (constraint) {
|
|
217
|
+
const constraintSchema = this._parseConstraint(baseSchema.type, constraint);
|
|
218
|
+
Object.assign(baseSchema, constraintSchema);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return baseSchema;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 获取基本类型Schema
|
|
226
|
+
* @private
|
|
227
|
+
* @static
|
|
228
|
+
* @param {string} type - 类型名称
|
|
229
|
+
* @returns {Object} 基本类型Schema
|
|
230
|
+
*/
|
|
231
|
+
static _getBaseType(type) {
|
|
232
|
+
const typeMap = {
|
|
233
|
+
// 基本类型
|
|
234
|
+
'string': { type: 'string' },
|
|
235
|
+
'number': { type: 'number' },
|
|
236
|
+
'integer': { type: 'integer' },
|
|
237
|
+
'boolean': { type: 'boolean' },
|
|
238
|
+
'object': { type: 'object' },
|
|
239
|
+
'array': { type: 'array' },
|
|
240
|
+
'null': { type: 'null' },
|
|
241
|
+
|
|
242
|
+
// 格式类型
|
|
243
|
+
'date': { type: 'string', format: 'date' },
|
|
244
|
+
'datetime': { type: 'string', format: 'date-time' },
|
|
245
|
+
'time': { type: 'string', format: 'time' },
|
|
246
|
+
'email': { type: 'string', format: 'email' },
|
|
247
|
+
'url': { type: 'string', format: 'uri' },
|
|
248
|
+
'uuid': { type: 'string', format: 'uuid' },
|
|
249
|
+
'ipv4': { type: 'string', format: 'ipv4' },
|
|
250
|
+
'ipv6': { type: 'string', format: 'ipv6' },
|
|
251
|
+
|
|
252
|
+
// 特殊类型(对比 joi 补充)
|
|
253
|
+
'binary': { type: 'string', contentEncoding: 'base64' },
|
|
254
|
+
'objectId': { type: 'string', pattern: '^[0-9a-fA-F]{24}$' },
|
|
255
|
+
'hexColor': { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' },
|
|
256
|
+
'macAddress': { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' },
|
|
257
|
+
'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]))$' },
|
|
258
|
+
'any': {} // 任意类型
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return typeMap[type] || { type: 'string' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 解析约束
|
|
266
|
+
* @private
|
|
267
|
+
* @static
|
|
268
|
+
* @param {string} baseType - 基础类型
|
|
269
|
+
* @param {string} constraint - 约束字符串
|
|
270
|
+
* @returns {Object} 约束对象
|
|
271
|
+
*/
|
|
272
|
+
static _parseConstraint(baseType, constraint) {
|
|
273
|
+
if (!constraint) return {};
|
|
274
|
+
|
|
275
|
+
const result = {};
|
|
276
|
+
|
|
277
|
+
// 枚举: value1|value2|value3 (优先检查,避免被数字范围误判)
|
|
278
|
+
if (constraint.includes('|') && !/^\d+-\d+$/.test(constraint)) {
|
|
279
|
+
result.enum = constraint.split('|').map(v => v.trim());
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 范围约束: min-max 或 min- 或 -max
|
|
284
|
+
const rangeMatch = constraint.match(/^(\d*)-(\d*)$/);
|
|
285
|
+
if (rangeMatch) {
|
|
286
|
+
const [, min, max] = rangeMatch;
|
|
287
|
+
if (baseType === 'string') {
|
|
288
|
+
if (min) result.minLength = parseInt(min, 10);
|
|
289
|
+
if (max) result.maxLength = parseInt(max, 10);
|
|
290
|
+
} else if (baseType === 'array') {
|
|
291
|
+
if (min) result.minItems = parseInt(min, 10);
|
|
292
|
+
if (max) result.maxItems = parseInt(max, 10);
|
|
293
|
+
} else {
|
|
294
|
+
if (min) result.minimum = parseInt(min, 10);
|
|
295
|
+
if (max) result.maximum = parseInt(max, 10);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 单个约束: min, max
|
|
301
|
+
const singleMatch = constraint.match(/^(\d+)$/);
|
|
302
|
+
if (singleMatch) {
|
|
303
|
+
const value = parseInt(singleMatch[1], 10);
|
|
304
|
+
if (baseType === 'string') {
|
|
305
|
+
result.maxLength = value;
|
|
306
|
+
} else if (baseType === 'array') {
|
|
307
|
+
result.maxItems = value;
|
|
308
|
+
} else {
|
|
309
|
+
result.maximum = value;
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return constraint;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 解析对象为Schema(支持DslBuilder)
|
|
319
|
+
* @static
|
|
320
|
+
* @param {Object} dslObject - DSL对象定义
|
|
321
|
+
* @returns {Object} JSON Schema对象
|
|
322
|
+
*/
|
|
323
|
+
static parseObject(dslObject) {
|
|
324
|
+
const schema = {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {},
|
|
327
|
+
required: []
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
for (const [key, value] of Object.entries(dslObject)) {
|
|
331
|
+
let fieldKey = key;
|
|
332
|
+
let isFieldRequired = false;
|
|
333
|
+
|
|
334
|
+
// 检查 key 是否带 ! 后缀(表示该字段本身必填)
|
|
335
|
+
if (key.endsWith('!')) {
|
|
336
|
+
fieldKey = key.slice(0, -1);
|
|
337
|
+
isFieldRequired = true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let fieldSchema;
|
|
341
|
+
|
|
342
|
+
// 1. Match 结构 (dsl.match)
|
|
343
|
+
if (value && value._isMatch) {
|
|
344
|
+
const matchSchema = this._buildMatchSchema(value.field, fieldKey, value.map);
|
|
345
|
+
if (matchSchema) {
|
|
346
|
+
if (!schema.allOf) schema.allOf = [];
|
|
347
|
+
schema.allOf.push(matchSchema);
|
|
348
|
+
}
|
|
349
|
+
// 占位Schema,确保字段存在
|
|
350
|
+
fieldSchema = { description: `Depends on ${value.field}` };
|
|
351
|
+
}
|
|
352
|
+
// 2. If 结构 (dsl.if)
|
|
353
|
+
else if (value && value._isIf) {
|
|
354
|
+
const ifSchema = this._buildIfSchema(value.condition, fieldKey, value.then, value.else);
|
|
355
|
+
if (ifSchema) {
|
|
356
|
+
if (!schema.allOf) schema.allOf = [];
|
|
357
|
+
schema.allOf.push(ifSchema);
|
|
358
|
+
}
|
|
359
|
+
fieldSchema = { description: `Conditional field based on ${value.condition}` };
|
|
360
|
+
}
|
|
361
|
+
// 3. DslBuilder 实例(链式调用结果)
|
|
362
|
+
else if (value instanceof DslBuilder) {
|
|
363
|
+
fieldSchema = value.toSchema();
|
|
364
|
+
}
|
|
365
|
+
// 4. 纯字符串 DSL
|
|
366
|
+
else if (typeof value === 'string') {
|
|
367
|
+
const builder = new DslBuilder(value);
|
|
368
|
+
fieldSchema = builder.toSchema();
|
|
369
|
+
}
|
|
370
|
+
// 5. 嵌套对象
|
|
371
|
+
else if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
|
|
372
|
+
fieldSchema = this.parseObject(value);
|
|
373
|
+
}
|
|
374
|
+
// 6. 其他类型(保留原样)
|
|
375
|
+
else {
|
|
376
|
+
fieldSchema = value;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 处理必填标记(优先级:key! > 字段内部的!)
|
|
380
|
+
if (isFieldRequired) {
|
|
381
|
+
schema.required.push(fieldKey);
|
|
382
|
+
} else if (fieldSchema && fieldSchema._required) {
|
|
383
|
+
schema.required.push(fieldKey);
|
|
384
|
+
delete fieldSchema._required;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 清理所有_required标记(包括嵌套的)
|
|
388
|
+
this._cleanRequiredMarks(fieldSchema);
|
|
389
|
+
|
|
390
|
+
schema.properties[fieldKey] = fieldSchema;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (schema.required.length === 0) {
|
|
394
|
+
delete schema.required;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return schema;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 创建 Match 结构
|
|
402
|
+
* @static
|
|
403
|
+
* @param {string} field - 依赖字段名
|
|
404
|
+
* @param {Object} map - 值映射
|
|
405
|
+
* @returns {Object} Match结构
|
|
406
|
+
*/
|
|
407
|
+
static match(field, map) {
|
|
408
|
+
return {
|
|
409
|
+
_isMatch: true,
|
|
410
|
+
field,
|
|
411
|
+
map
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 创建 If 结构
|
|
417
|
+
* @static
|
|
418
|
+
* @param {string} condition - 条件字段
|
|
419
|
+
* @param {string|Object} thenSchema - 满足条件时的Schema
|
|
420
|
+
* @param {string|Object} elseSchema - 不满足条件时的Schema
|
|
421
|
+
* @returns {Object} If结构
|
|
422
|
+
*/
|
|
423
|
+
static if(condition, thenSchema, elseSchema) {
|
|
424
|
+
return {
|
|
425
|
+
_isIf: true,
|
|
426
|
+
condition,
|
|
427
|
+
then: thenSchema,
|
|
428
|
+
else: elseSchema
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 解析DSL定义(字符串/Builder/对象)
|
|
434
|
+
* @private
|
|
435
|
+
* @static
|
|
436
|
+
*/
|
|
437
|
+
static _resolveDsl(dsl) {
|
|
438
|
+
if (!dsl) return {};
|
|
439
|
+
if (dsl instanceof DslBuilder) return dsl.toSchema();
|
|
440
|
+
if (typeof dsl === 'string') return new DslBuilder(dsl).toSchema();
|
|
441
|
+
if (typeof dsl === 'object' && !Array.isArray(dsl)) return this.parseObject(dsl);
|
|
442
|
+
return dsl;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 构建 Match 对应的 JSON Schema (if-then-else 链)
|
|
447
|
+
* @private
|
|
448
|
+
* @static
|
|
449
|
+
*/
|
|
450
|
+
static _buildMatchSchema(conditionField, targetField, map) {
|
|
451
|
+
const entries = Object.entries(map).filter(([k]) => k !== '_default');
|
|
452
|
+
const defaultDsl = map._default;
|
|
453
|
+
|
|
454
|
+
const build = (index) => {
|
|
455
|
+
if (index >= entries.length) {
|
|
456
|
+
if (defaultDsl) {
|
|
457
|
+
const s = this._resolveDsl(defaultDsl);
|
|
458
|
+
const isRequired = s._required;
|
|
459
|
+
// 清理标记
|
|
460
|
+
this._cleanRequiredMarks(s);
|
|
461
|
+
|
|
462
|
+
const result = { properties: { [targetField]: s } };
|
|
463
|
+
if (isRequired) result.required = [targetField];
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const [val, dsl] = entries[index];
|
|
470
|
+
const branchSchema = this._resolveDsl(dsl);
|
|
471
|
+
const isRequired = branchSchema._required;
|
|
472
|
+
this._cleanRequiredMarks(branchSchema);
|
|
473
|
+
|
|
474
|
+
const thenSchema = { properties: { [targetField]: branchSchema } };
|
|
475
|
+
if (isRequired) thenSchema.required = [targetField];
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
if: { properties: { [conditionField]: { const: val } } },
|
|
479
|
+
then: thenSchema,
|
|
480
|
+
else: build(index + 1)
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
return build(0);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 构建 If 对应的 JSON Schema
|
|
489
|
+
* @private
|
|
490
|
+
* @static
|
|
491
|
+
*/
|
|
492
|
+
static _buildIfSchema(conditionField, targetField, thenDsl, elseDsl) {
|
|
493
|
+
const thenSchema = this._resolveDsl(thenDsl);
|
|
494
|
+
const elseSchema = elseDsl ? this._resolveDsl(elseDsl) : null;
|
|
495
|
+
|
|
496
|
+
const thenRequired = thenSchema._required;
|
|
497
|
+
this._cleanRequiredMarks(thenSchema);
|
|
498
|
+
|
|
499
|
+
const thenResult = { properties: { [targetField]: thenSchema } };
|
|
500
|
+
if (thenRequired) thenResult.required = [targetField];
|
|
501
|
+
|
|
502
|
+
let elseResult = {};
|
|
503
|
+
if (elseSchema) {
|
|
504
|
+
const elseRequired = elseSchema._required;
|
|
505
|
+
this._cleanRequiredMarks(elseSchema);
|
|
506
|
+
elseResult = { properties: { [targetField]: elseSchema } };
|
|
507
|
+
if (elseRequired) elseResult.required = [targetField];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 简单假设 condition 是字段存在性或布尔值 true
|
|
511
|
+
// 如果需要更复杂的条件,可能需要解析 condition 字符串
|
|
512
|
+
// 这里假设 conditionField 是一个布尔字段,检查它是否为 true
|
|
513
|
+
// 或者检查它是否存在 (如果它是一个 required 字段)
|
|
514
|
+
|
|
515
|
+
// 默认行为:检查字段值为 true (适用于 isVip: boolean)
|
|
516
|
+
// 如果需要检查存在性,JSON Schema 比较复杂
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
if: { properties: { [conditionField]: { const: true } } },
|
|
520
|
+
then: thenResult,
|
|
521
|
+
else: elseResult
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 解析对象为Schema(旧版本,兼容)
|
|
527
|
+
* @static
|
|
528
|
+
* @param {Object} dslObject - DSL对象定义
|
|
529
|
+
* @returns {Object} JSON Schema对象
|
|
530
|
+
*/
|
|
531
|
+
static parseObjectOld(dslObject) {
|
|
532
|
+
if (!dslObject || typeof dslObject !== 'object') {
|
|
533
|
+
throw new Error('DSL object must be an object');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const schema = {
|
|
537
|
+
type: 'object',
|
|
538
|
+
properties: {},
|
|
539
|
+
required: []
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
for (const [key, value] of Object.entries(dslObject)) {
|
|
543
|
+
if (typeof value === 'string') {
|
|
544
|
+
const parsed = this.parse(value);
|
|
545
|
+
|
|
546
|
+
// 处理必填标记
|
|
547
|
+
if (parsed._required) {
|
|
548
|
+
schema.required.push(key);
|
|
549
|
+
delete parsed._required; // 清理临时标记
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 清理所有_required标记(包括嵌套的)
|
|
553
|
+
this._cleanRequiredMarks(parsed);
|
|
554
|
+
|
|
555
|
+
schema.properties[key] = parsed;
|
|
556
|
+
} else if (typeof value === 'object') {
|
|
557
|
+
// 嵌套对象
|
|
558
|
+
schema.properties[key] = this.parseObject(value);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (schema.required.length === 0) {
|
|
563
|
+
delete schema.required;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return schema;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 清理Schema中的临时标记
|
|
571
|
+
* @private
|
|
572
|
+
* @static
|
|
573
|
+
* @param {Object} schema - Schema对象
|
|
574
|
+
*/
|
|
575
|
+
static _cleanRequiredMarks(schema) {
|
|
576
|
+
if (!schema || typeof schema !== 'object') {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 只删除 _required 标记,保留其他扩展属性(如 _customValidators)
|
|
581
|
+
// 这些属性将由自定义关键字处理
|
|
582
|
+
delete schema._required;
|
|
583
|
+
|
|
584
|
+
// 递归处理properties
|
|
585
|
+
if (schema.properties) {
|
|
586
|
+
for (const prop of Object.values(schema.properties)) {
|
|
587
|
+
this._cleanRequiredMarks(prop);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 递归处理items
|
|
592
|
+
if (schema.items) {
|
|
593
|
+
this._cleanRequiredMarks(schema.items);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* 转换为JSONSchemaCore实例
|
|
599
|
+
* @static
|
|
600
|
+
* @param {string|Object} dsl - DSL字符串或对象
|
|
601
|
+
* @returns {JSONSchemaCore} JSONSchemaCore实例
|
|
602
|
+
*/
|
|
603
|
+
static toCore(dsl) {
|
|
604
|
+
let schema;
|
|
605
|
+
if (typeof dsl === 'string') {
|
|
606
|
+
// 创建DslBuilder并转为Schema
|
|
607
|
+
const builder = new DslBuilder(dsl);
|
|
608
|
+
schema = builder.toSchema();
|
|
609
|
+
} else {
|
|
610
|
+
schema = this.parseObject(dsl);
|
|
611
|
+
}
|
|
612
|
+
return new JSONSchemaCore(schema);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ========== 导出统一API ==========
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* DSL函数 - 统一入口
|
|
620
|
+
* @param {string|Object} definition - DSL字符串或对象定义
|
|
621
|
+
* @returns {DslBuilder|Object} DslBuilder实例或JSON Schema对象
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* // 字符串:返回 DslBuilder(可链式调用)
|
|
625
|
+
* const schema = dsl('email!')
|
|
626
|
+
* .pattern(/custom/)
|
|
627
|
+
* .messages({ 'string.pattern': '格式不正确' });
|
|
628
|
+
*
|
|
629
|
+
* // 对象:返回 JSON Schema
|
|
630
|
+
* const schema = dsl({
|
|
631
|
+
* username: 'string:3-32!',
|
|
632
|
+
* email: dsl('email!').label('邮箱')
|
|
633
|
+
* });
|
|
634
|
+
*/
|
|
635
|
+
function dsl(definition) {
|
|
636
|
+
// 字符串:返回 DslBuilder
|
|
637
|
+
if (typeof definition === 'string') {
|
|
638
|
+
return new DslBuilder(definition);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 对象:解析对象Schema
|
|
642
|
+
if (typeof definition === 'object' && !Array.isArray(definition)) {
|
|
643
|
+
return DslAdapter.parseObject(definition);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
throw new Error('Invalid DSL definition: must be string or object');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 导出
|
|
650
|
+
module.exports = dsl;
|
|
651
|
+
module.exports.DslAdapter = DslAdapter;
|
|
652
|
+
module.exports.DslBuilder = DslBuilder;
|
|
653
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapters - 适配器统一导出
|
|
3
|
+
* @module lib/adapters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const joi = require('./JoiAdapter');
|
|
7
|
+
const dsl = require('./DslAdapter');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
// Joi风格
|
|
11
|
+
joi,
|
|
12
|
+
JoiAdapter: joi.JoiAdapter,
|
|
13
|
+
|
|
14
|
+
// DSL风格
|
|
15
|
+
dsl,
|
|
16
|
+
DslAdapter: dsl.DslAdapter,
|
|
17
|
+
s: dsl.s,
|
|
18
|
+
_: dsl._
|
|
19
|
+
};
|
|
20
|
+
|