schema-dsl 1.2.5 → 2.0.1

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 (243) hide show
  1. package/CHANGELOG.md +130 -238
  2. package/LICENSE +21 -21
  3. package/README.md +628 -2486
  4. package/dist/DslBuilder-BIgQOAXp.d.ts +343 -0
  5. package/dist/DslBuilder-CjHTucNQ.d.cts +343 -0
  6. package/dist/Validator-CllRdrY0.d.ts +192 -0
  7. package/dist/Validator-D6okG9tr.d.cts +192 -0
  8. package/dist/index.cjs +6640 -0
  9. package/dist/index.d.cts +1151 -0
  10. package/dist/index.d.ts +1151 -0
  11. package/dist/index.js +6574 -0
  12. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  13. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  14. package/dist/plugins/custom-format.cjs +3818 -0
  15. package/dist/plugins/custom-format.d.cts +12 -0
  16. package/dist/plugins/custom-format.d.ts +12 -0
  17. package/dist/plugins/custom-format.js +3788 -0
  18. package/dist/plugins/custom-type-example.cjs +3811 -0
  19. package/dist/plugins/custom-type-example.d.cts +8 -0
  20. package/dist/plugins/custom-type-example.d.ts +8 -0
  21. package/dist/plugins/custom-type-example.js +3781 -0
  22. package/dist/plugins/custom-validator.cjs +144 -0
  23. package/dist/plugins/custom-validator.d.cts +10 -0
  24. package/dist/plugins/custom-validator.d.ts +10 -0
  25. package/dist/plugins/custom-validator.js +119 -0
  26. package/docs/FEATURE-INDEX.md +553 -519
  27. package/docs/add-custom-locale.md +496 -483
  28. package/docs/add-keyword.md +24 -0
  29. package/docs/api-reference.md +1047 -805
  30. package/docs/api.md +13 -0
  31. package/docs/best-practices-project-structure.md +417 -408
  32. package/docs/best-practices.md +712 -672
  33. package/docs/cache-manager.md +344 -336
  34. package/docs/compile.md +45 -0
  35. package/docs/conditional-api.md +1307 -1278
  36. package/docs/custom-extensions-guide.md +339 -411
  37. package/docs/design-philosophy.md +606 -601
  38. package/docs/doc-index.md +324 -0
  39. package/docs/dsl-syntax.md +714 -664
  40. package/docs/dynamic-locale.md +608 -598
  41. package/docs/enum.md +482 -475
  42. package/docs/error-handling.md +1975 -1966
  43. package/docs/export-guide.md +501 -462
  44. package/docs/export-limitations.md +567 -551
  45. package/docs/faq.md +596 -577
  46. package/docs/frontend-i18n-guide.md +307 -293
  47. package/docs/i18n-user-guide.md +487 -474
  48. package/docs/i18n.md +476 -457
  49. package/docs/index.md +48 -0
  50. package/docs/json-schema-basics.md +40 -0
  51. package/docs/label-vs-description.md +271 -262
  52. package/docs/markdown-exporter.md +406 -397
  53. package/docs/mongodb-exporter.md +302 -295
  54. package/docs/multi-language.md +26 -0
  55. package/docs/multi-type-support.md +322 -329
  56. package/docs/mysql-exporter.md +280 -273
  57. package/docs/number-operators.md +449 -442
  58. package/docs/optional-marker-guide.md +326 -321
  59. package/docs/performance-guide.md +49 -0
  60. package/docs/plugin-system.md +381 -542
  61. package/docs/plugin-type-registration.md +34 -0
  62. package/docs/postgresql-exporter.md +311 -304
  63. package/docs/public/favicon.svg +5 -0
  64. package/docs/quick-start.md +435 -761
  65. package/docs/runtime-locale-support.md +532 -521
  66. package/docs/schema-helper.md +345 -340
  67. package/docs/schema-utils-advanced-issues.md +23 -0
  68. package/docs/schema-utils-best-practices.md +20 -0
  69. package/docs/schema-utils-chaining.md +150 -143
  70. package/docs/schema-utils.md +524 -490
  71. package/docs/security-checklist.md +20 -0
  72. package/docs/string-extensions.md +488 -480
  73. package/docs/troubleshooting.md +486 -471
  74. package/docs/type-converter.md +310 -319
  75. package/docs/type-reference.md +242 -219
  76. package/docs/typescript-guide.md +584 -573
  77. package/docs/union-type-guide.md +157 -147
  78. package/docs/union-types.md +284 -277
  79. package/docs/validate-async.md +491 -480
  80. package/docs/validate-batch.md +49 -0
  81. package/docs/validate-dsl-object-support.md +578 -573
  82. package/docs/validate.md +506 -486
  83. package/docs/validation-guide.md +502 -484
  84. package/docs/validator.md +39 -0
  85. package/package.json +131 -73
  86. package/plugins/custom-format.cjs +8 -0
  87. package/plugins/custom-type-example.cjs +8 -0
  88. package/plugins/custom-validator.cjs +8 -0
  89. package/src/adapters/DslAdapter.ts +111 -0
  90. package/src/adapters/index.ts +1 -0
  91. package/src/config/constants.ts +83 -0
  92. package/src/config/index.ts +2 -0
  93. package/src/config/patterns.ts +77 -0
  94. package/src/core/CacheManager.ts +169 -0
  95. package/src/core/ConditionalBuilder.ts +382 -0
  96. package/src/core/ConditionalRuntime.ts +28 -0
  97. package/src/core/ConditionalValidator.ts +255 -0
  98. package/src/core/DslBuilder.ts +687 -0
  99. package/src/core/ErrorCodes.ts +38 -0
  100. package/src/core/ErrorFormatter.ts +271 -0
  101. package/src/core/JSONSchemaCore.ts +65 -0
  102. package/src/core/Locale.ts +187 -0
  103. package/src/core/MessageTemplate.ts +42 -0
  104. package/src/core/ObjectDslBuilder.ts +64 -0
  105. package/src/core/PluginManager.ts +326 -0
  106. package/src/core/StringExtensions.ts +140 -0
  107. package/src/core/TemplateEngine.ts +44 -0
  108. package/src/core/Validator.ts +448 -0
  109. package/src/errors/I18nError.ts +159 -0
  110. package/src/errors/ValidationError.ts +105 -0
  111. package/src/exporters/BaseExporter.ts +60 -0
  112. package/src/exporters/MarkdownExporter.ts +305 -0
  113. package/src/exporters/MongoDBExporter.ts +126 -0
  114. package/src/exporters/MySQLExporter.ts +156 -0
  115. package/src/exporters/PostgreSQLExporter.ts +222 -0
  116. package/src/exporters/index.ts +18 -0
  117. package/src/index.ts +651 -0
  118. package/{lib/locales/en-US.js → src/locales/en-US.ts} +160 -176
  119. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +160 -113
  120. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +160 -113
  121. package/src/locales/index.ts +103 -0
  122. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +160 -118
  123. package/src/locales/types.ts +156 -0
  124. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +160 -177
  125. package/src/parser/ConstraintParser.ts +101 -0
  126. package/src/parser/DslParser.ts +470 -0
  127. package/src/parser/SchemaCompiler.ts +66 -0
  128. package/src/parser/TypeRegistry.ts +250 -0
  129. package/src/parser/index.ts +6 -0
  130. package/src/plugins/custom-format.ts +124 -0
  131. package/src/plugins/custom-type-example.ts +106 -0
  132. package/src/plugins/custom-validator.ts +138 -0
  133. package/src/types/conditional.ts +28 -0
  134. package/src/types/config.ts +59 -0
  135. package/src/types/dsl.ts +131 -0
  136. package/src/types/error.ts +60 -0
  137. package/src/types/index.ts +17 -0
  138. package/src/types/infer.ts +128 -0
  139. package/src/types/plugin.ts +58 -0
  140. package/src/types/safe-regex.d.ts +9 -0
  141. package/src/types/schema.ts +66 -0
  142. package/src/types/validate.ts +71 -0
  143. package/src/utils/SchemaHelper.ts +196 -0
  144. package/src/utils/SchemaUtils.ts +365 -0
  145. package/src/utils/TypeConverter.ts +215 -0
  146. package/src/utils/index.ts +10 -0
  147. package/src/validators/CustomKeywords.ts +477 -0
  148. package/.eslintignore +0 -11
  149. package/.eslintrc.json +0 -27
  150. package/CONTRIBUTING.md +0 -368
  151. package/STATUS.md +0 -491
  152. package/changelogs/v1.0.0.md +0 -328
  153. package/changelogs/v1.0.9.md +0 -367
  154. package/changelogs/v1.1.0.md +0 -389
  155. package/changelogs/v1.1.1.md +0 -308
  156. package/changelogs/v1.1.2.md +0 -183
  157. package/changelogs/v1.1.3.md +0 -161
  158. package/changelogs/v1.1.4.md +0 -432
  159. package/changelogs/v1.1.5.md +0 -493
  160. package/changelogs/v1.1.6.md +0 -211
  161. package/changelogs/v1.1.8.md +0 -376
  162. package/changelogs/v1.2.3.md +0 -124
  163. package/docs/INDEX.md +0 -252
  164. package/docs/issues-resolved-summary.md +0 -196
  165. package/docs/performance-benchmark-report.md +0 -179
  166. package/docs/performance-quick-reference.md +0 -123
  167. package/docs/user-questions-answered.md +0 -353
  168. package/docs/validation-rules-v1.0.2.md +0 -1608
  169. package/examples/README.md +0 -81
  170. package/examples/array-dsl-example.js +0 -227
  171. package/examples/conditional-example.js +0 -288
  172. package/examples/conditional-non-object.js +0 -129
  173. package/examples/conditional-validate-example.js +0 -321
  174. package/examples/custom-extension.js +0 -85
  175. package/examples/dsl-match-example.js +0 -74
  176. package/examples/dsl-style.js +0 -118
  177. package/examples/dynamic-locale-configuration.js +0 -348
  178. package/examples/dynamic-locale-example.js +0 -287
  179. package/examples/enum.examples.js +0 -324
  180. package/examples/export-demo.js +0 -130
  181. package/examples/express-integration.js +0 -376
  182. package/examples/i18n-error-handling-complete.js +0 -381
  183. package/examples/i18n-error-handling-quickstart.md +0 -0
  184. package/examples/i18n-error.examples.js +0 -181
  185. package/examples/i18n-full-demo.js +0 -301
  186. package/examples/i18n-memory-safety.examples.js +0 -268
  187. package/examples/markdown-export.js +0 -71
  188. package/examples/middleware-usage.js +0 -93
  189. package/examples/new-features-comparison.js +0 -315
  190. package/examples/password-reset/README.md +0 -153
  191. package/examples/password-reset/schema.js +0 -26
  192. package/examples/password-reset/test.js +0 -101
  193. package/examples/plugin-system.examples.js +0 -205
  194. package/examples/schema-utils-chaining.examples.js +0 -250
  195. package/examples/simple-example.js +0 -122
  196. package/examples/slug.examples.js +0 -179
  197. package/examples/string-extensions.js +0 -297
  198. package/examples/union-type-example.js +0 -127
  199. package/examples/union-types-example.js +0 -77
  200. package/examples/user-registration/README.md +0 -156
  201. package/examples/user-registration/routes.js +0 -92
  202. package/examples/user-registration/schema.js +0 -150
  203. package/examples/user-registration/server.js +0 -74
  204. package/index.d.ts +0 -3658
  205. package/index.js +0 -475
  206. package/index.mjs +0 -60
  207. package/lib/adapters/DslAdapter.js +0 -995
  208. package/lib/adapters/index.js +0 -20
  209. package/lib/config/constants.js +0 -286
  210. package/lib/config/patterns/common.js +0 -47
  211. package/lib/config/patterns/creditCard.js +0 -9
  212. package/lib/config/patterns/idCard.js +0 -9
  213. package/lib/config/patterns/index.js +0 -9
  214. package/lib/config/patterns/licensePlate.js +0 -4
  215. package/lib/config/patterns/passport.js +0 -4
  216. package/lib/config/patterns/phone.js +0 -9
  217. package/lib/config/patterns/postalCode.js +0 -5
  218. package/lib/core/CacheManager.js +0 -376
  219. package/lib/core/ConditionalBuilder.js +0 -503
  220. package/lib/core/DslBuilder.js +0 -1589
  221. package/lib/core/ErrorCodes.js +0 -233
  222. package/lib/core/ErrorFormatter.js +0 -445
  223. package/lib/core/JSONSchemaCore.js +0 -347
  224. package/lib/core/Locale.js +0 -130
  225. package/lib/core/MessageTemplate.js +0 -98
  226. package/lib/core/PluginManager.js +0 -448
  227. package/lib/core/StringExtensions.js +0 -240
  228. package/lib/core/Validator.js +0 -654
  229. package/lib/errors/I18nError.js +0 -328
  230. package/lib/errors/ValidationError.js +0 -191
  231. package/lib/exporters/MarkdownExporter.js +0 -420
  232. package/lib/exporters/MongoDBExporter.js +0 -162
  233. package/lib/exporters/MySQLExporter.js +0 -212
  234. package/lib/exporters/PostgreSQLExporter.js +0 -289
  235. package/lib/exporters/index.js +0 -24
  236. package/lib/locales/index.js +0 -8
  237. package/lib/utils/LRUCache.js +0 -174
  238. package/lib/utils/SchemaHelper.js +0 -240
  239. package/lib/utils/SchemaUtils.js +0 -445
  240. package/lib/utils/TypeConverter.js +0 -245
  241. package/lib/utils/index.js +0 -13
  242. package/lib/validators/CustomKeywords.js +0 -616
  243. package/lib/validators/index.js +0 -11
