schema-dsl 1.0.0 → 1.0.4

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +263 -529
  2. package/README.md +814 -896
  3. package/STATUS.md +135 -2
  4. package/docs/INDEX.md +1 -2
  5. package/docs/api-reference.md +1 -292
  6. package/docs/custom-extensions-guide.md +411 -0
  7. package/docs/enum.md +475 -0
  8. package/docs/i18n.md +394 -0
  9. package/docs/performance-benchmark-report.md +179 -0
  10. package/docs/plugin-system.md +8 -8
  11. package/docs/typescript-guide.md +554 -0
  12. package/docs/validate-async.md +1 -1
  13. package/docs/validation-rules-v1.0.2.md +1601 -0
  14. package/examples/README.md +81 -0
  15. package/examples/enum.examples.js +324 -0
  16. package/examples/express-integration.js +54 -54
  17. package/examples/i18n-full-demo.js +15 -24
  18. package/examples/schema-utils-chaining.examples.js +2 -2
  19. package/examples/slug.examples.js +179 -0
  20. package/index.d.ts +246 -17
  21. package/index.js +30 -34
  22. package/lib/config/constants.js +1 -1
  23. package/lib/config/patterns/common.js +47 -0
  24. package/lib/config/patterns/index.js +2 -1
  25. package/lib/core/DslBuilder.js +500 -8
  26. package/lib/core/StringExtensions.js +31 -0
  27. package/lib/core/Validator.js +42 -15
  28. package/lib/errors/ValidationError.js +3 -3
  29. package/lib/locales/en-US.js +79 -19
  30. package/lib/locales/es-ES.js +60 -19
  31. package/lib/locales/fr-FR.js +84 -43
  32. package/lib/locales/ja-JP.js +83 -42
  33. package/lib/locales/zh-CN.js +32 -0
  34. package/lib/validators/CustomKeywords.js +405 -0
  35. package/package.json +1 -1
  36. package/.github/CODE_OF_CONDUCT.md +0 -45
  37. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -57
  38. package/.github/ISSUE_TEMPLATE/config.yml +0 -11
  39. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -45
  40. package/.github/ISSUE_TEMPLATE/question.md +0 -31
  41. package/.github/PULL_REQUEST_TEMPLATE.md +0 -70
  42. package/.github/SECURITY.md +0 -184
  43. package/.github/workflows/ci.yml +0 -33
  44. package/plugins/custom-format.js +0 -101
  45. package/plugins/custom-validator.js +0 -200
@@ -97,12 +97,36 @@ class DslBuilder {
97
97
  }
98
98
  }
99
99
 
