schema-dsl 1.2.4 → 2.0.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.
Files changed (242) hide show
  1. package/CHANGELOG.md +87 -210
  2. package/README.md +391 -2249
  3. package/dist/DslBuilder-DQDN0ZxZ.d.cts +341 -0
  4. package/dist/DslBuilder-DkLaOo9Q.d.ts +341 -0
  5. package/dist/Validator-C7GsVQOH.d.cts +192 -0
  6. package/dist/Validator-hFWKGxir.d.ts +192 -0
  7. package/dist/index.cjs +6594 -0
  8. package/dist/index.d.cts +1145 -0
  9. package/dist/index.d.ts +1145 -0
  10. package/dist/index.js +6528 -0
  11. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  12. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  13. package/dist/plugins/custom-format.cjs +3802 -0
  14. package/dist/plugins/custom-format.d.cts +12 -0
  15. package/dist/plugins/custom-format.d.ts +12 -0
  16. package/dist/plugins/custom-format.js +3772 -0
  17. package/dist/plugins/custom-type-example.cjs +3795 -0
  18. package/dist/plugins/custom-type-example.d.cts +8 -0
  19. package/dist/plugins/custom-type-example.d.ts +8 -0
  20. package/dist/plugins/custom-type-example.js +3765 -0
  21. package/dist/plugins/custom-validator.cjs +146 -0
  22. package/dist/plugins/custom-validator.d.cts +10 -0
  23. package/dist/plugins/custom-validator.d.ts +10 -0
  24. package/dist/plugins/custom-validator.js +121 -0
  25. package/docs/FEATURE-INDEX.md +102 -68
  26. package/docs/add-custom-locale.md +48 -35
  27. package/docs/add-keyword.md +24 -0
  28. package/docs/api-reference.md +396 -154
  29. package/docs/api.md +13 -0
  30. package/docs/best-practices-project-structure.md +19 -10
  31. package/docs/best-practices.md +93 -53
  32. package/docs/cache-manager.md +23 -15
  33. package/docs/compile.md +45 -0
  34. package/docs/conditional-api.md +40 -11
  35. package/docs/custom-extensions-guide.md +80 -152
  36. package/docs/design-philosophy.md +76 -71
  37. package/docs/doc-index.md +324 -0
  38. package/docs/dsl-syntax.md +69 -19
  39. package/docs/dynamic-locale.md +24 -14
  40. package/docs/enum.md +12 -5
  41. package/docs/error-handling.md +53 -44
  42. package/docs/export-guide.md +47 -8
  43. package/docs/export-limitations.md +27 -11
  44. package/docs/faq.md +86 -67
  45. package/docs/frontend-i18n-guide.md +26 -12
  46. package/docs/i18n-user-guide.md +60 -47
  47. package/docs/i18n.md +51 -32
  48. package/docs/index.md +48 -0
  49. package/docs/json-schema-basics.md +40 -0
  50. package/docs/label-vs-description.md +12 -3
  51. package/docs/markdown-exporter.md +15 -6
  52. package/docs/mongodb-exporter.md +11 -4
  53. package/docs/multi-language.md +26 -0
  54. package/docs/multi-type-support.md +26 -33
  55. package/docs/mysql-exporter.md +9 -2
  56. package/docs/number-operators.md +12 -5
  57. package/docs/optional-marker-guide.md +28 -23
  58. package/docs/performance-guide.md +49 -0
  59. package/docs/plugin-system.md +205 -366
  60. package/docs/plugin-type-registration.md +34 -0
  61. package/docs/postgresql-exporter.md +9 -2
  62. package/docs/public/favicon.svg +5 -0
  63. package/docs/quick-start.md +37 -363
  64. package/docs/runtime-locale-support.md +20 -9
  65. package/docs/schema-helper.md +10 -5
  66. package/docs/schema-utils-advanced-issues.md +23 -0
  67. package/docs/schema-utils-best-practices.md +20 -0
  68. package/docs/schema-utils-chaining.md +7 -0
  69. package/docs/schema-utils.md +76 -42
  70. package/docs/security-checklist.md +20 -0
  71. package/docs/string-extensions.md +17 -9
  72. package/docs/troubleshooting.md +36 -21
  73. package/docs/type-converter.md +41 -50
  74. package/docs/type-reference.md +38 -15
  75. package/docs/typescript-guide.md +53 -42
  76. package/docs/union-type-guide.md +11 -1
  77. package/docs/union-types.md +10 -3
  78. package/docs/validate-async.md +36 -25
  79. package/docs/validate-batch.md +49 -0
  80. package/docs/validate-dsl-object-support.md +33 -28
  81. package/docs/validate.md +36 -16
  82. package/docs/validation-guide.md +25 -7
  83. package/docs/validator.md +39 -0
  84. package/package.json +85 -27
  85. package/plugins/custom-format.cjs +8 -0
  86. package/plugins/custom-type-example.cjs +8 -0
  87. package/plugins/custom-validator.cjs +8 -0
  88. package/src/adapters/DslAdapter.ts +111 -0
  89. package/src/adapters/index.ts +1 -0
  90. package/src/config/constants.ts +83 -0
  91. package/src/config/index.ts +2 -0
  92. package/src/config/patterns.ts +77 -0
  93. package/src/core/CacheManager.ts +159 -0
  94. package/src/core/ConditionalBuilder.ts +382 -0
  95. package/src/core/ConditionalRuntime.ts +28 -0
  96. package/src/core/ConditionalValidator.ts +255 -0
  97. package/src/core/DslBuilder.ts +677 -0
  98. package/src/core/ErrorCodes.ts +38 -0
  99. package/src/core/ErrorFormatter.ts +271 -0
  100. package/src/core/JSONSchemaCore.ts +65 -0
  101. package/src/core/Locale.ts +187 -0
  102. package/src/core/MessageTemplate.ts +42 -0
  103. package/src/core/ObjectDslBuilder.ts +64 -0
  104. package/src/core/PluginManager.ts +326 -0
  105. package/src/core/StringExtensions.ts +140 -0
  106. package/src/core/TemplateEngine.ts +44 -0
  107. package/src/core/Validator.ts +448 -0
  108. package/src/errors/I18nError.ts +159 -0
  109. package/src/errors/ValidationError.ts +105 -0
  110. package/src/exporters/BaseExporter.ts +60 -0
  111. package/src/exporters/MarkdownExporter.ts +305 -0
  112. package/src/exporters/MongoDBExporter.ts +126 -0
  113. package/src/exporters/MySQLExporter.ts +155 -0
  114. package/src/exporters/PostgreSQLExporter.ts +222 -0
  115. package/src/exporters/index.ts +18 -0
  116. package/src/index.ts +633 -0
  117. package/{lib/locales/en-US.js → src/locales/en-US.ts} +21 -37
  118. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +63 -16
  119. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +74 -27
  120. package/src/locales/index.ts +103 -0
  121. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +59 -17
  122. package/src/locales/types.ts +156 -0
  123. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +21 -38
  124. package/src/parser/ConstraintParser.ts +101 -0
  125. package/src/parser/DslParser.ts +470 -0
  126. package/src/parser/SchemaCompiler.ts +66 -0
  127. package/src/parser/TypeRegistry.ts +250 -0
  128. package/src/parser/index.ts +6 -0
  129. package/src/plugins/custom-format.ts +126 -0
  130. package/src/plugins/custom-type-example.ts +108 -0
  131. package/src/plugins/custom-validator.ts +140 -0
  132. package/src/types/conditional.ts +28 -0
  133. package/src/types/config.ts +59 -0
  134. package/src/types/dsl.ts +131 -0
  135. package/src/types/error.ts +60 -0
  136. package/src/types/index.ts +17 -0
  137. package/src/types/infer.ts +128 -0
  138. package/src/types/plugin.ts +58 -0
  139. package/src/types/safe-regex.d.ts +9 -0
  140. package/src/types/schema.ts +66 -0
  141. package/src/types/validate.ts +71 -0
  142. package/src/utils/SchemaHelper.ts +196 -0
  143. package/src/utils/SchemaUtils.ts +346 -0
  144. package/src/utils/TypeConverter.ts +215 -0
  145. package/src/utils/index.ts +10 -0
  146. package/src/validators/CustomKeywords.ts +477 -0
  147. package/.eslintignore +0 -11
  148. package/.eslintrc.json +0 -27
  149. package/CONTRIBUTING.md +0 -368
  150. package/STATUS.md +0 -491
  151. package/changelogs/v1.0.0.md +0 -328
  152. package/changelogs/v1.0.9.md +0 -367
  153. package/changelogs/v1.1.0.md +0 -389
  154. package/changelogs/v1.1.1.md +0 -308
  155. package/changelogs/v1.1.2.md +0 -183
  156. package/changelogs/v1.1.3.md +0 -161
  157. package/changelogs/v1.1.4.md +0 -432
  158. package/changelogs/v1.1.5.md +0 -493
  159. package/changelogs/v1.1.6.md +0 -211
  160. package/changelogs/v1.1.8.md +0 -376
  161. package/changelogs/v1.2.3.md +0 -124
  162. package/docs/INDEX.md +0 -252
  163. package/docs/issues-resolved-summary.md +0 -196
  164. package/docs/performance-benchmark-report.md +0 -179
  165. package/docs/performance-quick-reference.md +0 -123
  166. package/docs/user-questions-answered.md +0 -353
  167. package/docs/validation-rules-v1.0.2.md +0 -1608
  168. package/examples/README.md +0 -81
  169. package/examples/array-dsl-example.js +0 -227
  170. package/examples/conditional-example.js +0 -288
  171. package/examples/conditional-non-object.js +0 -129
  172. package/examples/conditional-validate-example.js +0 -321
  173. package/examples/custom-extension.js +0 -85
  174. package/examples/dsl-match-example.js +0 -74
  175. package/examples/dsl-style.js +0 -118
  176. package/examples/dynamic-locale-configuration.js +0 -348
  177. package/examples/dynamic-locale-example.js +0 -287
  178. package/examples/enum.examples.js +0 -324
  179. package/examples/export-demo.js +0 -130
  180. package/examples/express-integration.js +0 -376
  181. package/examples/i18n-error-handling-complete.js +0 -381
  182. package/examples/i18n-error-handling-quickstart.md +0 -0
  183. package/examples/i18n-error.examples.js +0 -181
  184. package/examples/i18n-full-demo.js +0 -301
  185. package/examples/i18n-memory-safety.examples.js +0 -268
  186. package/examples/markdown-export.js +0 -71
  187. package/examples/middleware-usage.js +0 -93
  188. package/examples/new-features-comparison.js +0 -315
  189. package/examples/password-reset/README.md +0 -153
  190. package/examples/password-reset/schema.js +0 -26
  191. package/examples/password-reset/test.js +0 -101
  192. package/examples/plugin-system.examples.js +0 -205
  193. package/examples/schema-utils-chaining.examples.js +0 -250
  194. package/examples/simple-example.js +0 -122
  195. package/examples/slug.examples.js +0 -179
  196. package/examples/string-extensions.js +0 -297
  197. package/examples/union-type-example.js +0 -127
  198. package/examples/union-types-example.js +0 -77
  199. package/examples/user-registration/README.md +0 -156
  200. package/examples/user-registration/routes.js +0 -92
  201. package/examples/user-registration/schema.js +0 -150
  202. package/examples/user-registration/server.js +0 -74
  203. package/index.d.ts +0 -3540
  204. package/index.js +0 -457
  205. package/index.mjs +0 -60
  206. package/lib/adapters/DslAdapter.js +0 -871
  207. package/lib/adapters/index.js +0 -20
  208. package/lib/config/constants.js +0 -286
  209. package/lib/config/patterns/common.js +0 -47
  210. package/lib/config/patterns/creditCard.js +0 -9
  211. package/lib/config/patterns/idCard.js +0 -9
  212. package/lib/config/patterns/index.js +0 -9
  213. package/lib/config/patterns/licensePlate.js +0 -4
  214. package/lib/config/patterns/passport.js +0 -4
  215. package/lib/config/patterns/phone.js +0 -9
  216. package/lib/config/patterns/postalCode.js +0 -5
  217. package/lib/core/CacheManager.js +0 -376
  218. package/lib/core/ConditionalBuilder.js +0 -503
  219. package/lib/core/DslBuilder.js +0 -1400
  220. package/lib/core/ErrorCodes.js +0 -233
  221. package/lib/core/ErrorFormatter.js +0 -445
  222. package/lib/core/JSONSchemaCore.js +0 -347
  223. package/lib/core/Locale.js +0 -130
  224. package/lib/core/MessageTemplate.js +0 -98
  225. package/lib/core/PluginManager.js +0 -448
  226. package/lib/core/StringExtensions.js +0 -240
  227. package/lib/core/Validator.js +0 -654
  228. package/lib/errors/I18nError.js +0 -328
  229. package/lib/errors/ValidationError.js +0 -191
  230. package/lib/exporters/MarkdownExporter.js +0 -420
  231. package/lib/exporters/MongoDBExporter.js +0 -162
  232. package/lib/exporters/MySQLExporter.js +0 -212
  233. package/lib/exporters/PostgreSQLExporter.js +0 -289
  234. package/lib/exporters/index.js +0 -24
  235. package/lib/locales/index.js +0 -8
  236. package/lib/utils/LRUCache.js +0 -174
  237. package/lib/utils/SchemaHelper.js +0 -240
  238. package/lib/utils/SchemaUtils.js +0 -445
  239. package/lib/utils/TypeConverter.js +0 -245
  240. package/lib/utils/index.js +0 -13
  241. package/lib/validators/CustomKeywords.js +0 -616
  242. package/lib/validators/index.js +0 -11