@@ -0,0 +1,38 @@
1
+ import type { ErrorCodeMap } from '../types/error.js'
2
+
3
+ /**
4
+ * Error code constants.
5
+ * Defines all built-in error codes used by ErrorFormatter and Locale.
6
+ */
7
+ export const ErrorCodes: ErrorCodeMap = {
8
+ // Validation errors
9
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
10
+ INVALID_SCHEMA: 'INVALID_SCHEMA',
11
+ // Configuration errors
12
+ INVALID_CONFIG: 'INVALID_CONFIG',
13
+ INVALID_LOCALE: 'INVALID_LOCALE',
14
+ // Plugin errors
15
+ PLUGIN_INSTALL_ERROR: 'PLUGIN_INSTALL_ERROR',
16
+ PLUGIN_NOT_FOUND: 'PLUGIN_NOT_FOUND',
17
+ }
18
+
19
+ /**
20
+ * Error type → short code mapping (maps AJV keywords to schema-dsl shorthand).
21
+ */
22
+ export const KEYWORD_MAP: Record<string, string> = {
23
+ minLength: 'min',
24
+ maxLength: 'max',
25
+ minimum: 'min',
26
+ maximum: 'max',
27
+ minItems: 'min',
28
+ maxItems: 'max',
29
+ exclusiveMinimum: 'min',
30
+ exclusiveMaximum: 'max',
31
+ pattern: 'pattern',
32
+ format: 'format',
33
+ required: 'required',
34
+ enum: 'enum',
35
+ type: 'type',
36
+ uniqueItems: 'uniqueItems',
37
+ additionalProperties: 'additionalProperties',
38
+ }
@@ -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
+ }