schema-dsl 1.1.3 → 1.1.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.
package/index.d.ts CHANGED
@@ -1,9 +1,64 @@
1
- // Type definitions for schema-dsl v1.1.2
1
+ // Type definitions for schema-dsl v1.1.5
2
2
  // Project: https://github.com/vextjs/schema-dsl
3
3
  // Definitions by: schema-dsl Team
4
4
 
5
5
  // ========== 核心类型 ==========
6
6
 
7
+ /**
8
+ * 错误消息配置(字符串或对象)
9
+ *
10
+ * @description v1.1.5 新增:支持对象格式配置错误代码和消息
11
+ *
12
+ * @example 字符串格式(向后兼容)
13
+ * ```typescript
14
+ * const messages = {
15
+ * 'user.notFound': '用户不存在'
16
+ * };
17
+ * ```
18
+ *
19
+ * @example 对象格式(v1.1.5 新增)
20
+ * ```typescript
21
+ * const messages = {
22
+ * 'account.notFound': {
23
+ * code: 'ACCOUNT_NOT_FOUND',
24
+ * message: '账户不存在'
25
+ * }
26
+ * };
27
+ * ```
28
+ *
29
+ * @since v1.1.5
30
+ */
31
+ export type ErrorMessageConfig =
32
+ | string // 向后兼容:'账户不存在'
33
+ | { // 新格式:{ code, message }
34
+ /** 错误代码(可选,默认使用 key) */
35
+ code?: string;
36
+ /** 错误消息(必需) */
37
+ message: string;
38
+ };
39
+
40
+ /**
41
+ * 语言包定义
42
+ *
43
+ * @description 语言包对象,key 为错误代码,value 为错误消息配置
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const zhCN: LocaleMessages = {
48
+ * 'user.notFound': '用户不存在',
49
+ * 'account.notFound': {
50
+ * code: 'ACCOUNT_NOT_FOUND',
51
+ * message: '账户不存在'
52
+ * }
53
+ * };
54
+ * ```
55
+ *
56
+ * @since v1.1.5
57
+ */
58
+ export interface LocaleMessages {
59
+ [key: string]: ErrorMessageConfig;
60
+ }
61
+
7
62
  /**
8
63
  * JSON Schema 对象
9
64
  *
@@ -285,13 +340,24 @@ export class DslBuilder {
285
340
 
286
341
  /**
287
342
  * 构造函数
288
- * @param dslString - DSL字符串(如 'email!', 'string:3-32!', 'types:string|number')
343
+ * @param dslString - DSL字符串(如 'email!', 'string:3-32!', 'string?', 'types:string|number')
289
344
  *
290
345
  * @example 基础类型
291
346
  * ```typescript
292
- * const builder = new DslBuilder('email!');
293
- * const builder2 = new DslBuilder('string:3-32');
294
- * const builder3 = new DslBuilder('types:string|number');
347
+ * const builder = new DslBuilder('email!'); // 必填邮箱
348
+ * const builder2 = new DslBuilder('string:3-32'); // 可选字符串(默认)
349
+ * const builder3 = new DslBuilder('string?'); // 显式可选字符串
350
+ * const builder4 = new DslBuilder('email?'); // 显式可选邮箱
351
+ * const builder5 = new DslBuilder('types:string|number'); // 联合类型
352
+ * ```
353
+ *
354
+ * @example 必填与可选标记
355
+ * ```typescript
356
+ * new DslBuilder('string!') // 必填字符串
357
+ * new DslBuilder('string') // 可选字符串(默认)
358
+ * new DslBuilder('string?') // 显式可选字符串
359
+ * new DslBuilder('email?') // 可选邮箱
360
+ * new DslBuilder('string:3-32?') // 可选字符串,长度3-32
295
361
  * ```
296
362
  *
297
363
  * @example 数字类型比较运算符 (v1.1.2+)
@@ -317,6 +383,7 @@ export class DslBuilder {
317
383
  * // 配合必填标记
318
384
  * new DslBuilder('number:>=18!') // 必填且 >= 18
319
385
  * new DslBuilder('number:>0!') // 必填且 > 0
386
+ * new DslBuilder('number:>0?') // 可选且 > 0(当有值时)
320
387
  * ```
321
388
  */
322
389
  constructor(dslString: string);