100
- // 处理枚举
101
- if (dsl.includes('|') && !dsl.includes(':')) {
102
- return {
103
- type: 'string',
104
- enum: dsl.split('|').map(v => v.trim())
105
- };
100
+ // 处理枚举(支持多种格式)
101
+ if (dsl.includes('|')) {
102
+ let enumType = 'string'; // 默认字符串
103
+ let enumValues = dsl;
104
+
105
+ // 识别 enum:type:values 或 enum:values 格式
106
+ if (dsl.startsWith('enum:')) {
107
+ const parts = dsl.slice(5).split(':');
108
+
109
+ if (parts.length === 2) {
110
+ // enum:type:values
111
+ enumType = parts[0];
112
+ enumValues = parts[1];
113
+ } else if (parts.length === 1) {
114
+ // enum:values (默认 string)
115
+ enumValues = parts[0];
116
+ }
117
+ } else if (dsl.includes(':') && !this._isKnownType(dsl.split(':')[0])) {
118
+ // 如果有冒号但不是已知类型(如 string:3-32),不作为枚举
119
+ // 让后续逻辑处理
120
+ } else {
121
+ // 简写形式:value1|value2
122
+ // 自动识别类型
123
+ enumType = this._detectEnumType(enumValues);
124
+ }
125
+
126
+ // 如果是枚举,解析值
127
+ if (enumValues.includes('|')) {
128
+ return this._parseEnum(enumType, enumValues);
129
+ }
106
130
  }
107
131
 
108
132
  // 处理类型:约束格式
@@ -230,7 +254,14 @@ class DslBuilder {
230
254
  'hexColor': { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$', _customMessages: { 'pattern': 'pattern.hexColor' } },
231
255
  'macAddress': { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', _customMessages: { 'pattern': 'pattern.macAddress' } },
232
256
  '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' } },
233
- 'any': {}
257
+ 'slug': { type: 'string', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', _customMessages: { 'pattern': 'pattern.slug' } },
258
+ 'any': {},
259
+ // v1.0.2 新增类型
260
+ 'alphanum': { type: 'string', alphanum: true },
261
+ 'lower': { type: 'string', lowercase: true },
262
+ 'upper': { type: 'string', uppercase: true },
263
+ 'json': { type: 'string', jsonString: true },
264
+ 'port': { type: 'integer', port: true }
234
265
  };
235
266
 
236
267
  return typeMap[type] || { type: 'string' };
@@ -260,8 +291,10 @@ class DslBuilder {
260
291
  const value = constraint.trim();
261
292
  if (value) {
262
293
  if (type === 'string') {
263
- result.maxLength = parseInt(value);
294
+ // 🔴 String单值 = 精确长度(常用于验证码、国家代码等)
295
+ result.exactLength = parseInt(value);
264
296
  } else {
297
+ // Number单值 = 最大值(符合直觉:不超过某值)
265
298
  result.maximum = parseFloat(value);
266
299
  }
267
300
  }
@@ -271,6 +304,76 @@ class DslBuilder {
271
304
  return result;
272
305
  }
273
306
 
307
+ /**
308
+ * 检查是否为已知类型
309
+ * @private
310
+ */
311
+ _isKnownType(type) {
312
+ const knownTypes = [
313
+ 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
314
+ 'email', 'url', 'uuid', 'date', 'datetime', 'time', 'ipv4', 'ipv6',
315
+ 'binary', 'objectId', 'hexColor', 'macAddress', 'cron', 'any',
316
+ 'phone', 'idCard', 'creditCard', 'licensePlate', 'postalCode', 'passport',
317
+ // v1.0.2 新增
318
+ 'alphanum', 'lower', 'upper', 'json', 'port'
319
+ ];
320
+ return knownTypes.includes(type);
321
+ }
322
+
323
+ /**
324
+ * 自动检测枚举类型
325
+ * @private
326
+ */
327
+ _detectEnumType(enumValues) {
328
+ const values = enumValues.split('|').map(v => v.trim());
329
+
330
+ // 检查是否全部为布尔值
331
+ const allBoolean = values.every(v => v === 'true' || v === 'false');
332
+ if (allBoolean) return 'boolean';
333
+
334
+ // 检查是否全部为数字
335
+ const allNumber = values.every(v => !isNaN(parseFloat(v)) && isFinite(v));
336
+ if (allNumber) return 'number';
337
+
338
+ // 默认字符串
339
+ return 'string';
340
+ }
341
+
342
+ /**
343
+ * 解析枚举值
344
+ * @private
345
+ */
346
+ _parseEnum(enumType, enumValues) {
347
+ let values = enumValues.split('|').map(v => v.trim());
348
+
349
+ // 类型转换
350
+ if (enumType === 'boolean') {
351
+ values = values.map(v => {
352
+ if (v === 'true') return true;
353
+ if (v === 'false') return false;
354
+ throw new Error(`Invalid boolean enum value: ${v}. Must be 'true' or 'false'`);
355
+ });
356
+ return { type: 'boolean', enum: values };
357
+ } else if (enumType === 'number') {
358
+ values = values.map(v => {
359
+ const num = parseFloat(v);
360
+ if (isNaN(num)) throw new Error(`Invalid number enum value: ${v}`);
361
+ return num;
362
+ });
363
+ return { type: 'number', enum: values };
364
+ } else if (enumType === 'integer') {
365
+ values = values.map(v => {
366
+ const num = parseInt(v, 10);
367
+ if (isNaN(num)) throw new Error(`Invalid integer enum value: ${v}`);
368
+ return num;
369
+ });
370
+ return { type: 'integer', enum: values };
371
+ } else {
372
+ // 字符串枚举(默认)
373
+ return { type: 'string', enum: values };
374
+ }
375
+ }
376
+
274
377
  /**
275
378
  * 添加正则表达式验证
276
379
  * @param {RegExp|string} regex - 正则表达式
@@ -734,6 +837,395 @@ class DslBuilder {
734
837
  }
735
838
  return this.pattern(config.pattern).messages({ 'pattern': config.key });
736
839
  }
840
+
841
+ // ========== v1.0.2 新增验证器方法 ==========
842
+
843
+ /**
844
+ * String 最小长度(使用AJV原生minLength)
845
+ * @param {number} n - 最小长度
846
+ * @returns {DslBuilder}
847
+ *
848
+ * @example
849
+ * dsl('string!').min(3) // 最少3个字符
850
+ */
851
+ min(n) {
852
+ if (this._baseSchema.type !== 'string') {
853
+ throw new Error('min() only applies to string type');
854
+ }
855
+ this._baseSchema.minLength = n;
856
+ return this;
857
+ }
858
+
859
+ /**
860
+ * String 最大长度(使用AJV原生maxLength)
861
+ * @param {number} n - 最大长度
862
+ * @returns {DslBuilder}
863
+ *
864
+ * @example
865
+ * dsl('string!').max(32) // 最多32个字符
866
+ */
867
+ max(n) {
868
+ if (this._baseSchema.type !== 'string') {
869
+ throw new Error('max() only applies to string type');
870
+ }
871
+ this._baseSchema.maxLength = n;
872
+ return this;
873
+ }
874
+
875
+ /**
876
+ * String 精确长度
877
+ * @param {number} n - 精确长度
878
+ * @returns {DslBuilder}
879
+ *
880
+ * @example
881
+ * dsl('string!').length(11) // 必须是11个字符
882
+ */
883
+ length(n) {
884
+ if (this._baseSchema.type !== 'string') {
885
+ throw new Error('length() only applies to string type');
886
+ }
887
+ this._baseSchema.exactLength = n;
888
+ return this;
889
+ }
890
+
891
+ /**
892
+ * String 只能包含字母和数字
893
+ * @returns {DslBuilder}
894
+ *
895
+ * @example
896
+ * dsl('string!').alphanum() // 只能是字母和数字
897
+ */
898
+ alphanum() {
899
+ if (this._baseSchema.type !== 'string') {
900
+ throw new Error('alphanum() only applies to string type');
901
+ }
902
+ this._baseSchema.alphanum = true;
903
+ return this;
904
+ }
905
+
906
+ /**
907
+ * String 不能包含前后空格
908
+ * @returns {DslBuilder}
909
+ *
910
+ * @example
911
+ * dsl('string!').trim() // 不能有前后空格
912
+ */
913
+ trim() {
914
+ if (this._baseSchema.type !== 'string') {
915
+ throw new Error('trim() only applies to string type');
916
+ }
917
+ this._baseSchema.trim = true;
918
+ return this;
919
+ }
920
+
921
+ /**
922
+ * String 必须是小写
923
+ * @returns {DslBuilder}
924
+ *
925
+ * @example
926
+ * dsl('string!').lowercase() // 必须全小写
927
+ */
928
+ lowercase() {
929
+ if (this._baseSchema.type !== 'string') {
930
+ throw new Error('lowercase() only applies to string type');
931
+ }
932
+ this._baseSchema.lowercase = true;
933
+ return this;
934
+ }
935
+
936
+ /**
937
+ * String 必须是大写
938
+ * @returns {DslBuilder}
939
+ *
940
+ * @example
941
+ * dsl('string!').uppercase() // 必须全大写
942
+ */
943
+ uppercase() {
944
+ if (this._baseSchema.type !== 'string') {
945
+ throw new Error('uppercase() only applies to string type');
946
+ }
947
+ this._baseSchema.uppercase = true;
948
+ return this;
949
+ }
950
+
951
+ /**
952
+ * Number 小数位数限制
953
+ * @param {number} n - 最大小数位数
954
+ * @returns {DslBuilder}
955
+ *
956
+ * @example
957
+ * dsl('number!').precision(2) // 最多2位小数
958
+ */
959
+ precision(n) {
960
+ if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
961
+ throw new Error('precision() only applies to number type');
962
+ }
963
+ this._baseSchema.precision = n;
964
+ return this;
965
+ }
966
+
967
+ /**
968
+ * Number 倍数验证(使用AJV原生multipleOf)
969
+ * @param {number} n - 必须是此数的倍数
970
+ * @returns {DslBuilder}
971
+ *
972
+ * @example
973
+ * dsl('number!').multiple(5) // 必须是5的倍数
974
+ */
975
+ multiple(n) {
976
+ if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
977
+ throw new Error('multiple() only applies to number type');
978
+ }
979
+ this._baseSchema.multipleOf = n;
980
+ return this;
981
+ }
982
+
983
+ /**
984
+ * Number 端口号验证(1-65535)
985
+ * @returns {DslBuilder}
986
+ *
987
+ * @example
988
+ * dsl('integer!').port() // 必须是有效端口号
989
+ */
990
+ port() {
991
+ if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
992
+ throw new Error('port() only applies to number type');
993
+ }
994
+ this._baseSchema.port = true;
995
+ return this;
996
+ }
997
+
998
+ /**
999
+ * Object 要求所有属性都必须存在
1000
+ * @returns {DslBuilder}
1001
+ *
1002
+ * @example
1003
+ * dsl({ name: 'string', age: 'number' }).requireAll()
1004
+ */
1005
+ requireAll() {
1006
+ if (this._baseSchema.type !== 'object') {
1007
+ throw new Error('requireAll() only applies to object type');
1008
+ }
1009
+ this._baseSchema.requiredAll = true;
1010
+ return this;
1011
+ }
1012
+
1013
+ /**
1014
+ * Object 严格模式,不允许额外属性
1015
+ * @returns {DslBuilder}
1016
+ *
1017
+ * @example
1018
+ * dsl({ name: 'string!' }).strict()
1019
+ */
1020
+ strict() {
1021
+ if (this._baseSchema.type !== 'object') {
1022
+ throw new Error('strict() only applies to object type');
1023
+ }
1024
+ this._baseSchema.strictSchema = true;
1025
+ return this;
1026
+ }
1027
+
1028
+ /**
1029
+ * Array 不允许稀疏数组
1030
+ * @returns {DslBuilder}
1031
+ *
1032
+ * @example
1033
+ * dsl('array<string>').noSparse()
1034
+ */
1035
+ noSparse() {
1036
+ if (this._baseSchema.type !== 'array') {
1037
+ throw new Error('noSparse() only applies to array type');
1038
+ }
1039
+ this._baseSchema.noSparse = true;
1040
+ return this;
1041
+ }
1042
+
1043
+ /**
1044
+ * Array 必须包含指定元素
1045
+ * @param {Array} items - 必须包含的元素
1046
+ * @returns {DslBuilder}
1047
+ *
1048
+ * @example
1049
+ * dsl('array<string>').includesRequired(['admin', 'user'])
1050
+ */
1051
+ includesRequired(items) {
1052
+ if (this._baseSchema.type !== 'array') {
1053
+ throw new Error('includesRequired() only applies to array type');
1054
+ }
1055
+ if (!Array.isArray(items)) {
1056
+ throw new Error('includesRequired() requires an array parameter');
1057
+ }
1058
+ this._baseSchema.includesRequired = items;
1059
+ return this;
1060
+ }
1061
+
1062
+ /**
1063
+ * Date 自定义日期格式验证
1064
+ * @param {string} fmt - 日期格式(YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, DD/MM/YYYY, ISO8601)
1065
+ * @returns {DslBuilder}
1066
+ *
1067
+ * @example
1068
+ * dsl('string!').dateFormat('YYYY-MM-DD')
1069
+ */
1070
+ dateFormat(fmt) {
1071
+ if (this._baseSchema.type !== 'string') {
1072
+ throw new Error('dateFormat() only applies to string type');
1073
+ }
1074
+ this._baseSchema.dateFormat = fmt;
1075
+ return this;
1076
+ }
1077
+
1078
+ /**
1079
+ * Date 必须晚于指定日期
1080
+ * @param {string} date - 比较日期
1081
+ * @returns {DslBuilder}
1082
+ *
1083
+ * @example
1084
+ * dsl('date!').after('2024-01-01')
1085
+ */
1086
+ after(date) {
1087
+ if (this._baseSchema.type !== 'string') {
1088
+ throw new Error('after() only applies to string type');
1089
+ }
1090
+ this._baseSchema.dateGreater = date;
1091
+ return this;
1092
+ }
1093
+
1094
+ /**
1095
+ * Date 必须早于指定日期
1096
+ * @param {string} date - 比较日期
1097
+ * @returns {DslBuilder}
1098
+ *
1099
+ * @example
1100
+ * dsl('date!').before('2025-12-31')
1101
+ */
1102
+ before(date) {
1103
+ if (this._baseSchema.type !== 'string') {
1104
+ throw new Error('before() only applies to string type');
1105
+ }
1106
+ this._baseSchema.dateLess = date;
1107
+ return this;
1108
+ }
1109
+
1110
+ /**
1111
+ * Pattern 域名验证
1112
+ * @returns {DslBuilder}
1113
+ *
1114
+ * @example
1115
+ * dsl('string!').domain()
1116
+ */
1117
+ domain() {
1118
+ if (this._baseSchema.type !== 'string') {
1119
+ throw new Error('domain() only applies to string type');
1120
+ }
1121
+ const config = patterns.common.domain;
1122
+ return this.pattern(config.pattern).messages({ 'pattern': config.key });
1123
+ }
1124
+
1125
+ /**
1126
+ * Pattern IP地址验证(IPv4或IPv6)
1127
+ * @returns {DslBuilder}
1128
+ *
1129
+ * @example
1130
+ * dsl('string!').ip()
1131
+ */
1132
+ ip() {
1133
+ if (this._baseSchema.type !== 'string') {
1134
+ throw new Error('ip() only applies to string type');
1135
+ }
1136
+ const config = patterns.common.ip;
1137
+ return this.pattern(config.pattern).messages({ 'pattern': config.key });
1138
+ }
1139
+
1140
+ /**
1141
+ * Pattern Base64编码验证
1142
+ * @returns {DslBuilder}
1143
+ *
1144
+ * @example
1145
+ * dsl('string!').base64()
1146
+ */
1147
+ base64() {
1148
+ if (this._baseSchema.type !== 'string') {
1149
+ throw new Error('base64() only applies to string type');
1150
+ }
1151
+ const config = patterns.common.base64;
1152
+ return this.pattern(config.pattern).messages({ 'pattern': config.key });
1153
+ }
1154
+
1155
+ /**
1156
+ * Pattern JWT令牌验证
1157
+ * @returns {DslBuilder}
1158
+ *
1159
+ * @example
1160
+ * dsl('string!').jwt()
1161
+ */
1162
+ jwt() {
1163
+ if (this._baseSchema.type !== 'string') {
1164
+ throw new Error('jwt() only applies to string type');
1165
+ }
1166
+ const config = patterns.common.jwt;
1167
+ return this.pattern(config.pattern).messages({ 'pattern': config.key });
1168
+ }
1169
+
1170
+ /**
1171
+ * Pattern JSON字符串验证
1172
+ * @returns {DslBuilder}
1173
+ *
1174
+ * @example
1175
+ * dsl('string!').json()
1176
+ */
1177
+ json() {
1178
+ if (this._baseSchema.type !== 'string') {
1179
+ throw new Error('json() only applies to string type');
1180
+ }
1181
+ this._baseSchema.jsonString = true;
1182
+ return this;
1183
+ }
1184
+
1185
+ /**
1186
+ * Pattern URL slug验证 (v1.0.3)
1187
+ * URL slug只能包含小写字母、数字和连字符
1188
+ * @returns {DslBuilder}
1189
+ *
1190
+ * @example
1191
+ * dsl('string!').slug() // my-blog-post, hello-world-123
1192
+ */
1193
+ slug() {
1194
+ if (this._baseSchema.type !== 'string') {
1195
+ throw new Error('slug() only applies to string type');
1196
+ }
1197
+ this._baseSchema.pattern = '^[a-z0-9]+(?:-[a-z0-9]+)*$';
1198
+ this._baseSchema._customMessages = this._baseSchema._customMessages || {};
1199
+ this._baseSchema._customMessages['pattern'] = 'pattern.slug';
1200
+ return this;
1201
+ }
1202
+
1203
+
1204
+ /**
1205
+ * 日期大于验证 (v1.0.2)
1206
+ * @param {string} date - 对比日期
1207
+ * @returns {DslBuilder}
1208
+ *
1209
+ * @example
1210
+ * dsl('string!').dateGreater('2025-01-01')
1211
+ */
1212
+ dateGreater(date) {
1213
+ this._baseSchema.dateGreater = date;
1214
+ return this;
1215
+ }
1216
+
1217
+ /**
1218
+ * 日期小于验证 (v1.0.2)
1219
+ * @param {string} date - 对比日期
1220
+ * @returns {DslBuilder}
1221
+ *
1222
+ * @example
1223
+ * dsl('string!').dateLess('2025-12-31')
1224
+ */
1225
+ dateLess(date) {
1226
+ this._baseSchema.dateLess = date;
1227
+ return this;
1228
+ }
737
1229
  }
