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
package/src/index.ts ADDED
@@ -0,0 +1,651 @@
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
+ // Initial PATTERNS keys snapshot — used by resetRuntimeState() to prune user-added patterns
503
+ const _INITIAL_PATTERN_KEYS = {
504
+ phone: new Set(Object.keys(_PATTERNS.phone)),
505
+ idCard: new Set(Object.keys(_PATTERNS.idCard)),
506
+ creditCard: new Set(Object.keys(_PATTERNS.creditCard)),
507
+ }
508
+
509
+ /**
510
+ * Reset global runtime state that may leak across tests, workers, or tenants.
511
+ */
512
+ export function resetRuntimeState(): void {
513
+ resetDefaultValidator()
514
+ _DslBuilder.clearCustomTypes()
515
+ _Locale.reset()
516
+ _TypeRegistry.setStrict(false)
517
+ // Remove any keys added to PATTERNS via dsl.config({ patterns })
518
+ for (const key of Object.keys(_PATTERNS.phone)) {
519
+ if (!_INITIAL_PATTERN_KEYS.phone.has(key)) delete _PATTERNS.phone[key]
520
+ }
521
+ for (const key of Object.keys(_PATTERNS.idCard)) {
522
+ if (!_INITIAL_PATTERN_KEYS.idCard.has(key)) delete _PATTERNS.idCard[key]
523
+ }
524
+ for (const key of Object.keys(_PATTERNS.creditCard)) {
525
+ if (!_INITIAL_PATTERN_KEYS.creditCard.has(key)) delete _PATTERNS.creditCard[key]
526
+ }
527
+ }
528
+
529
+ // ==================== Convenience validation functions ====================
530
+
531
+ /**
532
+ * Convenience validate function (uses the default Validator singleton).
533
+ * Automatically coerces string → number when options.coerce !== false.
534
+ */
535
+ export function validate<T = unknown>(
536
+ schema: _JSONSchema | _DslDefinition | _IDslBuilder | _IConditionalBuilder,
537
+ data: T,
538
+ options: Record<string, unknown> = {},
539
+ ): _ValidationResult<T> {
540
+ const normalizedSchema = _normalizeSchemaInput(schema)
541
+ const shouldCoerce = options['coerce'] !== false
542
+ // O5b: use candidate-field cache instead of _hasCoercibleFields + Object.keys scan
543
+ const coercedData = shouldCoerce && _getCoerceCandidates(normalizedSchema)
544
+ ? smartCoerceTypes(data, normalizedSchema)
545
+ : data
546
+ return _getDefaultValidator().validate(normalizedSchema, coercedData as T, options)
547
+ }
548
+
549
+ /**
550
+ * Convenience async validate function
551
+ */
552
+ export async function validateAsync<T = unknown>(
553
+ schema: _JSONSchema | _DslDefinition | _IDslBuilder | _IConditionalBuilder,
554
+ data: T,
555
+ options: Record<string, unknown> = {},
556
+ ): Promise<T> {
557
+ const normalizedSchema = _normalizeSchemaInput(schema)
558
+ const shouldCoerce = options['coerce'] !== false
559
+ // O5b: use candidate-field cache instead of _hasCoercibleFields + Object.keys scan
560
+ const coercedData = shouldCoerce && _getCoerceCandidates(normalizedSchema)
561
+ ? smartCoerceTypes(data, normalizedSchema)
562
+ : data
563
+ return _getDefaultValidator().validateAsync(normalizedSchema, coercedData as T, options)
564
+ }
565
+
566
+ // ==================== dsl main function ====================
567
+
568
+ // Core dsl function: string → IDslBuilder (chain), object definition → JSONSchema
569
+ function _dslFn(def: string): _IDslBuilder
570
+ function _dslFn(def: _DslDefinition): _JSONSchema
571
+ function _dslFn(def: unknown): _IDslBuilder | _JSONSchema {
572
+ if (typeof def === 'string') return new _DslBuilder(def)
573
+ if (def === null || def === undefined || typeof def !== 'object' || Array.isArray(def)) {
574
+ throw new Error('[schema-dsl] Invalid DSL definition: expected string or object')
575
+ }
576
+ return _DslAdapter.parseObject(def as _DslDefinition).toSchema() as _JSONSchema
577
+ }
578
+
579
+ // Namespace shape (mirrors DslFn interface in types/dsl.ts)
580
+ const _dslWithNS = _dslFn as {
581
+ (def: string): _IDslBuilder
582
+ (def: _DslDefinition): _JSONSchema
583
+ config: (options?: Partial<_DslConfigOptions>) => void
584
+ if: {
585
+ (condition: string, thenSchema: unknown, elseSchema?: unknown): _DslConditionMarker
586
+ (condition: (data: unknown) => boolean): ReturnType<typeof _ConditionalBuilder.start>
587
+ }
588
+ _if: {
589
+ (condition: string, thenSchema: unknown, elseSchema?: unknown): _DslConditionMarker
590
+ (condition: (data: unknown) => boolean): ReturnType<typeof _ConditionalBuilder.start>
591
+ }
592
+ match: (value: unknown, cases: Record<string, unknown>) => _DslConditionMarker
593
+ error: {
594
+ create: typeof _I18nError.create
595
+ throw: typeof _I18nError.throw
596
+ assert: typeof _I18nError.assert
597
+ [key: string]: unknown
598
+ }
599
+ }
600
+
601
+ _dslWithNS.config = _dslConfig
602
+
603
+ function _dslIf(condition: string, thenSchema: unknown, elseSchema?: unknown): _DslConditionMarker
604
+ function _dslIf(condition: (data: unknown) => boolean): ReturnType<typeof _ConditionalBuilder.start>
605
+ function _dslIf(condition: string | ((data: unknown) => boolean), thenSchema?: unknown, elseSchema?: unknown): _DslConditionMarker | ReturnType<typeof _ConditionalBuilder.start> {
606
+ // When only a string is passed (no thenSchema), it's invalid — condition must be a function
607
+ // When a string + thenSchema are passed, the string is a field name reference (v1 compat)
608
+ if (typeof condition !== 'function' && thenSchema === undefined) {
609
+ throw new Error('Condition must be a function')
610
+ }
611
+ if (typeof condition === 'string') {
612
+ return _DslAdapter.if(condition, thenSchema, elseSchema) as _DslConditionMarker
613
+ }
614
+ return _ConditionalBuilder.start(condition)
615
+ }
616
+
617
+ _dslWithNS.if = _dslIf
618
+ _dslWithNS._if = _dslIf
619
+
620
+ _dslWithNS.match = (field: unknown, cases: Record<string, unknown>): _DslConditionMarker => {
621
+ return _DslAdapter.match(String(field), cases) as _DslConditionMarker
622
+ }
623
+
624
+ _dslWithNS.error = {
625
+ create: _I18nError.create.bind(_I18nError),
626
+ throw: _I18nError.throw.bind(_I18nError),
627
+ assert: _I18nError.assert.bind(_I18nError),
628
+ }
629
+
630
+ /**
631
+ * dsl — main API entry point
632
+ *
633
+ * @example
634
+ * // String DSL → DslBuilder (chainable)
635
+ * const builder = dsl('email!').label('Email address')
636
+ *
637
+ * @example
638
+ * // Object DSL → JSON Schema
639
+ * const schema = dsl({ email: 'email!', name: 'string:2-32!' })
640
+ */
641
+ export const dsl = _dslWithNS
642
+
643
+ export default dsl
644
+
645
+ export const config = _dslConfig
646
+
647
+ export function installStringExtensions(dslFunction: Parameters<typeof _install>[0] = _dslWithNS as unknown as Parameters<typeof _install>[0]): void {
648
+ _install(dslFunction)
649
+ }
650
+
651
+