@@ -1254,12 +1321,26 @@ export namespace dsl {
1254
1321
  * @param code - 错误代码(多语言 key)
1255
1322
  * @param params - 错误参数
1256
1323
  * @param statusCode - HTTP 状态码
1324
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1257
1325
  * @returns 错误实例
1326
+ *
1327
+ * @example 全局语言
1328
+ * ```typescript
1329
+ * Locale.setLocale('zh-CN');
1330
+ * const error = dsl.error.create('account.notFound');
1331
+ * ```
1332
+ *
1333
+ * @example 运行时指定语言
1334
+ * ```typescript
1335
+ * const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
1336
+ * const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
1337
+ * ```
1258
1338
  */
1259
1339
  create(
1260
1340
  code: string,
1261
1341
  params?: Record<string, any>,
1262
- statusCode?: number
1342
+ statusCode?: number,
1343
+ locale?: string
1263
1344
  ): I18nError;
1264
1345
 
1265
1346
  /**
@@ -1267,12 +1348,25 @@ export namespace dsl {
1267
1348
  * @param code - 错误代码(多语言 key)
1268
1349
  * @param params - 错误参数
1269
1350
  * @param statusCode - HTTP 状态码
1351
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1270
1352
  * @throws I18nError
1353
+ *
1354
+ * @example 全局语言
1355
+ * ```typescript
1356
+ * Locale.setLocale('zh-CN');
1357
+ * dsl.error.throw('account.notFound');
1358
+ * ```
1359
+ *
1360
+ * @example 运行时指定语言
1361
+ * ```typescript
1362
+ * dsl.error.throw('account.notFound', {}, 404, 'en-US');
1363
+ * ```
1271
1364
  */
1272
1365
  throw(
1273
1366
  code: string,
1274
1367
  params?: Record<string, any>,
1275
- statusCode?: number
1368
+ statusCode?: number,
1369
+ locale?: string
1276
1370
  ): never;
1277
1371
 
1278
1372
  /**
@@ -1281,13 +1375,26 @@ export namespace dsl {
1281
1375
  * @param code - 错误代码(多语言 key)
1282
1376
  * @param params - 错误参数
1283
1377
  * @param statusCode - HTTP 状态码
1378
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1284
1379
  * @throws I18nError 条件为 false 时抛出
1380
+ *
1381
+ * @example 全局语言
1382
+ * ```typescript
1383
+ * Locale.setLocale('zh-CN');
1384
+ * dsl.error.assert(account, 'account.notFound');
1385
+ * ```
1386
+ *
1387
+ * @example 运行时指定语言
1388
+ * ```typescript
1389
+ * dsl.error.assert(account, 'account.notFound', {}, 404, 'en-US');
1390
+ * ```
1285
1391
  */
1286
1392
  assert(
1287
1393
  condition: any,
1288
1394
  code: string,
1289
1395
  params?: Record<string, any>,
1290
- statusCode?: number
1396
+ statusCode?: number,
1397
+ locale?: string
1291
1398
  ): asserts condition;
1292
1399
  };
1293
1400
  }