738
1230
 
739
1231
  module.exports = DslBuilder;
@@ -172,6 +172,32 @@ function installStringExtensions(dslFunction) {
172
172
  return dslFunction(String(this)).passport(country);
173
173
  };
174
174
 
175
+ /**
176
+ * v1.0.2 新增方法(dateGreater和dateLess)
177
+ * v1.0.3 新增方法(slug)
178
+ */
179
+
180
+ /**
181
+ * URL slug验证
182
+ */
183
+ String.prototype.slug = function() {
184
+ return dslFunction(String(this)).slug();
185
+ };
186
+
187
+ /**
188
+ * 日期大于验证
189
+ */
190
+ String.prototype.dateGreater = function(date) {
191
+ return dslFunction(String(this)).dateGreater(date);
192
+ };
193
+
194
+ /**
195
+ * 日期小于验证
196
+ */
197
+ String.prototype.dateLess = function(date) {
198
+ return dslFunction(String(this)).dateLess(date);
199
+ };
200
+
175
201
  // 标记已安装
176
202
  String.prototype._dslExtensionsInstalled = true;
177
203
  }
@@ -200,6 +226,11 @@ function uninstallStringExtensions() {
200
226
  delete String.prototype.licensePlate;
201
227
  delete String.prototype.postalCode;
202
228
  delete String.prototype.passport;
229
+ // v1.0.2 新增
230
+ delete String.prototype.dateGreater;
231
+ delete String.prototype.dateLess;
232
+ // v1.0.3 新增
233
+ delete String.prototype.slug;
203
234
  delete String.prototype._dslExtensionsInstalled;
204
235
  }
