@amritk/generate-validators 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/generators/build-schema.d.ts +0 -1
- package/dist/generators/build-schema.js +29 -0
- package/dist/generators/collect-validator-imports.d.ts +0 -1
- package/dist/generators/collect-validator-imports.js +5 -1
- package/dist/generators/generate-files.d.ts +0 -1
- package/dist/generators/generate-files.js +6 -0
- package/dist/generators/generate-validator-function.d.ts +0 -1
- package/dist/generators/generate-validator-function.js +309 -40
- package/dist/index.d.ts +0 -1
- package/package.json +10 -7
- package/dist/generators/build-schema.d.ts.map +0 -1
- package/dist/generators/collect-validator-imports.d.ts.map +0 -1
- package/dist/generators/generate-files.d.ts.map +0 -1
- package/dist/generators/generate-validator-function.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/src/generators/build-schema.test.ts +0 -163
- package/src/generators/build-schema.ts +0 -81
- package/src/generators/collect-validator-imports.ts +0 -134
- package/src/generators/generate-files.ts +0 -79
- package/src/generators/generate-validator-function.test.ts +0 -311
- package/src/generators/generate-validator-function.ts +0 -550
- package/src/index.ts +0 -2
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern'
|
|
2
|
-
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
3
|
-
import { refToName } from '@amritk/helpers/ref-to-name'
|
|
4
|
-
import {
|
|
5
|
-
hasAdditionalProperties,
|
|
6
|
-
hasConst,
|
|
7
|
-
hasDependentRequired,
|
|
8
|
-
hasEnum,
|
|
9
|
-
hasExclusiveMaximum,
|
|
10
|
-
hasExclusiveMinimum,
|
|
11
|
-
hasItems,
|
|
12
|
-
hasMaximum,
|
|
13
|
-
hasMaxLength,
|
|
14
|
-
hasMinimum,
|
|
15
|
-
hasMinLength,
|
|
16
|
-
hasMultipleOf,
|
|
17
|
-
hasOneOf,
|
|
18
|
-
hasPattern,
|
|
19
|
-
hasProperties,
|
|
20
|
-
hasPropertyNames,
|
|
21
|
-
hasRef,
|
|
22
|
-
hasRequired,
|
|
23
|
-
hasType,
|
|
24
|
-
isObjectSchema,
|
|
25
|
-
isSchemaObject,
|
|
26
|
-
} from '@amritk/helpers/schema-guards'
|
|
27
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Derives the validator function name from a type name.
|
|
31
|
-
* e.g. "InfoObject" → "validateInfoObject"
|
|
32
|
-
*/
|
|
33
|
-
const validatorName = (typeName: string): string => `validate${typeName}`
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Returns the TypeScript typeof string for a JSON Schema primitive type.
|
|
37
|
-
*/
|
|
38
|
-
const typeofString = (type: string): string => {
|
|
39
|
-
if (type === 'integer') return 'number'
|
|
40
|
-
return type
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Generates the inline condition that is TRUE when `accessor` does NOT equal the
|
|
45
|
-
* `const` value. Primitives compare with `!==`; objects/arrays compare by their
|
|
46
|
-
* canonical JSON serialization (sufficient for the literal, fixed shapes `const`
|
|
47
|
-
* is used for).
|
|
48
|
-
*/
|
|
49
|
-
const constMismatchCondition = (accessor: string, value: unknown): string => {
|
|
50
|
-
if (value === null || typeof value !== 'object') {
|
|
51
|
-
return `${accessor} !== ${JSON.stringify(value)}`
|
|
52
|
-
}
|
|
53
|
-
return `JSON.stringify(${accessor}) !== ${JSON.stringify(JSON.stringify(value))}`
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Generates the inline condition that is TRUE when a value is the wrong type.
|
|
58
|
-
*/
|
|
59
|
-
const wrongTypeCondition = (accessor: string, type: string): string => {
|
|
60
|
-
switch (type) {
|
|
61
|
-
case 'string':
|
|
62
|
-
return `typeof ${accessor} !== 'string'`
|
|
63
|
-
case 'number':
|
|
64
|
-
case 'integer':
|
|
65
|
-
return `typeof ${accessor} !== 'number'`
|
|
66
|
-
case 'boolean':
|
|
67
|
-
return `typeof ${accessor} !== 'boolean'`
|
|
68
|
-
case 'array':
|
|
69
|
-
return `!Array.isArray(${accessor})`
|
|
70
|
-
case 'object':
|
|
71
|
-
return `typeof ${accessor} !== 'object' || ${accessor} === null || Array.isArray(${accessor})`
|
|
72
|
-
default:
|
|
73
|
-
return ''
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Generates validation lines for a single property in an object schema.
|
|
79
|
-
* Handles $ref delegation, enum checks, type checks, and string/number constraints.
|
|
80
|
-
*/
|
|
81
|
-
const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean, suffix: string): string[] => {
|
|
82
|
-
if (!isSchemaObject(propSchema)) return []
|
|
83
|
-
|
|
84
|
-
const raw = `obj[${JSON.stringify(key)}]`
|
|
85
|
-
const path = `\`\${_path}/${key}\``
|
|
86
|
-
const lines: string[] = []
|
|
87
|
-
|
|
88
|
-
// $ref — delegate to the imported validator
|
|
89
|
-
if (hasRef(propSchema)) {
|
|
90
|
-
const ref = propSchema.$ref
|
|
91
|
-
const vName = validatorName(refToName(ref, suffix))
|
|
92
|
-
|
|
93
|
-
if (isRequired) {
|
|
94
|
-
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
95
|
-
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
96
|
-
lines.push(` } else {`)
|
|
97
|
-
lines.push(` const _r = ${vName}(${raw}, ${path})`)
|
|
98
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`)
|
|
99
|
-
lines.push(` }`)
|
|
100
|
-
} else {
|
|
101
|
-
lines.push(` if (${raw} !== undefined) {`)
|
|
102
|
-
lines.push(` const _r = ${vName}(${raw}, ${path})`)
|
|
103
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`)
|
|
104
|
-
lines.push(` }`)
|
|
105
|
-
}
|
|
106
|
-
return lines
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// x-mjst instanceOf (e.g. Date) — value must be an instance of the class
|
|
110
|
-
const instanceOf = getMjstInstanceOf(propSchema)
|
|
111
|
-
if (instanceOf) {
|
|
112
|
-
if (isRequired) {
|
|
113
|
-
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
114
|
-
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
115
|
-
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`)
|
|
116
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`)
|
|
117
|
-
lines.push(` }`)
|
|
118
|
-
} else {
|
|
119
|
-
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`)
|
|
120
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`)
|
|
121
|
-
lines.push(` }`)
|
|
122
|
-
}
|
|
123
|
-
return lines
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// x-mjst primitive (e.g. bigint) — value must satisfy a typeof check
|
|
127
|
-
const primitive = getMjstPrimitive(propSchema)
|
|
128
|
-
if (primitive) {
|
|
129
|
-
if (isRequired) {
|
|
130
|
-
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
131
|
-
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
132
|
-
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`)
|
|
133
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`)
|
|
134
|
-
lines.push(` }`)
|
|
135
|
-
} else {
|
|
136
|
-
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`)
|
|
137
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`)
|
|
138
|
-
lines.push(` }`)
|
|
139
|
-
}
|
|
140
|
-
return lines
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// const — value must equal the fixed value exactly
|
|
144
|
-
if (hasConst(propSchema)) {
|
|
145
|
-
const mismatch = constMismatchCondition(raw, propSchema.const)
|
|
146
|
-
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`)
|
|
147
|
-
if (isRequired) {
|
|
148
|
-
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
149
|
-
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
150
|
-
lines.push(` } else if (${mismatch}) {`)
|
|
151
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`)
|
|
152
|
-
lines.push(` }`)
|
|
153
|
-
} else {
|
|
154
|
-
lines.push(` if (${raw} !== undefined && ${mismatch}) {`)
|
|
155
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`)
|
|
156
|
-
lines.push(` }`)
|
|
157
|
-
}
|
|
158
|
-
return lines
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// enum
|
|
162
|
-
if (hasEnum(propSchema)) {
|
|
163
|
-
const allowed = JSON.stringify(propSchema.enum)
|
|
164
|
-
const label = (propSchema.enum as unknown[]).map((v) => JSON.stringify(v)).join(', ')
|
|
165
|
-
|
|
166
|
-
if (isRequired) {
|
|
167
|
-
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
168
|
-
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
169
|
-
lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`)
|
|
170
|
-
lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`)
|
|
171
|
-
lines.push(` }`)
|
|
172
|
-
} else {
|
|
173
|
-
lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`)
|
|
174
|
-
lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`)
|
|
175
|
-
lines.push(` }`)
|
|
176
|
-
}
|
|
177
|
-
return lines
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// typed property
|
|
181
|
-
if (hasType(propSchema)) {
|
|
182
|
-
const t = propSchema.type as string
|
|
183
|
-
const wrongType = wrongTypeCondition(raw, t)
|
|
184
|
-
const typLabel = typeofString(t)
|
|
185
|
-
|
|
186
|
-
if (isRequired) {
|
|
187
|
-
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
188
|
-
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
189
|
-
if (wrongType) {
|
|
190
|
-
lines.push(` } else if (${wrongType}) {`)
|
|
191
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`)
|
|
192
|
-
}
|
|
193
|
-
lines.push(` }`)
|
|
194
|
-
} else if (wrongType) {
|
|
195
|
-
lines.push(` if (${raw} !== undefined && (${wrongType})) {`)
|
|
196
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`)
|
|
197
|
-
lines.push(` }`)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// String constraints
|
|
201
|
-
if (t === 'string') {
|
|
202
|
-
if (hasPattern(propSchema)) {
|
|
203
|
-
const re = escapeRegexPattern(propSchema.pattern)
|
|
204
|
-
const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`)
|
|
205
|
-
lines.push(` if (typeof ${raw} === 'string' && !/${re}/.test(${raw})) {`)
|
|
206
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`)
|
|
207
|
-
lines.push(` }`)
|
|
208
|
-
}
|
|
209
|
-
if (hasMinLength(propSchema)) {
|
|
210
|
-
lines.push(` if (typeof ${raw} === 'string' && ${raw}.length < ${propSchema.minLength}) {`)
|
|
211
|
-
lines.push(
|
|
212
|
-
` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`,
|
|
213
|
-
)
|
|
214
|
-
lines.push(` }`)
|
|
215
|
-
}
|
|
216
|
-
if (hasMaxLength(propSchema)) {
|
|
217
|
-
lines.push(` if (typeof ${raw} === 'string' && ${raw}.length > ${propSchema.maxLength}) {`)
|
|
218
|
-
lines.push(
|
|
219
|
-
` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`,
|
|
220
|
-
)
|
|
221
|
-
lines.push(` }`)
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Number constraints
|
|
226
|
-
if (t === 'number' || t === 'integer') {
|
|
227
|
-
if (hasMinimum(propSchema)) {
|
|
228
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} < ${propSchema.minimum}) {`)
|
|
229
|
-
lines.push(` errors.push({ message: 'must be >= ${propSchema.minimum}', path: ${path} })`)
|
|
230
|
-
lines.push(` }`)
|
|
231
|
-
}
|
|
232
|
-
if (hasMaximum(propSchema)) {
|
|
233
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} > ${propSchema.maximum}) {`)
|
|
234
|
-
lines.push(` errors.push({ message: 'must be <= ${propSchema.maximum}', path: ${path} })`)
|
|
235
|
-
lines.push(` }`)
|
|
236
|
-
}
|
|
237
|
-
if (hasExclusiveMinimum(propSchema)) {
|
|
238
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`)
|
|
239
|
-
lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`)
|
|
240
|
-
lines.push(` }`)
|
|
241
|
-
}
|
|
242
|
-
if (hasExclusiveMaximum(propSchema)) {
|
|
243
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`)
|
|
244
|
-
lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`)
|
|
245
|
-
lines.push(` }`)
|
|
246
|
-
}
|
|
247
|
-
if (hasMultipleOf(propSchema)) {
|
|
248
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} % ${propSchema.multipleOf} !== 0) {`)
|
|
249
|
-
lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`)
|
|
250
|
-
lines.push(` }`)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Array with typed items
|
|
255
|
-
if (t === 'array' && hasItems(propSchema)) {
|
|
256
|
-
const itemSchema = propSchema.items
|
|
257
|
-
if (hasRef(itemSchema)) {
|
|
258
|
-
const vName = validatorName(refToName(itemSchema.$ref, suffix))
|
|
259
|
-
lines.push(` if (Array.isArray(${raw})) {`)
|
|
260
|
-
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`)
|
|
261
|
-
lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`)
|
|
262
|
-
lines.push(` if (_ir !== true) errors.push(..._ir.errors)`)
|
|
263
|
-
lines.push(` }`)
|
|
264
|
-
lines.push(` }`)
|
|
265
|
-
} else if (hasType(itemSchema)) {
|
|
266
|
-
const itemType = itemSchema.type as string
|
|
267
|
-
const itemWrong = wrongTypeCondition('_item', itemType)
|
|
268
|
-
const itemLabel = typeofString(itemType)
|
|
269
|
-
if (itemWrong) {
|
|
270
|
-
lines.push(` if (Array.isArray(${raw})) {`)
|
|
271
|
-
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`)
|
|
272
|
-
lines.push(` const _item = ${raw}[_i]`)
|
|
273
|
-
lines.push(
|
|
274
|
-
` if (${itemWrong}) errors.push({ message: 'items must be ${itemLabel}', path: \`${path.slice(1, -1)}/${key}/\${_i}\` })`,
|
|
275
|
-
)
|
|
276
|
-
lines.push(` }`)
|
|
277
|
-
lines.push(` }`)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return lines
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Generates a validator function body for an object schema, checking each
|
|
288
|
-
* property's presence and type and collecting all errors.
|
|
289
|
-
*/
|
|
290
|
-
const generateObjectValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
|
|
291
|
-
const vName = validatorName(typeName)
|
|
292
|
-
const required = new Set(hasRequired(schema) ? schema.required : [])
|
|
293
|
-
const properties = hasProperties(schema) ? schema.properties : {}
|
|
294
|
-
|
|
295
|
-
const propertyLines: string[] = []
|
|
296
|
-
|
|
297
|
-
for (const [key, propSchema] of Object.entries(properties)) {
|
|
298
|
-
const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key), suffix)
|
|
299
|
-
if (checks.length > 0) {
|
|
300
|
-
propertyLines.push(...checks)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// additionalProperties with a $ref schema validates all extra keys
|
|
305
|
-
if (
|
|
306
|
-
hasAdditionalProperties(schema) &&
|
|
307
|
-
isSchemaObject(schema.additionalProperties) &&
|
|
308
|
-
hasRef(schema.additionalProperties)
|
|
309
|
-
) {
|
|
310
|
-
const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix))
|
|
311
|
-
propertyLines.push(` for (const _key of Object.keys(obj)) {`)
|
|
312
|
-
propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`)
|
|
313
|
-
propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`)
|
|
314
|
-
propertyLines.push(` if (_r !== true) errors.push(..._r.errors)`)
|
|
315
|
-
propertyLines.push(` }`)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// dependentRequired — when a trigger property is present, its dependencies must be too.
|
|
319
|
-
if (hasDependentRequired(schema)) {
|
|
320
|
-
for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
|
|
321
|
-
if (!Array.isArray(deps)) continue
|
|
322
|
-
for (const dep of deps) {
|
|
323
|
-
const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`)
|
|
324
|
-
propertyLines.push(` if (${JSON.stringify(trigger)} in obj && !(${JSON.stringify(dep)} in obj)) {`)
|
|
325
|
-
propertyLines.push(` errors.push({ message: ${msg}, path: _path })`)
|
|
326
|
-
propertyLines.push(` }`)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// propertyNames with a pattern — every key must match. (Only the pattern form
|
|
332
|
-
// is emitted; a $ref/complex propertyNames schema is left to runtime validation.)
|
|
333
|
-
if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames) && hasPattern(schema.propertyNames)) {
|
|
334
|
-
const re = escapeRegexPattern(schema.propertyNames.pattern)
|
|
335
|
-
const msg = JSON.stringify(`property name must match pattern ${schema.propertyNames.pattern}`)
|
|
336
|
-
propertyLines.push(` for (const _name of Object.keys(obj)) {`)
|
|
337
|
-
propertyLines.push(` if (!/${re}/.test(_name)) {`)
|
|
338
|
-
propertyLines.push(` errors.push({ message: ${msg}, path: \`\${_path}/\${_name}\` })`)
|
|
339
|
-
propertyLines.push(` }`)
|
|
340
|
-
propertyLines.push(` }`)
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const body = propertyLines.length > 0 ? '\n' + propertyLines.join('\n') + '\n' : ''
|
|
344
|
-
|
|
345
|
-
return [
|
|
346
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
347
|
-
` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
|
|
348
|
-
` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
|
|
349
|
-
` }`,
|
|
350
|
-
``,
|
|
351
|
-
` const errors: ValidationError[] = []`,
|
|
352
|
-
` const obj = input as Record<string, unknown>`,
|
|
353
|
-
body,
|
|
354
|
-
` return errors.length > 0 ? { valid: false, errors } : true`,
|
|
355
|
-
`}`,
|
|
356
|
-
].join('\n')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Generates a validator function for a non-object schema (primitive, array, enum, $ref).
|
|
361
|
-
*/
|
|
362
|
-
const generateScalarValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
|
|
363
|
-
const vName = validatorName(typeName)
|
|
364
|
-
|
|
365
|
-
if (!isSchemaObject(schema)) {
|
|
366
|
-
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
|
|
367
|
-
'\n',
|
|
368
|
-
)
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Top-level $ref — delegate entirely
|
|
372
|
-
if (hasRef(schema)) {
|
|
373
|
-
const delegateName = validatorName(refToName(schema.$ref, suffix))
|
|
374
|
-
return [
|
|
375
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
376
|
-
` return ${delegateName}(input, _path)`,
|
|
377
|
-
`}`,
|
|
378
|
-
].join('\n')
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Top-level x-mjst instanceOf (e.g. a schema that is itself a Date)
|
|
382
|
-
const instanceOf = getMjstInstanceOf(schema)
|
|
383
|
-
if (instanceOf) {
|
|
384
|
-
return [
|
|
385
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
386
|
-
` if (!(input instanceof ${instanceOf})) {`,
|
|
387
|
-
` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
|
|
388
|
-
` }`,
|
|
389
|
-
` return true`,
|
|
390
|
-
`}`,
|
|
391
|
-
].join('\n')
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Top-level x-mjst primitive (e.g. a schema that is itself a bigint)
|
|
395
|
-
const primitive = getMjstPrimitive(schema)
|
|
396
|
-
if (primitive) {
|
|
397
|
-
return [
|
|
398
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
399
|
-
` if (typeof input !== "${primitive}") {`,
|
|
400
|
-
` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
|
|
401
|
-
` }`,
|
|
402
|
-
` return true`,
|
|
403
|
-
`}`,
|
|
404
|
-
].join('\n')
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Top-level const
|
|
408
|
-
if (hasConst(schema)) {
|
|
409
|
-
const mismatch = constMismatchCondition('input', schema.const)
|
|
410
|
-
const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`)
|
|
411
|
-
return [
|
|
412
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
413
|
-
` if (${mismatch}) {`,
|
|
414
|
-
` return { valid: false, errors: [{ message: ${msg}, path: _path }] }`,
|
|
415
|
-
` }`,
|
|
416
|
-
` return true`,
|
|
417
|
-
`}`,
|
|
418
|
-
].join('\n')
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Top-level enum
|
|
422
|
-
if (hasEnum(schema)) {
|
|
423
|
-
const allowed = JSON.stringify(schema.enum)
|
|
424
|
-
const label = (schema.enum as unknown[]).map((v) => JSON.stringify(v)).join(', ')
|
|
425
|
-
return [
|
|
426
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
427
|
-
` if (!(${allowed} as unknown[]).includes(input)) {`,
|
|
428
|
-
` return { valid: false, errors: [{ message: \`must be one of: ${label}\`, path: _path }] }`,
|
|
429
|
-
` }`,
|
|
430
|
-
` return true`,
|
|
431
|
-
`}`,
|
|
432
|
-
].join('\n')
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// oneOf — try each branch, return errors from all if none match
|
|
436
|
-
if (hasOneOf(schema)) {
|
|
437
|
-
const branches = schema.oneOf
|
|
438
|
-
.map((branch, i) => {
|
|
439
|
-
if (!hasRef(branch)) return null
|
|
440
|
-
const bName = validatorName(refToName((branch as { $ref: string }).$ref, suffix))
|
|
441
|
-
return ` const _r${i} = ${bName}(input, _path)\n if (_r${i} === true) return true`
|
|
442
|
-
})
|
|
443
|
-
.filter(Boolean)
|
|
444
|
-
.join('\n')
|
|
445
|
-
|
|
446
|
-
return [
|
|
447
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
448
|
-
branches,
|
|
449
|
-
` return { valid: false, errors: [{ message: 'must match one of the expected schemas', path: _path }] }`,
|
|
450
|
-
`}`,
|
|
451
|
-
].join('\n')
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Top-level typed schema (string, number, boolean, array)
|
|
455
|
-
if (hasType(schema)) {
|
|
456
|
-
const t = schema.type as string
|
|
457
|
-
const wrongType = wrongTypeCondition('input', t)
|
|
458
|
-
const typLabel = typeofString(t)
|
|
459
|
-
|
|
460
|
-
const constraintLines: string[] = []
|
|
461
|
-
|
|
462
|
-
if (t === 'string') {
|
|
463
|
-
if (hasPattern(schema)) {
|
|
464
|
-
const re = escapeRegexPattern(schema.pattern)
|
|
465
|
-
const msg = JSON.stringify(`must match pattern ${schema.pattern}`)
|
|
466
|
-
constraintLines.push(` if (typeof input === 'string' && !/${re}/.test(input)) {`)
|
|
467
|
-
constraintLines.push(` errors.push({ message: ${msg}, path: _path })`)
|
|
468
|
-
constraintLines.push(` }`)
|
|
469
|
-
}
|
|
470
|
-
if (hasMinLength(schema)) {
|
|
471
|
-
constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`)
|
|
472
|
-
constraintLines.push(
|
|
473
|
-
` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`,
|
|
474
|
-
)
|
|
475
|
-
constraintLines.push(` }`)
|
|
476
|
-
}
|
|
477
|
-
if (hasMaxLength(schema)) {
|
|
478
|
-
constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`)
|
|
479
|
-
constraintLines.push(
|
|
480
|
-
` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`,
|
|
481
|
-
)
|
|
482
|
-
constraintLines.push(` }`)
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (!wrongType) {
|
|
487
|
-
return [
|
|
488
|
-
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
489
|
-
` return true`,
|
|
490
|
-
`}`,
|
|
491
|
-
].join('\n')
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (constraintLines.length === 0) {
|
|
495
|
-
return [
|
|
496
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
497
|
-
` if (${wrongType}) {`,
|
|
498
|
-
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
499
|
-
` }`,
|
|
500
|
-
` return true`,
|
|
501
|
-
`}`,
|
|
502
|
-
].join('\n')
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return [
|
|
506
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
507
|
-
` if (${wrongType}) {`,
|
|
508
|
-
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
509
|
-
` }`,
|
|
510
|
-
` const errors: ValidationError[] = []`,
|
|
511
|
-
constraintLines.join('\n'),
|
|
512
|
-
` return errors.length > 0 ? { valid: false, errors } : true`,
|
|
513
|
-
`}`,
|
|
514
|
-
].join('\n')
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
|
|
518
|
-
'\n',
|
|
519
|
-
)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Generates a TypeScript validator function from a JSON Schema.
|
|
524
|
-
*
|
|
525
|
-
* The generated function accepts `unknown` input and returns `true` if valid,
|
|
526
|
-
* or `{ valid: false, errors }` with a list of errors if not.
|
|
527
|
-
*
|
|
528
|
-
* Object schemas check that required properties are present and that all
|
|
529
|
-
* provided properties match their declared types. Non-object schemas (strings,
|
|
530
|
-
* numbers, enums, $refs) emit an inline type check.
|
|
531
|
-
*
|
|
532
|
-
* @example
|
|
533
|
-
* ```typescript
|
|
534
|
-
* generateValidatorFunction({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
|
|
535
|
-
* // export const validateInfo = (input: unknown, _path = ''): ValidationResult => {
|
|
536
|
-
* // if (typeof input !== 'object' || ...) return { valid: false, ... }
|
|
537
|
-
* // const errors: ValidationError[] = []
|
|
538
|
-
* // const obj = input as Record<string, unknown>
|
|
539
|
-
* // if (!('name' in obj)) { errors.push(...) } else if (typeof obj['name'] !== 'string') { errors.push(...) }
|
|
540
|
-
* // return errors.length > 0 ? { valid: false, errors } : true
|
|
541
|
-
* // }
|
|
542
|
-
* ```
|
|
543
|
-
*/
|
|
544
|
-
export const generateValidatorFunction = (schema: JSONSchema, typeName: string, suffix = ''): string => {
|
|
545
|
-
if (isObjectSchema(schema)) {
|
|
546
|
-
return generateObjectValidator(schema, typeName, suffix)
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return generateScalarValidator(schema, typeName, suffix)
|
|
550
|
-
}
|
package/src/index.ts
DELETED