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