205
236
 
@@ -58,6 +58,13 @@ class Validator {
58
58
  // 错误格式化器
59
59
  this.errorFormatter = new ErrorFormatter();
60
60
 
61
+ // ✅ 性能优化:WeakMap 缓存键(避免 JSON.stringify)
62
+ this.schemaMap = new WeakMap();
63
+ this.schemaKeyCounter = 0;
64
+
65
+ // ✅ 性能优化:缓存 DslBuilder 转换结果
66
+ this.dslSchemaCache = new WeakMap();
67
+
61
68
  // 自定义关键字注册表
62
69
  this.customKeywords = new Map();
63
70
  }
@@ -102,18 +109,35 @@ class Validator {
102
109
  * @returns {Object} 验证结果 { valid: boolean, errors: Array, data: * }
103
110
  */
104
111
  validate(schema, data, options = {}) {
105
- const shouldFormat = options.format !== false;
106
- const locale = options.locale || Locale.getLocale();
107
-
108
- // 如果指定了语言,临时切换
109
- const originalLocale = options.locale ? Locale.getLocale() : null;
110
- if (options.locale) {
112
+ // 性能优化:提前检查语言切换,避免99%的无效操作
113
+ if (options.locale && options.locale !== Locale.getLocale()) {
114
+ const originalLocale = Locale.getLocale();
111
115
  Locale.setLocale(options.locale);
116
+ try {
117
+ return this._validateInternal(schema, data, options);
118
+ } finally {
119
+ Locale.setLocale(originalLocale);
120
+ }
112
121
  }
113
122
 
114
- // 如果schema是DslBuilder实例,转换为JSON Schema
123
+ // 正常流程(无需切换语言)
124
+ return this._validateInternal(schema, data, options);
125
+ }
126
+
127
+ /**
128
+ * 内部验证方法
129
+ * @private
130
+ */
131
+ _validateInternal(schema, data, options = {}) {
132
+ const shouldFormat = options.format !== false;
133
+ const locale = options.locale || Locale.getLocale();
134
+
135
+ // ✅ 性能优化:缓存 DslBuilder 转换结果
115
136
  if (schema && typeof schema.toSchema === 'function') {
116
- schema = schema.toSchema();
137
+ if (!this.dslSchemaCache.has(schema)) {
138
+ this.dslSchemaCache.set(schema, schema.toSchema());
139
+ }
140
+ schema = this.dslSchemaCache.get(schema);
117
141
  }
118
142
 
119
143
  // 检查是否需要移除额外字段 (clean 模式)
@@ -167,11 +191,6 @@ class Validator {
167
191
  }],
168
192
  data
169
193
  };
170
- } finally {
171
- // 恢复原语言
172
- if (originalLocale) {
173
- Locale.setLocale(originalLocale);
174
- }
175
194
  }