@@ -0,0 +1,271 @@
1
+ import type { ValidationErrorItem } from '../types/validate.js'
2
+ import type { ErrorMessages } from '../types/error.js'
3
+ import { renderTemplate } from './TemplateEngine.js'
4
+ import { KEYWORD_MAP } from './ErrorCodes.js'
5
+ import { getMessages } from '../locales/index.js'
6
+ import type { LocaleMessage } from '../locales/types.js'
7
+ import { DEFAULT_LOCALE } from './Locale.js'
8
+
9
+ type AjvRawError = {
10
+ keyword: string
11
+ instancePath: string
12
+ schemaPath?: string
13
+ params: Record<string, unknown>
14
+ message?: string
15
+ data?: unknown
16
+ parentSchema?: Record<string, unknown>
17
+ schema?: unknown
18
+ }
19
+
20
+ /**
21
+ * Error formatter.
22
+ * Delegates template interpolation to TemplateEngine.renderTemplate() (fix CORE-03).
23
+ * Maintains full v1 API compatibility.
24
+ */
25
+ export class ErrorFormatter {
26
+ private messages: ErrorMessages
27
+ private _locale: string
28
+ private readonly _constructorCustomMessages: ErrorMessages
29
+
30
+ constructor(locale = DEFAULT_LOCALE, messages: ErrorMessages | Record<string, LocaleMessage | string | undefined> = {}) {
31
+ this._locale = locale
32
+ // Load locale messages as defaults; constructor-level custom messages override them
33
+ const rawLocaleMessages = getMessages(locale)
34
+ const localeMessages: ErrorMessages = Object.fromEntries(
35
+ Object.entries(rawLocaleMessages).map(([k, v]: [string, LocaleMessage]) => [
36
+ k,
37
+ typeof v === 'string' ? v : (v as { message: string }).message,
38
+ ])
39
+ )
40
+ // Normalise caller-supplied messages: LocaleMessage objects → plain string
41
+ const normMessages: ErrorMessages = Object.fromEntries(
42
+ Object.entries(messages).map(([k, v]) => [
43
+ k,
44
+ v == null ? undefined : typeof v === 'string' ? v : (v as { message: string }).message,
45
+ ])
46
+ )
47
+ this._constructorCustomMessages = normMessages
48
+ this.messages = { ...localeMessages, ...normMessages }
49
+ }
50
+
51
+ get locale(): string {
52
+ return this._locale
53
+ }
54
+
55
+ /**
56
+ * Format a single error object → message string (v1 API).
57
+ */
58
+ format(error: AjvRawError | Record<string, unknown>, locale?: string): string {
59
+ // If locale differs, reload messages for that locale
60
+ let msgs = this.messages
61
+ if (locale && locale !== this._locale) {
62
+ const rawLocaleMessages = getMessages(locale)
63
+ const localeMessages: ErrorMessages = Object.fromEntries(
64
+ Object.entries(rawLocaleMessages).map(([k, v]: [string, LocaleMessage]) => [
65
+ k,
66
+ typeof v === 'string' ? v : (v as { message: string }).message,
67
+ ])
68
+ )
69
+ // Preserve constructor-level custom messages across locale switches (R-01 fix)
70
+ msgs = { ...localeMessages, ...this._constructorCustomMessages }
71
+ }
72
+
73
+ // Convert simple { type, path } format to AJV-like error
74
+ const raw = error as Record<string, unknown>
75
+ const ajvError = {
76
+ keyword: (raw['keyword'] as string) ?? (raw['type'] as string) ?? 'validation',
77
+ instancePath: (raw['instancePath'] as string) ?? ('/' + (raw['path'] ?? '')),
78
+ params: (raw['params'] as Record<string, unknown>) ?? {},
79
+ parentSchema: raw['parentSchema'] as Record<string, unknown> | undefined,
80
+ } as AjvRawError
81
+ const item = this._formatOne(ajvError, msgs, locale)
82
+ return item.message
83
+ }
84
+
85
+ /**
86
+ * Format an AJV raw error array → ValidationErrorItem[].
87
+ *
88
+ * @param alreadyMerged - when true, customMessages is already a fully merged locale+custom result;
89
+ * skip `{ ...this.messages, ...customMessages }` spread (avoids 100+ key cold-spread overhead).
90
+ */
91
+ formatDetailed(
92
+ errors: AjvRawError[],
93
+ locale?: string,
94
+ customMessages?: ErrorMessages,
95
+ alreadyMerged = false
96
+ ): ValidationErrorItem[] {
97
+ const msgs = customMessages
98
+ ? (alreadyMerged ? customMessages : { ...this.messages, ...customMessages })
99
+ : this.messages
100
+
101
+ // Filter wrapper errors (if/anyOf/oneOf) when concrete field errors are present
102
+ const hasConcreteErrors = errors.some(
103
+ e => e.keyword !== 'if' && e.keyword !== 'anyOf' && e.keyword !== 'oneOf' && e.keyword !== 'error'
104
+ )
105
+ const filtered = hasConcreteErrors
106
+ ? errors.filter(e => e.keyword !== 'if' && e.keyword !== 'anyOf' && e.keyword !== 'oneOf')
107
+ : errors
108
+
109
+ return filtered.map(err => this._formatOne(err, msgs, locale))
110
+ }
111
+
112
+ /**
113
+ * Format a single error entry into a ValidationErrorItem.
114
+ */
115
+ private _formatOne(
116
+ err: AjvRawError,
117
+ messages: ErrorMessages,
118
+ _locale?: string
119
+ ): ValidationErrorItem {
120
+ const keyword = err.keyword ?? 'validation'
121
+ const instancePath = err.instancePath ?? ''
122
+ const params = err.params ?? {} as Record<string, unknown>
123
+
124
+ // Field path calculation (required errors get special handling)
125
+ let fieldName: string
126
+ if (keyword === 'required' && params['missingProperty']) {
127
+ const parentPath = instancePath.replace(/^\//, '')
128
+ const missing = String(params['missingProperty'])
129
+ fieldName = parentPath ? `${parentPath}/${missing}` : missing
130
+ } else {
131
+ fieldName = instancePath.replace(/^\//, '') || 'value'
132
+ }
133
+
134
+ // Label resolution
135
+ const schema = (err.parentSchema ?? {}) as Record<string, unknown>
136
+ let label: string | undefined
137
+
138
+ // For required errors, get label from the specific property schema
139
+ if (keyword === 'required' && params['missingProperty']) {
140
+ const missingProp = String(params['missingProperty'])
141
+ const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined
142
+ if (properties && properties[missingProp]) {
143
+ label = properties[missingProp]['_label'] as string | undefined
144
+ }
145
+ }
146
+
147
+ // Fallback to parent schema label
148
+ if (!label) {
149
+ label = schema['_label'] as string | undefined
150
+ }
151
+
152
+ // If _label is set, try to translate it as a locale key reference
153
+ if (label) {
154
+ label = (messages[label] != null ? String(messages[label]) : undefined) ?? label
155
+ }
156
+
157
+ if (!label) {
158
+ let labelKey: string
159
+ if (keyword === 'required' && params['missingProperty']) {
160
+ labelKey = String(params['missingProperty'])
161
+ } else {
162
+ const parts = fieldName.split('/')
163
+ labelKey = parts[parts.length - 1] ?? fieldName
164
+ }
165
+ const autoKey = `label.${labelKey.replace(/\//g, '.')}`
166
+ label = messages[autoKey] ?? labelKey
167
+ }
168
+
169
+ // Schema-level custom messages
170
+ let schemaCustomMessages = (schema['_customMessages'] ?? {}) as ErrorMessages
171
+
172
+ // For required errors, also check field-level custom messages
173
+ if (keyword === 'required' && params['missingProperty']) {
174
+ const missingProp = String(params['missingProperty'])
175
+ const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined
176
+ if (properties && properties[missingProp] && properties[missingProp]['_customMessages']) {
177
+ schemaCustomMessages = { ...schemaCustomMessages, ...(properties[missingProp]['_customMessages'] as ErrorMessages) }
178
+ }
179
+ }
180
+
181
+ // Performance: reuse `messages` directly when schemaCustomMessages is empty (99 % of calls)
182
+ const hasCustomMessages = Object.keys(schemaCustomMessages).length > 0
183
+ const mergedMessages = hasCustomMessages ? { ...messages, ...schemaCustomMessages } : messages
184
+ const mappedKeyword = KEYWORD_MAP[keyword] ?? keyword
185
+ const schemaType = typeof schema['type'] === 'string' ? schema['type'] : 'string'
186
+
187
+ // Message lookup order: schema custom > type+keyword > keyword > fallback
188
+ let message: string | undefined = hasCustomMessages
189
+ ? (schemaCustomMessages[keyword] ?? schemaCustomMessages[mappedKeyword])
190
+ : undefined
191
+
192
+ if (message) {
193
+ // May be a key reference — try to resolve from mergedMessages
194
+ message = mergedMessages[message] ?? message
195
+ } else {
196
+ // Special handling for format.email etc.
197
+ if (mappedKeyword === 'format' && params['format']) {
198
+ let fmt = String(params['format'])
199
+ if (fmt === 'uri') fmt = 'url'
200
+ message = mergedMessages[`format.${fmt}`]
201
+ }
202
+ message ??=
203
+ mergedMessages[`${schemaType}.${keyword}`] ??
204
+ mergedMessages[`${schemaType}.${mappedKeyword}`] ??
205
+ mergedMessages[mappedKeyword] ??
206
+ mergedMessages[keyword] ??
207
+ mergedMessages['default'] ??
208
+ err.message ??
209
+ 'Validation error'
210
+ }
211
+
212
+ // Interpolation params: spread AJV params first, then override fixed keys
213
+ const limit = params['limit'] ?? params['limitLength'] ?? params['comparison'] ?? ''
214
+ const allowedVals = Array.isArray(params['allowedValues'])
215
+ ? (params['allowedValues'] as unknown[]).join(', ')
216
+ : undefined
217
+ const interpolateData: Record<string, unknown> = {
218
+ ...params,
219
+ path: label,
220
+ label,
221
+ value: err.data !== undefined ? err.data : '',
222
+ limit,
223
+ min: limit,
224
+ max: limit,
225
+ expected: params['type'],
226
+ actual:
227
+ err.data === null
228
+ ? 'null'
229
+ : err.data === undefined
230
+ ? 'undefined'
231
+ : Array.isArray(err.data)
232
+ ? 'array'
233
+ : typeof err.data,
234
+ valids: allowedVals,
235
+ allowed: allowedVals,
236
+ key: params['additionalProperty'],
237
+ }
238
+
239
+ const rendered = renderTemplate(message, interpolateData)
240
+
241
+ return {
242
+ path: fieldName,
243
+ message: rendered,
244
+ keyword,
245
+ params,
246
+ field: fieldName,
247
+ type: keyword,
248
+ expected: params['type'] !== undefined ? String(params['type']) : undefined,
249
+ }
250
+ }
251
+
252
+ setLocale(locale: string): void {
253
+ this._locale = locale
254
+ const rawLocaleMessages = getMessages(locale)
255
+ const localeMessages: ErrorMessages = Object.fromEntries(
256
+ Object.entries(rawLocaleMessages).map(([k, v]: [string, LocaleMessage]) => [
257
+ k,
258
+ typeof v === 'string' ? v : (v as { message: string }).message,
259
+ ])
260
+ )
261
+ this.messages = { ...localeMessages, ...this._constructorCustomMessages }
262
+ }
263
+
264
+ addMessage(type: string, template: string): void {
265
+ this.messages[type] = template
266
+ }
267
+
268
+ addMessages(messages: ErrorMessages): void {
269
+ Object.assign(this.messages, messages)
270
+ }
271
+ }
@@ -0,0 +1,65 @@
1
+ import type { JSONSchema } from '../types/schema.js'
2
+ import { Validator } from './Validator.js'
3
+
4
+ /**
5
+ * JSONSchemaCore — v1 compatibility facade.
6
+ *
7
+ * The v2 internals have been split into DslParser / SchemaCompiler / Validator; this class
8
+ * restores the commonly-used chainable entry points from the v1 public API so that users
9
+ * who import from the main entry point do not encounter errors.
10
+ */
11
+ export class JSONSchemaCore {
12
+ schema: JSONSchema
13
+
14
+ constructor(schema: JSONSchema = {}) {
15
+ this.schema = { ...schema }
16
+ }
17
+
18
+ type(typeName: string): this {
19
+ this.schema.type = typeName
20
+ return this
21
+ }
22
+
23
+ property(name: string, schema: JSONSchema): this {
24
+ if (!this.schema.properties) this.schema.properties = {}
25
+ this.schema.properties[name] = schema
26
+ return this
27
+ }
28
+
29
+ properties(properties: Record<string, JSONSchema>): this {
30
+ this.schema.properties = { ...(this.schema.properties ?? {}), ...properties }
31
+ return this
32
+ }
33
+
34
+ required(fields: string[] | string): this {
35
+ this.schema.required = Array.isArray(fields) ? fields : [fields]
36
+ return this
37
+ }
38
+
39
+ format(formatName: string): this {
40
+ this.schema.format = formatName
41
+ return this
42
+ }
43
+
44
+ pattern(pattern: RegExp | string): this {
45
+ this.schema.pattern = pattern instanceof RegExp ? pattern.source : pattern
46
+ return this
47
+ }
48
+
49
+ items(schema: JSONSchema): this {
50
+ this.schema.items = schema
51
+ return this
52
+ }
53
+
54
+ toSchema(): JSONSchema {
55
+ return this.schema
56
+ }
57
+
58
+ getSchema(): JSONSchema {
59
+ return this.toSchema()
60
+ }
61
+
62
+ validate(data: unknown): ReturnType<Validator['validate']> {
63
+ return new Validator().validate(this.schema, data)
64
+ }
65
+ }
@@ -0,0 +1,187 @@
1
+ import type { LocaleKey, LocaleMessage } from '../locales/types.js'
2
+ import { getMessage, getMessages, isSupportedLocale, getSupportedLocales } from '../locales/index.js'
3
+
4
+ export interface LocaleResolvedMessage {
5
+ code: string | number
6
+ message: string
7
+ }
8
+
9
+ /**
10
+ * Locale — global locale manager (static class).
11
+ *
12
+ * v1 compatibility semantics:
13
+ * - getMessage() → returns { code, message } for every resolved message
14
+ * - getMessageText() → always returns final message text (used internally by v2)
15
+ * - getMessageConfig() → returns raw LocaleMessage (may contain code object; used by I18nError)
16
+ */
17
+ export const DEFAULT_LOCALE = 'en-US'
18
+
19
+ export class Locale {
20
+ private static _currentLocale: string = DEFAULT_LOCALE
21
+ private static _customMessages: Record<string, LocaleMessage> = {}
22
+
23
+ /** v1 compat: expose custom messages */
24
+ static get customMessages(): Record<string, LocaleMessage> {
25
+ return this._customMessages
26
+ }
27
+
28
+ /** v1 compat: expose all locales as { locale: messages } map */
29
+ static get locales(): Record<string, Record<string, LocaleMessage>> {
30
+ const result: Record<string, Record<string, LocaleMessage>> = {}
31
+ // Built-in locales
32
+ for (const locale of getSupportedLocales()) {
33
+ result[locale] = getMessages(locale) as Record<string, LocaleMessage>
34
+ }
35
+ // Custom locales added via addLocale
36
+ for (const key of Object.keys(this._customMessages)) {
37
+ if (key.includes(':')) {
38
+ const colonIdx = key.indexOf(':')
39
+ const locale = key.substring(0, colonIdx)
40
+ const msgKey = key.substring(colonIdx + 1)
41
+ if (!result[locale]) result[locale] = {}
42
+ result[locale][msgKey] = this._customMessages[key]
43
+ }
44
+ }
45
+ return result
46
+ }
47
+
48
+ // ─── Locale Switching ─────────────────────────────────────────────────────
49
+
50
+ static setLocale(locale: string): void {
51
+ this._currentLocale = locale
52
+ }
53
+
54
+ static getLocale(): string {
55
+ return this._currentLocale
56
+ }
57
+
58
+ // ─── Custom Messages (global override) ───────────────────────────────────
59
+
60
+ static setMessages(messages: Record<string, LocaleMessage>): void {
61
+ this._customMessages = { ...this._customMessages, ...messages }
62
+ }
63
+
64
+ static addLocale(locale: string, messages: Record<string, LocaleMessage>): void {
65
+ // Dynamically add a locale pack at runtime (merged into existing entries).
66
+ // Records into customMessages and takes priority during lookup.
67
+ for (const [k, v] of Object.entries(messages)) {
68
+ this._customMessages[`${locale}:${k}`] = v
69
+ }
70
+ }
71
+
72
+ static getAvailableLocales(): string[] {
73
+ return getSupportedLocales()
74
+ }
75
+
76
+ static isSupportedLocale(locale: string): boolean {
77
+ return isSupportedLocale(locale)
78
+ }
79
+
80
+ // ─── Core Query Methods ───────────────────────────────────────────────────
81
+
82
+ /**
83
+ * Get a resolved message (v1 compat: returns { code, message } on hit).
84
+ *
85
+ * Priority: custom messages > locale pack > key itself.
86
+ */
87
+ static getMessage(
88
+ type: string,
89
+ customMessages: Record<string, LocaleMessage> = {},
90
+ locale: string | null = null
91
+ ): LocaleResolvedMessage | string {
92
+ const resolved = this._resolveMessage(type, customMessages, locale)
93
+ if (!resolved) return type
94
+ return this._normalizeResolvedMessage(type, resolved)
95
+ }
96
+
97
+ /**
98
+ * Get the final message text (used internally by v2 to avoid "[object Object]" in message field).
99
+ */
100
+ static getMessageText(
101
+ type: string,
102
+ customMessages: Record<string, LocaleMessage> = {},
103
+ locale: string | null = null
104
+ ): string {
105
+ const resolved = this.getMessage(type, customMessages, locale)
106
+ return typeof resolved === 'string' ? resolved : resolved.message
107
+ }
108
+
109
+ /**
110
+ * Get raw message config (used by I18nError; may include a numeric code).
111
+ */
112
+ static getMessageConfig(
113
+ type: string,
114
+ customMessages: Record<string, LocaleMessage> = {},
115
+ locale: string | null = null
116
+ ): LocaleMessage {
117
+ return this._resolveMessage(type, customMessages, locale) ?? { code: type, message: type }
118
+ }
119
+
120
+ /**
121
+ * Get the full message table for the given locale (built-in + custom).
122
+ */
123
+ static getMessages(locale?: string): Record<string, LocaleMessage> {
124
+ const targetLocale = locale ?? this._currentLocale
125
+ const builtinMessages = getMessages(targetLocale) as Record<string, LocaleMessage>
126
+ // Merge custom messages added via addLocale/setMessages for this locale
127
+ const customForLocale: Record<string, LocaleMessage> = {}
128
+ for (const [k, v] of Object.entries(this._customMessages)) {
129
+ if (k.startsWith(`${targetLocale}:`)) {
130
+ customForLocale[k.slice(targetLocale.length + 1)] = v
131
+ } else if (!k.includes(':')) {
132
+ // Global custom messages (no locale prefix)
133
+ customForLocale[k] = v
134
+ }
135
+ }
136
+ return { ...builtinMessages, ...customForLocale }
137
+ }
138
+
139
+ /**
140
+ * Reset to defaults (for testing).
141
+ */
142
+ static reset(): void {
143
+ this._currentLocale = DEFAULT_LOCALE
144
+ this._customMessages = {}
145
+ }
146
+
147
+ // ─── Private Helpers ──────────────────────────────────────────────────────
148
+
149
+ private static _normalizeResolvedMessage(type: string, msg: LocaleMessage): LocaleResolvedMessage {
150
+ if (typeof msg === 'string') {
151
+ return { code: type, message: msg }
152
+ }
153
+ return {
154
+ code: msg.code ?? type,
155
+ message: msg.message,
156
+ }
157
+ }
158
+
159
+ private static _resolveMessage(
160
+ type: string,
161
+ customMessages: Record<string, LocaleMessage>,
162
+ locale: string | null
163
+ ): LocaleMessage | null {
164
+ const targetLocale = locale ?? this._currentLocale
165
+
166
+ const callerMsg = customMessages[type]
167
+ if (callerMsg !== undefined) return callerMsg
168
+
169
+ const globalMsg = this._customMessages[type]
170
+ if (globalMsg !== undefined) return globalMsg
171
+
172
+ const globalLocaleMsg = this._customMessages[`${targetLocale}:${type}`]
173
+ if (globalLocaleMsg !== undefined) return globalLocaleMsg
174
+
175
+ if (this._isLocaleKey(type)) {
176
+ return getMessage(type as LocaleKey, targetLocale)
177
+ }
178
+
179
+ return null
180
+ }
181
+
182
+ private static _isLocaleKey(key: string): boolean {
183
+ // All predefined locale keys are defined in language pack files
184
+ const msgs = getMessages()
185
+ return key in msgs
186
+ }
187
+ }
@@ -0,0 +1,42 @@
1
+ import { renderTemplate } from './TemplateEngine.js'
2
+
3
+ /**
4
+ * MessageTemplate — wraps a template string for rendering.
5
+ * Delegates to TemplateEngine.renderTemplate() (fix CORE-03).
6
+ * Maintains full v1 API compatibility (constructor + render + static render + static renderBatch).
7
+ */
8
+ export class MessageTemplate {
9
+ private readonly template: string
10
+
11
+ constructor(template: string) {
12
+ this.template = template
13
+ }
14
+
15
+ /**
16
+ * Render the template with the given context.
17
+ */
18
+ render(context: Record<string, unknown> = {}): string {
19
+ return renderTemplate(this.template, context)
20
+ }
21
+
22
+ /**
23
+ * Statically render a template string with the given context.
24
+ */
25
+ static render(template: string, context: Record<string, unknown> = {}): string {
26
+ return renderTemplate(template, context)
27
+ }
28
+
29
+ /**
30
+ * Statically render multiple templates in batch.
31
+ */
32
+ static renderBatch(
33
+ templates: Record<string, string>,
34
+ context: Record<string, unknown> = {}
35
+ ): Record<string, string> {
36
+ const result: Record<string, string> = {}
37
+ for (const [key, tmpl] of Object.entries(templates)) {
38
+ result[key] = renderTemplate(tmpl, context)
39
+ }
40
+ return result
41
+ }
42
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ObjectDslBuilder — v1 compat: dsl(object) returns a chainable builder.
3
+ *
4
+ * BC-2 fix: in v1, the schema returned by dsl({...}) / parseObject() supported .strict() /
5
+ * .requireAll() and other chain decorators, and implemented the toSchema() duck-type interface
6
+ * (Validator passes through internal schema). After v2 refactor, parseObject() returned plain
7
+ * JSONSchema, losing all chain API. This class wraps the DslParser.parseObject() result and
8
+ * exposes v1-equivalent chain methods:
9
+ * - toSchema() — return internal JSONSchema (Validator duck-type entry point)
10
+ * - toJsonSchema() — return clean JSON Schema (internal schema-dsl keywords stripped)
11
+ * - strict() — disallow extra properties
12
+ * - requireAll() — require all defined properties to be present
13
+ * - toString() — serialize to JSON string
14
+ */
15
+
16
+ import type { JSONSchema } from '../types/schema.js'
17
+ import { TypeRegistry } from '../parser/TypeRegistry.js'
18
+
19
+ export class ObjectDslBuilder {
20
+ readonly _isDslBuilder = true as const
21
+ readonly _isObjectDsl = true as const
22
+
23
+ private _schema: JSONSchema
24
+
25
+ constructor(schema: JSONSchema) {
26
+ this._schema = schema
27
+ }
28
+
29
+ // ==================== Output Methods ====================
30
+
31
+ /** Return internal JSONSchema (Validator.validate() duck-type entry point). */
32
+ toSchema(): JSONSchema {
33
+ return this._schema
34
+ }
35
+
36
+ /** Return clean JSON Schema (internal schema-dsl keywords stripped; safe for serialization or external tools). */
37
+ toJsonSchema(): JSONSchema {
38
+ return TypeRegistry.toJsonSchema(this._schema)
39
+ }
40
+
41
+ toString(): string {
42
+ return JSON.stringify(this.toJsonSchema())
43
+ }
44
+
45
+ // ==================== Chain Decorator Methods ====================
46
+
47
+ /**
48
+ * strict() — disallow extra properties (v1 compat).
49
+ * Equivalent to setting strictSchema: true on the compiled schema.
50
+ */
51
+ strict(): this {
52
+ ; (this._schema as Record<string, unknown>)['strictSchema'] = true
53
+ return this
54
+ }
55
+
56
+ /**
57
+ * requireAll() — require all defined properties to be present (v1 compat).
58
+ * Equivalent to setting requiredAll: true on the compiled schema.
59
+ */
60
+ requireAll(): this {
61
+ ; (this._schema as Record<string, unknown>)['requiredAll'] = true
62
+ return this
63
+ }
64
+ }