schema-dsl 2.0.0 → 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.
- package/CHANGELOG.md +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
package/src/index.ts
CHANGED
|
@@ -1,633 +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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Convenience
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
function
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
+
// 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
|
+
|