176
195
  }
177
196
 
@@ -338,8 +357,16 @@ class Validator {
338
357
  * @returns {string} 缓存键
339
358
  */
340
359
  _generateCacheKey(schema) {
341
- // 简单实现:使用JSON字符串的hash
342
- // 生产环境建议使用更高效的hash算法
360
+ // ✅ 性能优化:使用 WeakMap 避免昂贵的 JSON.stringify
361
+ // 对于对象类型的 schema,使用 WeakMap 存储唯一标识符
362
+ if (typeof schema === 'object' && schema !== null) {
363
+ if (!this.schemaMap.has(schema)) {
364
+ this.schemaMap.set(schema, `schema_${++this.schemaKeyCounter}`);
365
+ }
366
+ return this.schemaMap.get(schema);
367
+ }
368
+
369
+ // 对于原始类型或 null,降级到字符串化
343
370
  return JSON.stringify(schema);
344
371
  }
345
372
 
@@ -4,7 +4,7 @@
4
4
  * 用于 validateAsync() 方法,验证失败时自动抛出
5
5
  *
6
6
  * @module lib/errors/ValidationError
7
- * @version 2.1.0
7
+ * @version 1.0.3
8
8
  */
9
9
 
10
10
  /**
@@ -119,7 +119,7 @@ class ValidationError extends Error {
119
119
  * }
120
120
  */
121
121
  getFieldError(field) {
122
- // 规范化字段名(移除前导斜杠)
122
+ // 规范化字段名(移除前导斜杠)
123
123
  const normalizedField = field.replace(/^\//, '');
124
124
 
125
125
  // 查找匹配的错误(支持多种路径格式)
@@ -182,7 +182,7 @@ class ValidationError extends Error {
182
182
 
183
183
  // Support calling without new
184
184
  const ValidationErrorProxy = new Proxy(ValidationError, {
185
- apply: function(target, thisArg, argumentsList) {
185
+ apply: function (target, thisArg, argumentsList) {
186
186
  return new target(...argumentsList);
187
187
  }
188
188
  });