@@ -1648,7 +1755,10 @@ export class I18nError extends Error {
1648
1755
  /** 错误消息(已翻译) */
1649
1756
  message: string;
1650
1757
 
1651
- /** 错误代码(多语言 key */
1758
+ /** 原始 key(v1.1.5 新增) */
1759
+ originalKey: string;
1760
+
1761
+ /** 错误代码(从对象提取或使用 key) */
1652
1762
  code: string;
1653
1763
 
1654
1764
  /** 错误参数(用于插值) */
@@ -1679,12 +1789,30 @@ export class I18nError extends Error {
1679
1789
  * @param code - 错误代码
1680
1790
  * @param params - 错误参数
1681
1791
  * @param statusCode - HTTP 状态码
1792
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1682
1793
  * @returns 错误实例
1794
+ *
1795
+ * @example 全局语言
1796
+ * ```typescript
1797
+ * Locale.setLocale('zh-CN');
1798
+ * const error = I18nError.create('account.notFound');
1799
+ * // message: "账户不存在"
1800
+ * ```
1801
+ *
1802
+ * @example 运行时指定语言
1803
+ * ```typescript
1804
+ * const error1 = I18nError.create('account.notFound', {}, 404, 'zh-CN');
1805
+ * // message: "账户不存在"
1806
+ *
1807
+ * const error2 = I18nError.create('account.notFound', {}, 404, 'en-US');
1808
+ * // message: "Account not found"
1809
+ * ```
1683
1810
  */
1684
1811
  static create(
1685
1812
  code: string,
1686
1813
  params?: Record<string, any>,
1687
- statusCode?: number
1814
+ statusCode?: number,
1815
+ locale?: string
1688
1816
  ): I18nError;
1689
1817
 
1690
1818
  /**
@@ -1692,12 +1820,25 @@ export class I18nError extends Error {
1692
1820
  * @param code - 错误代码
1693
1821
  * @param params - 错误参数
1694
1822
  * @param statusCode - HTTP 状态码
1823
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1695
1824
  * @throws I18nError
1825
+ *
1826
+ * @example 全局语言
1827
+ * ```typescript
1828
+ * Locale.setLocale('zh-CN');
1829
+ * I18nError.throw('account.notFound');
1830
+ * ```
1831
+ *
1832
+ * @example 运行时指定语言
1833
+ * ```typescript
1834
+ * I18nError.throw('account.notFound', {}, 404, 'en-US');
1835
+ * ```
1696
1836
  */
1697
1837
  static throw(
1698
1838
  code: string,
1699
1839
  params?: Record<string, any>,
1700
- statusCode?: number
1840
+ statusCode?: number,
1841
+ locale?: string
1701
1842
  ): never;
1702
1843
 
1703
1844
  /**
@@ -1706,13 +1847,46 @@ export class I18nError extends Error {
1706
1847
  * @param code - 错误代码
1707
1848
  * @param params - 错误参数
1708
1849
  * @param statusCode - HTTP 状态码
1850
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1709
1851
  * @throws I18nError 条件为 false 时抛出
1852
+ *
1853
+ * @example 全局语言
1854
+ * ```typescript
1855
+ * Locale.setLocale('zh-CN');
1856
+ * I18nError.assert(account, 'account.notFound');
1857
+ * ```
1858
+ *
1859
+ * @example 运行时指定语言
1860
+ * ```typescript
1861
+ * I18nError.assert(account, 'account.notFound', {}, 404, 'en-US');
1862
+ * ```
1863
+ */
1864
+ /**
1865
+ * 断言方法 - 条件不满足时抛错
1866
+ * @param condition - 条件表达式
1867
+ * @param code - 错误代码
1868
+ * @param params - 错误参数
1869
+ * @param statusCode - HTTP 状态码
1870
+ * @param locale - 语言环境(可选,不传则使用全局语言)
1871
+ * @throws I18nError 条件为 false 时抛出
1872
+ *
1873
+ * @example 全局语言
1874
+ * ```typescript
1875
+ * Locale.setLocale('zh-CN');
1876
+ * I18nError.assert(account, 'account.notFound');
1877
+ * ```
1878
+ *
1879
+ * @example 运行时指定语言
1880
+ * ```typescript
1881
+ * I18nError.assert(account, 'account.notFound', {}, 404, 'en-US');
1882
+ * ```
1710
1883
  */
1711
1884
  static assert(
1712
1885
  condition: any,
1713
1886
  code: string,
1714
1887
  params?: Record<string, any>,
1715
- statusCode?: number
1888
+ statusCode?: number,
1889
+ locale?: string
1716
1890
  ): asserts condition;
1717
1891
 
1718
1892
  /**
@@ -1725,9 +1899,26 @@ export class I18nError extends Error {
1725
1899
  /**
1726
1900
  * 转为 JSON 格式(用于 API 响应)
1727
1901
  * @returns JSON 对象
1902
+ *
1903
+ * @example
1904
+ * ```typescript
1905
+ * const json = error.toJSON();
1906
+ * // {
1907
+ * // error: 'I18nError',
1908
+ * // originalKey: 'account.notFound', // v1.1.5 新增
1909
+ * // code: 'ACCOUNT_NOT_FOUND',
1910
+ * // message: '账户不存在',
1911
+ * // params: {},
1912
+ * // statusCode: 400,
1913
+ * // locale: 'zh-CN'
1914
+ * // }
1915
+ * ```
1916
+ *
1917
+ * @since v1.1.5 - 新增 originalKey 字段
1728
1918
  */
1729
1919
  toJSON(): {
1730
1920
  error: string;
1921
+ originalKey: string; // v1.1.5 新增
1731
1922
  code: string;
1732
1923
  message: string;
1733
1924
  params: Record<string, any>;
package/index.js CHANGED
@@ -59,18 +59,20 @@ dsl.error = {
59
59
  * @param {string} code - 错误代码(多语言 key)
60
60
  * @param {Object} params - 错误参数
61
61
  * @param {number} statusCode - HTTP 状态码
62
+ * @param {string} locale - 语言环境(可选,不传则使用全局语言)
62
63
  * @returns {I18nError} 错误实例
63
64
  */
64
- create: (code, params, statusCode) => I18nError.create(code, params, statusCode),
65
+ create: (code, params, statusCode, locale) => I18nError.create(code, params, statusCode, locale),
65
66
 
66
67
  /**
67
68
  * 抛出多语言错误
68
69
  * @param {string} code - 错误代码(多语言 key)
69
70
  * @param {Object} params - 错误参数
70
71
  * @param {number} statusCode - HTTP 状态码
72
+ * @param {string} locale - 语言环境(可选,不传则使用全局语言)
71
73
  * @throws {I18nError} 直接抛出错误
72
74
  */
73
- throw: (code, params, statusCode) => I18nError.throw(code, params, statusCode),
75
+ throw: (code, params, statusCode, locale) => I18nError.throw(code, params, statusCode, locale),
74
76
 
75
77
  /**
76
78
  * 断言方法 - 条件不满足时抛错
@@ -78,8 +80,9 @@ dsl.error = {
78
80
  * @param {string} code - 错误代码(多语言 key)
79
81
  * @param {Object} params - 错误参数
80
82
  * @param {number} statusCode - HTTP 状态码
83
+ * @param {string} locale - 语言环境(可选,不传则使用全局语言)
81
84
  */
82
- assert: (condition, code, params, statusCode) => I18nError.assert(condition, code, params, statusCode)
85
+ assert: (condition, code, params, statusCode, locale) => I18nError.assert(condition, code, params, statusCode, locale)
83
86
  };
84
87
 
85
88
  /**
package/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import schema-dsl from './index.js';
1
+ import schemaDsl from './index.js';
2
2
 
3
3
  export const {
4
4
  dsl,
@@ -26,5 +26,5 @@ export const {
26
26
  uninstallStringExtensions
27
27
  } = schema-dsl;
28
28
 
29
- export default schema-dsl;
29
+ export default schemaDsl;
30
30
 
@@ -123,11 +123,20 @@ class DslBuilder {
123
123
  processedDsl = trimmed.replace(/^array!/, 'array:') + '!';
124
124
  }
125
125
 
126
+ // 🔴 处理必填标记 ! 和可选标记 ?
127
+ // 优先级:! > ?(如果同时存在,! 优先)
126
128
  this._required = processedDsl.endsWith('!');
127
- const dslWithoutRequired = this._required ? processedDsl.slice(0, -1) : processedDsl;
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
+ }
128
137
 
129
138
  // 简单解析为基础Schema(避免循环依赖)
130
- this._baseSchema = this._parseSimple(dslWithoutRequired);
139
+ this._baseSchema = this._parseSimple(dslWithoutMarker);
131
140
 
132
141
  // 扩展属性
133
142
  this._customMessages = {};
@@ -62,11 +62,12 @@ class Locale {
62
62
  }
63
63
 
64
64
  /**
65
- * 获取错误消息模板
65
+ * 获取错误消息配置
66
66
  * @param {string} type - 错误类型或消息字符串
67
67
  * @param {Object} [customMessages] - 自定义消息
68
68
  * @param {string} [locale] - 指定语言(可选,默认使用当前语言)
69
- * @returns {string} 消息模板
69
+ * @returns {Object|string} 消息配置对象 { code, message } 或字符串(向后兼容)
70
+ * @version 1.1.5 - 支持对象格式
70
71
  */
71
72
  static getMessage(type, customMessages = {}, locale = null) {
72
73
  // 使用指定的语言或当前全局语言
@@ -74,32 +75,37 @@ class Locale {
74
75
 
75
76
  // 优先级: 自定义消息 > 全局自定义消息 > 语言包 > ErrorCodes > 原字符串
76
77
 
77
- // 1. 自定义消息
78
- if (customMessages[type]) {
79
- return customMessages[type];
78
+ // 1. 查找消息配置
79
+ let messageConfig = customMessages[type]
80
+ || this.customMessages[type]
81
+ || (this.locales[targetLocale] && this.locales[targetLocale][type]);
82
+
83
+ // 2. 如果未找到,尝试从 ErrorCodes 获取
84
+ if (!messageConfig) {
85
+ const errorInfo = getErrorInfo(type);
86
+ if (errorInfo.code === 'UNKNOWN_ERROR') {
87
+ // ✅ 向后兼容:直接返回原字符串(支持硬编码消息)
88
+ return type;
89
+ }
90
+ messageConfig = errorInfo.message;
80
91
  }
81
92
 
82
- // 2. 全局自定义消息
83
- if (this.customMessages[type]) {
84
- return this.customMessages[type];
93
+ // 3. 规范化为对象格式(v1.1.5 新增)
94
+ if (typeof messageConfig === 'string') {
95
+ // 字符串格式 → 转换为对象格式(向后兼容)
96
+ return { code: type, message: messageConfig };
85
97
  }
86
98
 
87
- // 3. 语言包(使用指定的语言)
88
- const localeMessages = this.locales[targetLocale];
89
- if (localeMessages && localeMessages[type]) {
90
- return localeMessages[type];
99
+ if (typeof messageConfig === 'object' && messageConfig !== null && messageConfig.message) {
100
+ // 对象格式 直接使用
101
+ return {
102
+ code: messageConfig.code || type, // 无 code 时使用 type 作为 code
103
+ message: messageConfig.message
104
+ };
91
105
  }
92
106
 
93
- // 4. 默认消息(从ErrorCodes获取)
94
- const errorInfo = getErrorInfo(type);
95
-
96
- // 5. 如果是未知错误,说明不是预定义的错误码
97
- if (errorInfo.code === 'UNKNOWN_ERROR') {
98
- // ✅ 向后兼容:直接返回原字符串(支持硬编码消息)
99
- return type;
100
- }
101
-
102
- return errorInfo.message;
107
+ // 4. 降级处理(边界情况)
108
+ return { code: type, message: type };
103
109
  }
104
110
 
105
111
  /**
@@ -29,32 +29,41 @@ class MessageTemplate {
29
29
  render(context = {}) {
30
30
  let message = this.template;
31
31
 
32
- // 替换所有模板变量
33
- message = message.replace(/\{\{#(\w+)\}\}/g, (match, key) => {
34
- const value = context[key];
32
+ // 定义支持的模板格式(按优先级)
33
+ const patterns = [
34
+ /\{\{#(\w+)\}\}/g, // {{#variable}} - 优先级1(现有格式)
35
+ /\{\{(\w+)\}\}/g, // {{variable}} - 优先级2(无井号)
36
+ /\{(\w+)\}/g // {variable} - 优先级3(单花括号)
37
+ ];
35
38
 
36
- // 特殊处理
37
- if (value === undefined || value === null) {
38
- return match; // 保留原样
39
- }
39
+ // 按优先级依次替换
40
+ for (const pattern of patterns) {
41
+ message = message.replace(pattern, (match, key) => {
42
+ const value = context[key];
40
43
 
41
- // 数组转字符串
42
- if (Array.isArray(value)) {
43
- return value.join(', ');
44
- }
44
+ // 特殊处理
45
+ if (value === undefined || value === null) {
46
+ return match; // 保留原样
47
+ }
45
48
 
46
- // RegExp转字符串
47
- if (value instanceof RegExp) {
48
- return value.toString();
49
- }
49
+ // 数组转字符串
50
+ if (Array.isArray(value)) {
51
+ return value.join(', ');
52
+ }
50
53
 
51
- // Date转字符串
52
- if (value instanceof Date) {
53
- return value.toISOString();
54
- }
54
+ // RegExp转字符串
55
+ if (value instanceof RegExp) {
56
+ return value.toString();
57
+ }
55
58
 
56
- return String(value);
57
- });
59
+ // Date转字符串
60
+ if (value instanceof Date) {
61
+ return value.toISOString();
62
+ }
63
+
64
+ return String(value);
65
+ });
66
+ }
58
67
 
59
68
  return message;
60
69
  }
@@ -477,7 +477,12 @@ class Validator {
477
477
  const errorMsg = failedMessage || cond.message;
478
478
  // 支持多语言:如果 message 是 key(如 'conditional.underAge'),从语言包获取翻译
479
479
  // 传递 locale 参数以支持动态语言切换
480
- const errorMessage = Locale.getMessage(errorMsg, options.messages || {}, locale);
480
+ const messageConfig = Locale.getMessage(errorMsg, options.messages || {}, locale);
481
+
482
+ // v1.1.5: Locale.getMessage 返回对象 { code, message },需要提取 message
483
+ const errorMessage = typeof messageConfig === 'object' && messageConfig.message
484
+ ? messageConfig.message
485
+ : messageConfig;
481
486
 
482
487
  return {
483
488
  valid: false,