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.
@@ -688,16 +688,261 @@ if (!result.valid) {
688
688
 
689
689
  ---
690
690
 
691
+ ## v1.1.5 新功能:对象格式错误配置
692
+
693
+ ### 概述
694
+
695
+ 从 v1.1.5 开始,语言包支持对象格式 `{ code, message }`,实现统一的错误代码管理。
696
+
697
+ ### 基础用法
698
+
699
+ **语言包配置**:
700
+ ```javascript
701
+ // lib/locales/zh-CN.js (或自定义语言包)
702
+ module.exports = {
703
+ // 字符串格式(向后兼容)
704
+ 'user.notFound': '用户不存在',
705
+
706
+ // 对象格式(v1.1.5 新增)✨ - 使用数字错误码
707
+ 'account.notFound': {
708
+ code: 40001,
709
+ message: '账户不存在'
710
+ },
711
+ 'account.insufficientBalance': {
712
+ code: 40002,
713
+ message: '余额不足,当前余额{{#balance}},需要{{#required}}'
714
+ },
715
+ 'order.notPaid': {
716
+ code: 50001,
717
+ message: '订单未支付'
718
+ }
719
+ };
720
+ ```
721
+
722
+ **使用示例**:
723
+ ```javascript
724
+ const { dsl } = require('schema-dsl');
725
+
726
+ try {
727
+ dsl.error.throw('account.notFound');
728
+ } catch (error) {
729
+ console.log(error.originalKey); // 'account.notFound'
730
+ console.log(error.code); // 40001 ✨ 数字错误码
731
+ console.log(error.message); // '账户不存在'
732
+ }
733
+ ```
734
+
735
+ ### 核心特性
736
+
737
+ #### 1. originalKey 字段(新增)
738
+
739
+ 保留原始的 key,便于调试和日志追踪:
740
+
741
+ ```javascript
742
+ try {
743
+ dsl.error.throw('account.notFound');
744
+ } catch (error) {
745
+ error.originalKey // 'account.notFound' (原始 key)
746
+ error.code // 40001 (数字错误码)
747
+ }
748
+ ```
749
+
750
+ #### 2. 多语言共享 code
751
+
752
+ 不同语言使用相同的数字 `code`,便于前端统一处理:
753
+
754
+ ```javascript
755
+ // zh-CN.js
756
+ 'account.notFound': {
757
+ code: 40001, // ← 数字 code 一致
758
+ message: '账户不存在'
759
+ }
760
+
761
+ // en-US.js
762
+ 'account.notFound': {
763
+ code: 40001, // ← 数字 code 一致
764
+ message: 'Account not found'
765
+ }
766
+
767
+ // 前端处理 - 不受语言影响
768
+ switch (error.code) {
769
+ case 40001:
770
+ redirectToLogin();
771
+ break;
772
+ case 40002:
773
+ showTopUpDialog();
774
+ break;
775
+ case 50001:
776
+ showPaymentDialog();
777
+ break;
778
+ }
779
+ #### 3. 增强的 error.is() 方法
780
+
781
+ 同时支持 `originalKey` 和数字 `code` 判断:
782
+
783
+ ```javascript
784
+ try {
785
+ dsl.error.throw('account.notFound');
786
+ } catch (error) {
787
+ // 两种方式都可以
788
+ if (error.is('account.notFound')) { } // ✅ 使用 originalKey
789
+ if (error.is(40001)) { } // ✅ 使用数字 code
790
+ }
791
+ ```
792
+
793
+ #### 4. toJSON 包含 originalKey
794
+
795
+ ```javascript
796
+ const json = error.toJSON();
797
+ // {
798
+ // error: 'I18nError',
799
+ // originalKey: 'account.notFound', // ✨ v1.1.5 新增
800
+ // code: 'ACCOUNT_NOT_FOUND',
801
+ // message: '账户不存在',
802
+ // params: {},
803
+ // statusCode: 400,
804
+ // locale: 'zh-CN'
805
+ // }
806
+ ```
807
+
808
+ ### 向后兼容
809
+
810
+ **完全向后兼容** ✅ - 字符串格式自动转换:
811
+
812
+ ```javascript
813
+ // 字符串格式(原有)
814
+ 'user.notFound': '用户不存在'
815
+
816
+ // 自动转换为对象
817
+ dsl.error.throw('user.notFound');
818
+ // error.code = 'user.notFound' (使用 key 作为 code)
819
+ // error.originalKey = 'user.notFound'
820
+ // error.message = '用户不存在'
821
+ ```
822
+
823
+ ### 最佳实践
824
+
825
+ #### 1. 何时使用对象格式
826
+
827
+ **推荐使用对象格式**:
828
+ - ✅ 需要在多语言中统一处理的错误
829
+ - ✅ 需要前端统一判断的错误
830
+ - ✅ 核心业务错误(账户、订单、支付等)
831
+
832
+ **可以使用字符串格式**:
833
+ - ✅ 简单的验证错误
834
+ - ✅ 内部错误(不暴露给前端)
835
+ - ✅ 不需要统一处理的错误
836
+
837
+ #### 2. 错误代码命名规范
838
+
839
+ 推荐使用**数字错误码**,按模块分段:
840
+
841
+ ```javascript
842
+ // 错误码规范(5位数字)
843
+ // 4xxxx - 客户端错误
844
+ // 5xxxx - 业务逻辑错误
845
+ // 6xxxx - 系统错误
846
+
847
+ 'account.notFound': {
848
+ code: 40001, // ✅ 推荐:账户模块,序号001
849
+ message: '账户不存在'
850
+ }
851
+
852
+ 'account.insufficientBalance': {
853
+ code: 40002, // 账户模块,序号002
854
+ message: '余额不足'
855
+ }
856
+
857
+ 'order.notPaid': {
858
+ code: 50001, // ✅ 订单模块,序号001
859
+ message: '订单未支付'
860
+ }
861
+
862
+ 'order.cancelled': {
863
+ code: 50002, // 订单模块,序号002
864
+ message: '订单已取消'
865
+ }
866
+
867
+ 'database.connectionError': {
868
+ code: 60001, // ✅ 系统错误
869
+ message: '数据库连接失败'
870
+ }
871
+ ```
872
+
873
+ **错误码分段建议**:
874
+ - `40001-49999` - 客户端错误(账户、权限、参数验证等)
875
+ - `50001-59999` - 业务逻辑错误(订单、支付、库存等)
876
+ - `60001-69999` - 系统错误(数据库、服务不可用等)
877
+
878
+ #### 3. 前端统一错误处理
879
+
880
+ ```javascript
881
+ // API 调用
882
+ try {
883
+ const response = await fetch('/api/account');
884
+ const data = await response.json();
885
+ } catch (error) {
886
+ // 使用数字 code 统一处理,不受语言影响
887
+ switch (error.code) {
888
+ case 40001: // ACCOUNT_NOT_FOUND
889
+ showNotFoundPage();
890
+ break;
891
+ case 40002: // INSUFFICIENT_BALANCE
892
+ showTopUpDialog(error.params);
893
+ break;
894
+ case 50001: // ORDER_NOT_PAID
895
+ showPaymentDialog();
896
+ break;
897
+ case 60001: // SYSTEM_ERROR
898
+ showSystemErrorPage();
899
+ break;
900
+ default:
901
+ showGenericError(error.message);
902
+ }
903
+ }
904
+ ```
905
+
906
+ **更优雅的方式 - 错误码映射**:
907
+ ```javascript
908
+ // errorCodeMap.js
909
+ const ERROR_HANDLERS = {
910
+ 40001: () => router.push('/account-not-found'),
911
+ 40002: (error) => showDialog('topup', error.params),
912
+ 50001: (error) => showDialog('payment', error.params),
913
+ 60001: () => showSystemErrorPage(),
914
+ };
915
+
916
+ // 统一错误处理
917
+ function handleError(error) {
918
+ const handler = ERROR_HANDLERS[error.code];
919
+ if (handler) {
920
+ handler(error);
921
+ } else {
922
+ showGenericError(error.message);
923
+ }
924
+ }
925
+ ```
926
+
927
+ ### 更多信息
928
+
929
+ - [v1.1.5 完整变更日志](../changelogs/v1.1.5.md)
930
+ - [升级指南](../changelogs/v1.1.5.md#升级指南)
931
+ - [最佳实践](../changelogs/v1.1.5.md#最佳实践)
932
+
933
+ ---
934
+
691
935
  ## 相关文档
692
936
 
693
937
  - [API 参考文档](./api-reference.md)
694
938
  - [DSL 语法指南](./dsl-syntax.md)
695
939
  - [String 扩展文档](./string-extensions.md)
696
940
  - [多语言配置](./dynamic-locale.md)
941
+ - [v1.1.5 变更日志](../changelogs/v1.1.5.md)
697
942
 
698
943
  ---
699
944
 
700
-
701
- **最后更新**: 2025-12-25
945
+ **最后更新**: 2026-01-17
946
+ **版本**: v1.1.5
702
947
 
703
948
 
@@ -0,0 +1,321 @@
1
+ # schema-dsl 可选标记 ? 支持
2
+
3
+ **版本**: v1.1.4+
4
+ **更新日期**: 2026-01-13
5
+
6
+ ---
7
+
8
+ ## 📋 功能概述
9
+
10
+ schema-dsl 现在支持使用 `?` 显式标记可选字段,提供更清晰的语义表达。
11
+
12
+ ### 支持的标记
13
+
14
+ | 标记 | 含义 | 示例 | 说明 |
15
+ |------|------|------|------|
16
+ | `!` | 必填 | `string!` | 字段不能为空 |
17
+ | `?` | 可选 | `string?` | 字段可以为空(显式表达) |
18
+ | 无标记 | 可选(默认) | `string` | 字段可以为空(默认行为) |
19
+
20
+ ---
21
+
22
+ ## ✅ 支持的语法
23
+
24
+ ### 1. 基础类型 + ?
25
+
26
+ ```javascript
27
+ const { dsl, validate } = require('schema-dsl');
28
+
29
+ const schema = dsl({
30
+ username: 'string!', // 必填字符串
31
+ nickname: 'string', // 可选字符串(默认)
32
+ bio: 'string?', // 显式可选字符串
33
+ email: 'email?' // 可选邮箱
34
+ });
35
+
36
+ // 验证
37
+ validate(schema, {}); // ✅ 通过(只有username必填)
38
+ validate(schema, { username: 'test' }); // ✅ 通过
39
+ validate(schema, { username: 'test', bio: 'hi' }); // ✅ 通过
40
+ validate(schema, { username: 'test', email: 'invalid' }); // ❌ 失败(email格式错误)
41
+ ```
42
+
43
+ ### 2. 带约束的类型 + ?
44
+
45
+ ```javascript
46
+ const schema = dsl({
47
+ username: 'string:3-32!', // 必填,长度3-32
48
+ nickname: 'string:3-32?', // 可选,有值时长度3-32
49
+ age: 'number:18-?', // 可选,有值时≥18
50
+ score: 'number:0-100?' // 可选,有值时0-100
51
+ });
52
+
53
+ validate(schema, { username: 'test' }); // ✅ 通过
54
+ validate(schema, { username: 'test', age: 16 }); // ❌ 失败(age<18)
55
+ validate(schema, { username: 'test', age: 20 }); // ✅ 通过
56
+ ```
57
+
58
+ ### 3. 格式类型 + ?
59
+
60
+ ```javascript
61
+ const schema = dsl({
62
+ email: 'email?', // 可选邮箱
63
+ url: 'url?', // 可选URL
64
+ uuid: 'uuid?', // 可选UUID
65
+ date: 'date?', // 可选日期
66
+ phone: 'phone:cn?' // 可选中国手机号
67
+ });
68
+
69
+ validate(schema, {}); // ✅ 通过(全部可选)
70
+ validate(schema, { email: 'test@example.com' }); // ✅ 通过
71
+ validate(schema, { email: 'invalid' }); // ❌ 失败(格式错误)
72
+ ```
73
+
74
+ ### 4. 数组类型 + ?
75
+
76
+ ```javascript
77
+ const schema = dsl({
78
+ tags: 'array<string>?', // 可选字符串数组
79
+ items: 'array:1-10?', // 可选数组,长度1-10
80
+ numbers: 'array<number>?' // 可选数字数组
81
+ });
82
+
83
+ validate(schema, {}); // ✅ 通过
84
+ validate(schema, { tags: ['a', 'b'] }); // ✅ 通过
85
+ validate(schema, { tags: [] }); // ✅ 通过(空数组)
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 🎯 语义对比
91
+
92
+ ### string vs string?
93
+
94
+ 虽然两者行为相同(都是可选),但语义不同:
95
+
96
+ ```javascript
97
+ // 方式1: 隐式可选(默认)
98
+ const schema1 = dsl({
99
+ nickname: 'string'
100
+ });
101
+
102
+ // 方式2: 显式可选(推荐)
103
+ const schema2 = dsl({
104
+ nickname: 'string?'
105
+ });
106
+ ```
107
+
108
+ **推荐使用 `?` 的场景**:
109
+ - 需要明确表达"此字段是故意设计为可选的"
110
+ - 与其他必填字段对比时,增强代码可读性
111
+ - 团队规范要求显式标记可选字段
112
+
113
+ **示例**:
114
+
115
+ ```javascript
116
+ // ❌ 不清晰:哪些是有意可选?哪些是遗漏了必填标记?
117
+ const schema = dsl({
118
+ username: 'string!',
119
+ nickname: 'string',
120
+ bio: 'string',
121
+ email: 'email!'
122
+ });
123
+
124
+ // ✅ 清晰:明确表达设计意图
125
+ const schema = dsl({
126
+ username: 'string!', // 必填
127
+ nickname: 'string?', // 可选
128
+ bio: 'string?', // 可选
129
+ email: 'email!' // 必填
130
+ });
131
+ ```
132
+
133
+ ---
134
+
135
+ ## ⚠️ 注意事项
136
+
137
+ ### 1. 枚举类型中的 ?
138
+
139
+ 当 `?` 出现在枚举值中时,需要特别注意:
140
+
141
+ ```javascript
142
+ // ❌ 错误:? 会被当作枚举值的一部分
143
+ const schema1 = dsl({
144
+ status: 'active|inactive?'
145
+ });
146
+ // 解析为: enum ['active', 'inactive?']
147
+ // 'inactive' 会验证失败!
148
+
149
+ // ✅ 正确:枚举默认就是可选的
150
+ const schema2 = dsl({
151
+ status: 'active|inactive'
152
+ });
153
+
154
+ // ✅ 正确:枚举必填时使用 !
155
+ const schema3 = dsl({
156
+ status: 'active|inactive!'
157
+ });
158
+ ```
159
+
160
+ ### 2. 优先级规则
161
+
162
+ 当 `!` 和 `?` 同时出现时(虽然不推荐),`!` 优先:
163
+
164
+ ```javascript
165
+ // ⚠️ 不推荐:同时使用 ! 和 ?
166
+ const schema = dsl({
167
+ field: 'string!?' // ! 优先,字段必填
168
+ });
169
+ ```
170
+
171
+ ### 3. 对象字段的可选
172
+
173
+ ```javascript
174
+ // 对象本身可选,内部字段必填
175
+ const schema1 = dsl({
176
+ user: {
177
+ name: 'string!', // 当user存在时,name必填
178
+ email: 'email!' // 当user存在时,email必填
179
+ }
180
+ });
181
+
182
+ // 对象本身可选(显式),内部字段必填
183
+ const schema2 = dsl({
184
+ 'user?': { // 显式可选
185
+ name: 'string!',
186
+ email: 'email!'
187
+ }
188
+ });
189
+
190
+ // 对象本身必填,内部字段可选
191
+ const schema3 = dsl({
192
+ 'user!': { // 对象必填
193
+ name: 'string?', // 可选
194
+ email: 'email?' // 可选
195
+ }
196
+ });
197
+ ```
198
+
199
+ ---
200
+
201
+ ## 📊 实际测试结果
202
+
203
+ ### 测试统计
204
+
205
+ - ✅ **string?** - 支持
206
+ - ✅ **string:3-32?** - 支持
207
+ - ✅ **email?** - 支持
208
+ - ✅ **number:18-?** - 支持
209
+ - ✅ **array<string>?** - 支持
210
+ - ✅ **所有单元测试通过** - 949/949
211
+
212
+ ### 测试代码
213
+
214
+ ```javascript
215
+ const { dsl, validate } = require('schema-dsl');
216
+
217
+ // 测试1: string?
218
+ const schema1 = dsl({ name: 'string?' });
219
+ console.log(validate(schema1, {}).valid); // true
220
+ console.log(validate(schema1, { name: 'test' }).valid); // true
221
+
222
+ // 测试2: email?
223
+ const schema2 = dsl({ email: 'email?' });
224
+ console.log(validate(schema2, {}).valid); // true
225
+ console.log(validate(schema2, { email: 'test@ex.com' }).valid); // true
226
+ console.log(validate(schema2, { email: 'invalid' }).valid); // false ✅
227
+
228
+ // 测试3: string:3-32?
229
+ const schema3 = dsl({ username: 'string:3-32?' });
230
+ console.log(validate(schema3, {}).valid); // true
231
+ console.log(validate(schema3, { username: 'ab' }).valid); // false ✅
232
+ console.log(validate(schema3, { username: 'test' }).valid); // true
233
+ ```
234
+
235
+ ---
236
+
237
+ ## 🔧 实现细节
238
+
239
+ ### DslBuilder 构造函数
240
+
241
+ ```javascript
242
+ constructor(dslString) {
243
+ // ...
244
+
245
+ // 🔴 处理必填标记 ! 和可选标记 ?
246
+ // 优先级:! > ?(如果同时存在,! 优先)
247
+ this._required = processedDsl.endsWith('!');
248
+ this._optional = processedDsl.endsWith('?') && !this._required;
249
+
250
+ let dslWithoutMarker = processedDsl;
251
+ if (this._required) {
252
+ dslWithoutMarker = processedDsl.slice(0, -1);
253
+ } else if (this._optional) {
254
+ dslWithoutMarker = processedDsl.slice(0, -1);
255
+ }
256
+
257
+ // ...
258
+ }
259
+ ```
260
+
261
+ ---
262
+
263
+ ## 📝 最佳实践
264
+
265
+ ### 推荐的使用方式
266
+
267
+ ```javascript
268
+ const { dsl } = require('schema-dsl');
269
+
270
+ // ✅ 推荐:显式标记所有字段
271
+ const schema = dsl({
272
+ // 必填字段 - 使用 !
273
+ username: 'string:3-32!',
274
+ password: 'string:8-!',
275
+ email: 'email!',
276
+
277
+ // 可选字段 - 使用 ?
278
+ nickname: 'string:3-32?',
279
+ bio: 'string:500?',
280
+ avatar: 'url?',
281
+ phone: 'phone:cn?',
282
+
283
+ // 对象字段
284
+ 'profile!': { // 对象必填
285
+ age: 'number:18-?', // 年龄可选
286
+ gender: 'male|female|other?', // 性别可选
287
+ }
288
+ });
289
+ ```
290
+
291
+ ### 代码审查清单
292
+
293
+ 在代码审查时,检查以下事项:
294
+
295
+ - [ ] 所有必填字段都使用 `!` 标记
296
+ - [ ] 可选字段根据团队规范决定是否使用 `?`
297
+ - [ ] 枚举类型中没有错误地使用 `?`(如 `active|inactive?`)
298
+ - [ ] 复杂约束的可选字段正确使用(如 `string:3-32?`)
299
+
300
+ ---
301
+
302
+ ## 🔄 版本兼容性
303
+
304
+ - **v1.1.3 及之前**:`?` 被忽略,但不影响功能(因为默认可选)
305
+ - **v1.1.4+**:`?` 被显式处理,语义更清晰
306
+
307
+ **向后兼容**:✅ 完全兼容,所有现有代码无需修改
308
+
309
+ ---
310
+
311
+ ## 📚 相关文档
312
+
313
+ - [DSL 语法完整指南](./dsl-syntax.md)
314
+ - [TypeScript 类型定义](../index.d.ts)
315
+ - [单元测试](../test/unit/dsl-adapter.test.js)
316
+
317
+ ---
318
+
319
+ **最后更新**: 2026-01-13
320
+ **作者**: schema-dsl Team
321
+