schema-dsl 1.2.5 → 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 -212
  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 -3658
  204. package/index.js +0 -475
  205. package/index.mjs +0 -60
  206. package/lib/adapters/DslAdapter.js +0 -995
  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 -1589
  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
package/src/index.ts ADDED
@@ -0,0 +1,633 @@
1
+ /**
2
+ * schema-dsl v2 — main entry point
3
+ *
4
+ * Fix IX-01: VERSION is read dynamically from package.json instead of being hard-coded
5
+ *
6
+ * @module schema-dsl
7
+ * @version 2.0.0
8
+ */
9
+
10
+ // ==================== Version (fix IX-01) ====================
11
+ import pkg from '../package.json' with { type: 'json' }
12
+ export const VERSION: string = (pkg as { version: string }).version
13
+
14
+ // ==================== Core classes ====================
15
+ export { Validator } from './core/Validator.js'
16
+ export { JSONSchemaCore } from './core/JSONSchemaCore.js'
17
+ export { DslBuilder } from './core/DslBuilder.js'
18
+ export { ConditionalBuilder } from './core/ConditionalBuilder.js'
19
+ export { ObjectDslBuilder } from './core/ObjectDslBuilder.js'
20
+ export { Locale } from './core/Locale.js'
21
+ export { CacheManager } from './core/CacheManager.js'
22
+ export { ErrorFormatter } from './core/ErrorFormatter.js'
23
+ export { MessageTemplate } from './core/MessageTemplate.js'
24
+ export { renderTemplate } from './core/TemplateEngine.js'
25
+ export { PluginManager } from './core/PluginManager.js'
26
+
27
+ // ==================== Parser layer ====================
28
+ export { TypeRegistry } from './parser/TypeRegistry.js'
29
+
30
+ // ==================== Error classes ====================
31
+ export { ValidationError } from './errors/ValidationError.js'
32
+ export { I18nError } from './errors/I18nError.js'
33
+
34
+ // ==================== String extensions ====================
35
+ export { uninstallStringExtensions } from './core/StringExtensions.js'
36
+
37
+ // ==================== Exporters ====================
38
+ export {
39
+ BaseExporter,
40
+ MongoDBExporter,
41
+ MySQLExporter,
42
+ PostgreSQLExporter,
43
+ MarkdownExporter,
44
+ } from './exporters/index.js'
45
+
46
+ // ==================== Utilities ====================
47
+ export { TypeConverter, SchemaHelper, SchemaUtils } from './utils/index.js'
48
+
49
+ // ==================== Validator extensions ====================
50
+ export { CustomKeywords } from './validators/CustomKeywords.js'
51
+
52
+ // ==================== Constants ====================
53
+ export { VALIDATION, CACHE, FORMATS, PATTERN_IPV4, PATTERN_IPV6 } from './config/constants.js'
54
+ export { ErrorCodes } from './core/ErrorCodes.js'
55
+ export { PATTERNS } from './config/patterns.js'
56
+
57
+ // ==================== Type exports ====================
58
+ export type { JSONSchema, SchemaIOOptions } from './types/schema.js'
59
+
60
+ export type {
61
+ IDslBuilder,
62
+ DslDefinition,
63
+ DslField,
64
+ DslInput,
65
+ DslFn,
66
+ DslIfFn,
67
+ DslConditionMarker,
68
+ DslErrorNamespace,
69
+ } from './types/dsl.js'
70
+
71
+ export type {
72
+ ValidateOptions,
73
+ ValidationResult,
74
+ ValidationErrorItem,
75
+ } from './types/validate.js'
76
+
77
+ export type { DslConfigOptions, I18nConfig, CacheOptions, ValidatorOptions } from './types/config.js'
78
+ // v1 BC: CacheConfig was renamed to CacheOptions in v2
79
+ export type { CacheOptions as CacheConfig } from './types/config.js'
80
+
81
+ export type { IConditionalBuilder } from './types/conditional.js'
82
+
83
+ export type {
84
+ InferSchema,
85
+ InferJsonSchema,
86
+ InferDslDefinition,
87
+ InferDslString,
88
+ } from './types/infer.js'
89
+
90
+ export type {
91
+ ExporterOptions,
92
+ MongoDBExporterOptions,
93
+ MySQLExporterOptions,
94
+ PostgreSQLExporterOptions,
95
+ MarkdownExporterOptions,
96
+ } from './exporters/index.js'
97
+
98
+ // ==================== dsl function (main API) ====================
99
+
100
+ import { DslBuilder as _DslBuilder } from './core/DslBuilder.js'
101
+ import { TypeRegistry as _TypeRegistry } from './parser/TypeRegistry.js'
102
+ import { DslAdapter as _DslAdapter } from './adapters/DslAdapter.js'
103
+ import { ConditionalBuilder as _ConditionalBuilder } from './core/ConditionalBuilder.js'
104
+ import { Locale as _Locale } from './core/Locale.js'
105
+ import { installStringExtensions as _install } from './core/StringExtensions.js'
106
+ import { PATTERNS as _PATTERNS } from './config/patterns.js'
107
+ import * as _CONSTANTS from './config/constants.js'
108
+ import * as _exporters from './exporters/index.js'
109
+ import { Validator as _Validator } from './core/Validator.js'
110
+ import { I18nError as _I18nError } from './errors/I18nError.js'
111
+ import type { LocaleMessage as _LocaleMessage } from './locales/types.js'
112
+ import type { JSONSchema as _JSONSchema } from './types/schema.js'
113
+ import type { IDslBuilder as _IDslBuilder, DslDefinition as _DslDefinition, DslConditionMarker as _DslConditionMarker } from './types/dsl.js'
114
+ import type { IConditionalBuilder as _IConditionalBuilder } from './types/conditional.js'
115
+ import type { DslConfigOptions as _DslConfigOptions } from './types/config.js'
116
+ import type { ValidationResult as _ValidationResult } from './types/validate.js'
117
+ import JSON5 from 'json5'
118
+ import { createRequire } from 'node:module'
119
+ import { readdirSync, statSync, readFileSync } from 'node:fs'
120
+ import { join, basename, extname } from 'node:path'
121
+
122
+ export const CONSTANTS = _CONSTANTS
123
+ export const exporters = _exporters
124
+
125
+ // Import all default locales for automatic initialization
126
+ import * as _locales from './locales/index.js'
127
+
128
+ // Initialize default locales at module load time
129
+ ; (() => {
130
+ for (const [locale, messages] of Object.entries(_locales)) {
131
+ _Locale.addLocale(locale, messages as Record<string, string>)
132
+ }
133
+ })()
134
+
135
+ // ==================== smartCoerceTypes ====================
136
+
137
+ // Perf O5b: pre-compute the set of coercible field candidates for a schema
138
+ // Avoids scanning all keys of `data` on every smartCoerceTypes call.
139
+ // Only iterates fields that may need coercion (numbers/arrays/objects).
140
+ type _CoerceCandidates = {
141
+ numbers: string[] // fields with type: 'number' | 'integer'
142
+ booleans: string[] // fields with type: 'boolean'
143
+ arrays: Array<{ key: string; itemType: 'number' | 'integer' | 'boolean' }>
144
+ objects: Array<{ key: string; schema: _JSONSchema }> // nested objects with properties
145
+ } | null // null = no coercible fields
146
+
147
+ const _coerceCandidatesCache = new WeakMap<object, _CoerceCandidates>()
148
+
149
+ function _getCoerceCandidates(schema: _JSONSchema): _CoerceCandidates {
150
+ const schemaObj = schema as object
151
+ const cached = _coerceCandidatesCache.get(schemaObj)
152
+ if (cached !== undefined) return cached
153
+
154
+ const props = schema.properties as Record<string, _JSONSchema> | undefined
155
+ if (!props) {
156
+ _coerceCandidatesCache.set(schemaObj, null)
157
+ return null
158
+ }
159
+
160
+ const numbers: string[] = []
161
+ const booleans: string[] = []
162
+ const arrays: Array<{ key: string; itemType: 'number' | 'integer' | 'boolean' }> = []
163
+ const objects: Array<{ key: string; schema: _JSONSchema }> = []
164
+
165
+ for (const [key, f] of Object.entries(props)) {
166
+ if (f.enum) continue
167
+ const ft = f.type
168
+ if (ft === 'number' || ft === 'integer') {
169
+ numbers.push(key)
170
+ } else if (ft === 'boolean') {
171
+ booleans.push(key)
172
+ } else if (ft === 'array' && (f.items as _JSONSchema | undefined)?.type === 'number') {
173
+ arrays.push({ key, itemType: 'number' })
174
+ } else if (ft === 'array' && (f.items as _JSONSchema | undefined)?.type === 'integer') {
175
+ arrays.push({ key, itemType: 'integer' })
176
+ } else if (ft === 'array' && (f.items as _JSONSchema | undefined)?.type === 'boolean') {
177
+ arrays.push({ key, itemType: 'boolean' })
178
+ } else if (ft === 'object' && f.properties) {
179
+ objects.push({ key, schema: f })
180
+ }
181
+ }
182
+
183
+ const result: _CoerceCandidates = (numbers.length || booleans.length || arrays.length || objects.length)
184
+ ? { numbers, booleans, arrays, objects }
185
+ : null
186
+ _coerceCandidatesCache.set(schemaObj, result)
187
+ return result
188
+ }
189
+
190
+ function _coerceNumber(value: unknown): unknown {
191
+ if (typeof value !== 'string') return value
192
+ const trimmed = value.trim()
193
+ if (trimmed === '') return value
194
+ const num = Number(trimmed)
195
+ return !isNaN(num) ? num : value
196
+ }
197
+
198
+ function _coerceBoolean(value: unknown): unknown {
199
+ if (typeof value !== 'string') return value
200
+ const trimmed = value.trim().toLowerCase()
201
+ if (trimmed === 'true') return true
202
+ if (trimmed === 'false') return false
203
+ return value
204
+ }
205
+
206
+ function smartCoerceTypes(data: unknown, schema: _JSONSchema): unknown {
207
+ if (!data || typeof data !== 'object') return data
208
+
209
+ if (Array.isArray(data)) {
210
+ return data.map(item => smartCoerceTypes(item, schema))
211
+ }
212
+
213
+ // O5b: use pre-computed candidate list instead of Object.keys(data) scan
214
+ // Only processes fields known to potentially need coercion
215
+ const candidates = _getCoerceCandidates(schema)
216
+ if (!candidates) return data // fast path: no coercible fields
217
+
218
+ let result: Record<string, unknown> | null = null
219
+ const src = data as Record<string, unknown>
220
+
221
+ for (const key of candidates.numbers) {
222
+ const value = src[key]
223
+ const converted = _coerceNumber(value)
224
+ if (converted !== value) {
225
+ if (!result) result = { ...src }
226
+ result[key] = converted
227
+ }
228
+ }
229
+
230
+ for (const key of candidates.booleans) {
231
+ const value = src[key]
232
+ const converted = _coerceBoolean(value)
233
+ if (converted !== value) {
234
+ if (!result) result = { ...src }
235
+ result[key] = converted
236
+ }
237
+ }
238
+
239
+ for (const { key, itemType } of candidates.arrays) {
240
+ const value = src[key]
241
+ if (Array.isArray(value)) {
242
+ const converted = value.map(item => {
243
+ if (itemType === 'boolean') return _coerceBoolean(item)
244
+ return _coerceNumber(item)
245
+ })
246
+ if (!result) result = { ...src }
247
+ result[key] = converted
248
+ }
249
+ }
250
+
251
+ for (const { key, schema: nestedSchema } of candidates.objects) {
252
+ const value = src[key]
253
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
254
+ const converted = smartCoerceTypes(value, nestedSchema)
255
+ if (converted !== value) {
256
+ if (!result) result = { ...src }
257
+ result[key] = converted
258
+ }
259
+ }
260
+ }
261
+
262
+ return result ?? data // return original when no conversion needed (zero-copy)
263
+ }
264
+
265
+ // ==================== Top-level schema normalization (raw DSL object support) ====================
266
+
267
+ const _JSON_SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'array', 'object', 'null'])
268
+
269
+ function _isRawJsonSchemaLike(obj: Record<string, unknown>): boolean {
270
+ if (typeof obj['type'] === 'string' && _JSON_SCHEMA_TYPES.has(obj['type'] as string)) return true
271
+ if ('anyOf' in obj || 'oneOf' in obj || 'allOf' in obj || '$ref' in obj || '$defs' in obj || 'definitions' in obj) return true
272
+
273
+ const props = obj['properties']
274
+ if (props && typeof props === 'object' && !Array.isArray(props)) {
275
+ const values = Object.values(props as Record<string, unknown>)
276
+ if (values.length === 0) return true
277
+ if (values.every(value => value && typeof value === 'object' && !Array.isArray(value) && _isRawJsonSchemaLike(value as Record<string, unknown>))) {
278
+ return true
279
+ }
280
+ }
281
+
282
+ const items = obj['items']
283
+ if (items && typeof items === 'object' && !Array.isArray(items)) {
284
+ return _isRawJsonSchemaLike(items as Record<string, unknown>)
285
+ }
286
+
287
+ return false
288
+ }
289
+
290
+ function _isDslObject(schema: unknown): schema is _DslDefinition {
291
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return false
292
+
293
+ const obj = schema as Record<string, unknown>
294
+ if (typeof obj['toSchema'] === 'function') return false
295
+ if (obj['_isConditional']) return false
296
+
297
+ return !_isRawJsonSchemaLike(obj)
298
+ }
299
+
300
+ // Perf O6: cache _normalizeSchemaInput results for immutable raw JSON Schema objects only.
301
+ // Plain DSL definition objects ({ email: 'email!' }) are mutable — skip cache to prevent
302
+ // stale results when the caller mutates the object between validate() calls (N-04 fix).
303
+ const _normalizeSchemaCache = new WeakMap<object, _JSONSchema>()
304
+
305
+ function _normalizeSchemaInput(schema: _JSONSchema | _DslDefinition | _IDslBuilder | _IConditionalBuilder): _JSONSchema {
306
+ if (!schema || typeof schema !== 'object') return schema as _JSONSchema
307
+
308
+ const obj = schema as Record<string, unknown>
309
+ if (typeof obj['toSchema'] === 'function') {
310
+ // Mutable builders: never cache — schema changes as chain methods are called
311
+ return (obj['toSchema'] as () => _JSONSchema)()
312
+ }
313
+ if (_isDslObject(schema)) {
314
+ // Plain DSL definition objects are mutable — skip cache
315
+ return _DslAdapter.parseObject(schema).toSchema()
316
+ }
317
+ // Raw JSON Schema objects: safe to cache (treated as immutable by convention)
318
+ const schemaObj = schema as object
319
+ const cached = _normalizeSchemaCache.get(schemaObj)
320
+ if (cached !== undefined) return cached
321
+ const result = schema as _JSONSchema
322
+ _normalizeSchemaCache.set(schemaObj, result)
323
+ return result
324
+ }
325
+
326
+ // ==================== i18n locale directory scan ====================
327
+
328
+ const _LOCALE_NAME_RE = /^[a-z]{2,3}(-[A-Z]{2,4})?$/
329
+ const _LOCALE_REQUIRE_EXTENSIONS = new Set(['.js', '.cjs', '.json'])
330
+ const _LOCALE_TEXT_EXTENSIONS = new Set(['.jsonc', '.json5'])
331
+
332
+ function _normalizeLocaleModule(moduleValue: unknown): Record<string, _LocaleMessage> | null {
333
+ if (!moduleValue || typeof moduleValue !== 'object' || Array.isArray(moduleValue)) return null
334
+
335
+ const raw = moduleValue as Record<string, unknown>
336
+ const keys = Object.keys(raw)
337
+ const defaultValue = raw['default']
338
+ const nonMetaKeys = keys.filter(key => key !== '__esModule' && key !== 'default')
339
+
340
+ if (defaultValue && typeof defaultValue === 'object' && !Array.isArray(defaultValue) && nonMetaKeys.length === 0) {
341
+ return defaultValue as Record<string, _LocaleMessage>
342
+ }
343
+
344
+ return raw as Record<string, _LocaleMessage>
345
+ }
346
+
347
+ function _loadLocaleFile(fullPath: string, ext: string, _require: NodeRequire): Record<string, _LocaleMessage> | null {
348
+ if (_LOCALE_TEXT_EXTENSIONS.has(ext)) {
349
+ const rawText = readFileSync(fullPath, 'utf8')
350
+ return _normalizeLocaleModule(JSON5.parse(rawText) as Record<string, _LocaleMessage>)
351
+ }
352
+
353
+ if (_LOCALE_REQUIRE_EXTENSIONS.has(ext)) {
354
+ return _normalizeLocaleModule(_require(fullPath) as Record<string, _LocaleMessage>)
355
+ }
356
+
357
+ return null
358
+ }
359
+
360
+ function _loadLocalesFromDir(dirPath: string, strict = false): void {
361
+ let _require: NodeRequire
362
+ try {
363
+ // ESM: import.meta.url is defined
364
+ _require = createRequire(import.meta.url)
365
+ } catch {
366
+ // CJS fallback: import.meta.url is undefined
367
+ _require = typeof require !== 'undefined' ? require : createRequire(__filename)
368
+ }
369
+
370
+ // Track registered keys per locale for conflict detection
371
+ const registeredKeys = new Map<string, Map<string, string>>() // locale → key → filePath
372
+
373
+ function scanDir(dir: string): void {
374
+ let entries: string[]
375
+ try {
376
+ entries = readdirSync(dir)
377
+ } catch {
378
+ return
379
+ }
380
+ for (const entry of entries) {
381
+ const fullPath = join(dir, entry)
382
+ let stat
383
+ try {
384
+ stat = statSync(fullPath)
385
+ } catch {
386
+ continue
387
+ }
388
+ if (stat.isDirectory()) {
389
+ scanDir(fullPath)
390
+ } else {
391
+ const ext = extname(entry).toLowerCase()
392
+ if (!_LOCALE_REQUIRE_EXTENSIONS.has(ext) && !_LOCALE_TEXT_EXTENSIONS.has(ext)) continue
393
+
394
+ const locale = basename(entry, ext)
395
+ // Only load files that look like locale identifiers (e.g., zh-CN, en-US, zh, en)
396
+ if (_LOCALE_NAME_RE.test(locale)) {
397
+ try {
398
+ const messages = _loadLocaleFile(fullPath, ext, _require)
399
+ if (messages && typeof messages === 'object') {
400
+ // Conflict detection
401
+ if (!registeredKeys.has(locale)) registeredKeys.set(locale, new Map())
402
+ const localeKeys = registeredKeys.get(locale)!
403
+ for (const key of Object.keys(messages)) {
404
+ if (localeKeys.has(key)) {
405
+ const prevFile = localeKeys.get(key)!
406
+ if (strict) {
407
+ throw new Error(
408
+ `i18n locale "${locale}" key conflict: "${key}" is defined in both "${prevFile}" and "${fullPath}"`
409
+ )
410
+ } else {
411
+ console.warn(
412
+ `[schema-dsl] i18n key conflict: "${locale}:${key}" is defined in "${prevFile}" and "${fullPath}" (using latter)`
413
+ )
414
+ }
415
+ }
416
+ localeKeys.set(key, fullPath)
417
+ }
418
+ _Locale.addLocale(locale, messages as Record<string, _LocaleMessage>)
419
+ }
420
+ } catch (err) {
421
+ // Re-throw in strict mode; silently skip in default mode
422
+ if (strict && err instanceof Error && err.message.includes('i18n locale')) throw err
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ scanDir(dirPath)
430
+ }
431
+
432
+ // ==================== dsl.config ====================
433
+
434
+ function _dslConfig(options: Partial<_DslConfigOptions> = {}): void {
435
+ const strict = (options as Record<string, unknown>)['strict'] === true
436
+ _TypeRegistry.setStrict(strict)
437
+
438
+ if (options.patterns) {
439
+ const p = options.patterns as Record<string, unknown>
440
+ if (p['phone']) Object.assign(_PATTERNS.phone, p['phone'])
441
+ if (p['idCard']) Object.assign(_PATTERNS.idCard, p['idCard'])
442
+ if (p['creditCard']) Object.assign(_PATTERNS.creditCard, p['creditCard'])
443
+ }
444
+
445
+ // Legacy phone/idCard/creditCard at top level (v1 compat)
446
+ const raw = options as Record<string, unknown>
447
+ if (raw['phone'] && typeof raw['phone'] === 'object') Object.assign(_PATTERNS.phone, raw['phone'])
448
+ if (raw['idCard'] && typeof raw['idCard'] === 'object') Object.assign(_PATTERNS.idCard, raw['idCard'])
449
+ if (raw['creditCard'] && typeof raw['creditCard'] === 'object') Object.assign(_PATTERNS.creditCard, raw['creditCard'])
450
+
451
+ // Cache configuration — update default validator's cache options
452
+ const cacheConfig = (options as Record<string, unknown>)['cache'] as Record<string, unknown> | undefined
453
+ if (cacheConfig && typeof cacheConfig === 'object') {
454
+ const validator = _getDefaultValidator()
455
+ // Merge with existing options to preserve unspecified defaults
456
+ validator.cache.options = {
457
+ ...validator.cache.options,
458
+ ...cacheConfig,
459
+ } as Partial<{ maxSize: number; ttl: number; enabled: boolean; statsEnabled: boolean }>
460
+ }
461
+
462
+ if (options.i18n) {
463
+ if (typeof options.i18n === 'string') {
464
+ // Directory path: scan recursively for locale files
465
+ _loadLocalesFromDir(options.i18n, strict)
466
+ } else if (typeof options.i18n === 'object' && 'localesPath' in options.i18n) {
467
+ // { localesPath: string } form
468
+ _loadLocalesFromDir((options.i18n as { localesPath: string }).localesPath, strict)
469
+ } else if (typeof options.i18n === 'object' && 'locales' in options.i18n) {
470
+ // v1 / docs compat: { locales: { locale: messages } }
471
+ const locales = (options.i18n as { locales: Record<string, Record<string, string>> }).locales
472
+ for (const [locale, messages] of Object.entries(locales ?? {})) {
473
+ _Locale.addLocale(locale, messages)
474
+ }
475
+ } else if (typeof options.i18n === 'object' && !Array.isArray(options.i18n)) {
476
+ // Inline { locale: messages } mapping
477
+ for (const [locale, messages] of Object.entries(options.i18n)) {
478
+ _Locale.addLocale(locale, messages as Record<string, string>)
479
+ }
480
+ }
481
+ }
482
+ }
483
+
484
+ // ==================== Default Validator singleton ====================
485
+
486
+ let _defaultValidator: InstanceType<typeof _Validator> | null = null
487
+
488
+ function _getDefaultValidator(): InstanceType<typeof _Validator> {
489
+ if (!_defaultValidator) _defaultValidator = new _Validator()
490
+ return _defaultValidator
491
+ }
492
+
493
+ export { _getDefaultValidator as getDefaultValidator }
494
+
495
+ /**
496
+ * Reset the default Validator singleton (useful for cleaning up state in test environments)
497
+ */
498
+ export function resetDefaultValidator(): void {
499
+ _defaultValidator = null
500
+ }
501
+
502
+ /**
503
+ * Reset global runtime state that may leak across tests, workers, or tenants.
504
+ */
505
+ export function resetRuntimeState(): void {
506
+ resetDefaultValidator()
507
+ _DslBuilder.clearCustomTypes()
508
+ _Locale.reset()
509
+ }
510
+
511
+ // ==================== Convenience validation functions ====================
512
+
513
+ /**
514
+ * Convenience validate function (uses the default Validator singleton).
515
+ * Automatically coerces string → number when options.coerce !== false.
516
+ */
517
+ export function validate<T = unknown>(
518
+ schema: _JSONSchema | _DslDefinition | _IDslBuilder | _IConditionalBuilder,
519
+ data: T,
520
+ options: Record<string, unknown> = {},
521
+ ): _ValidationResult<T> {
522
+ const normalizedSchema = _normalizeSchemaInput(schema)
523
+ const shouldCoerce = options['coerce'] !== false
524
+ // O5b: use candidate-field cache instead of _hasCoercibleFields + Object.keys scan
525
+ const coercedData = shouldCoerce && _getCoerceCandidates(normalizedSchema)
526
+ ? smartCoerceTypes(data, normalizedSchema)
527
+ : data
528
+ return _getDefaultValidator().validate(normalizedSchema, coercedData as T, options)
529
+ }
530
+
531
+ /**
532
+ * Convenience async validate function
533
+ */
534
+ export async function validateAsync<T = unknown>(
535
+ schema: _JSONSchema | _DslDefinition | _IDslBuilder | _IConditionalBuilder,
536
+ data: T,
537
+ options: Record<string, unknown> = {},
538
+ ): Promise<T> {
539
+ const normalizedSchema = _normalizeSchemaInput(schema)
540
+ const shouldCoerce = options['coerce'] !== false
541
+ // O5b: use candidate-field cache instead of _hasCoercibleFields + Object.keys scan
542
+ const coercedData = shouldCoerce && _getCoerceCandidates(normalizedSchema)
543
+ ? smartCoerceTypes(data, normalizedSchema)
544
+ : data
545
+ return _getDefaultValidator().validateAsync(normalizedSchema, coercedData as T, options)
546
+ }
547
+
548
+ // ==================== dsl main function ====================
549
+
550
+ // Core dsl function: string → IDslBuilder (chain), object definition → JSONSchema
551
+ function _dslFn(def: string): _IDslBuilder
552
+ function _dslFn(def: _DslDefinition): _JSONSchema
553
+ function _dslFn(def: unknown): _IDslBuilder | _JSONSchema {
554
+ if (typeof def === 'string') return new _DslBuilder(def)
555
+ if (def === null || def === undefined || typeof def !== 'object' || Array.isArray(def)) {
556
+ throw new Error('[schema-dsl] Invalid DSL definition: expected string or object')
557
+ }
558
+ return _DslAdapter.parseObject(def as _DslDefinition).toSchema() as _JSONSchema
559
+ }
560
+
561
+ // Namespace shape (mirrors DslFn interface in types/dsl.ts)
562
+ const _dslWithNS = _dslFn as {
563
+ (def: string): _IDslBuilder
564
+ (def: _DslDefinition): _JSONSchema
565
+ config: (options?: Partial<_DslConfigOptions>) => void
566
+ if: {
567
+ (condition: string, thenSchema: unknown, elseSchema?: unknown): _DslConditionMarker
568
+ (condition: (data: unknown) => boolean): ReturnType<typeof _ConditionalBuilder.start>
569
+ }
570
+ _if: {
571
+ (condition: string, thenSchema: unknown, elseSchema?: unknown): _DslConditionMarker
572
+ (condition: (data: unknown) => boolean): ReturnType<typeof _ConditionalBuilder.start>
573
+ }
574
+ match: (value: unknown, cases: Record<string, unknown>) => _DslConditionMarker
575
+ error: {
576
+ create: typeof _I18nError.create
577
+ throw: typeof _I18nError.throw
578
+ assert: typeof _I18nError.assert
579
+ [key: string]: unknown
580
+ }
581
+ }
582
+
583
+ _dslWithNS.config = _dslConfig
584
+
585
+ function _dslIf(condition: string, thenSchema: unknown, elseSchema?: unknown): _DslConditionMarker
586
+ function _dslIf(condition: (data: unknown) => boolean): ReturnType<typeof _ConditionalBuilder.start>
587
+ function _dslIf(condition: string | ((data: unknown) => boolean), thenSchema?: unknown, elseSchema?: unknown): _DslConditionMarker | ReturnType<typeof _ConditionalBuilder.start> {
588
+ // When only a string is passed (no thenSchema), it's invalid — condition must be a function
589
+ // When a string + thenSchema are passed, the string is a field name reference (v1 compat)
590
+ if (typeof condition !== 'function' && thenSchema === undefined) {
591
+ throw new Error('Condition must be a function')
592
+ }
593
+ if (typeof condition === 'string') {
594
+ return _DslAdapter.if(condition, thenSchema, elseSchema) as _DslConditionMarker
595
+ }
596
+ return _ConditionalBuilder.start(condition)
597
+ }
598
+
599
+ _dslWithNS.if = _dslIf
600
+ _dslWithNS._if = _dslIf
601
+
602
+ _dslWithNS.match = (field: unknown, cases: Record<string, unknown>): _DslConditionMarker => {
603
+ return _DslAdapter.match(String(field), cases) as _DslConditionMarker
604
+ }
605
+
606
+ _dslWithNS.error = {
607
+ create: _I18nError.create.bind(_I18nError),
608
+ throw: _I18nError.throw.bind(_I18nError),
609
+ assert: _I18nError.assert.bind(_I18nError),
610
+ }
611
+
612
+ /**
613
+ * dsl — main API entry point
614
+ *
615
+ * @example
616
+ * // String DSL → DslBuilder (chainable)
617
+ * const builder = dsl('email!').label('Email address')
618
+ *
619
+ * @example
620
+ * // Object DSL → JSON Schema
621
+ * const schema = dsl({ email: 'email!', name: 'string:2-32!' })
622
+ */
623
+ export const dsl = _dslWithNS
624
+
625
+ export default dsl
626
+
627
+ export const config = _dslConfig
628
+
629
+ export function installStringExtensions(dslFunction: Parameters<typeof _install>[0] = _dslWithNS as unknown as Parameters<typeof _install>[0]): void {
630
+ _install(dslFunction)
631
+ }
632
+
633
+