@amritk/generate-validators 0.10.1 → 0.11.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.
|
@@ -2,7 +2,7 @@ import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
|
2
2
|
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
|
|
3
3
|
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
4
4
|
import { safeAccessor } from '@amritk/helpers/safe-accessor';
|
|
5
|
-
import { hasAdditionalProperties, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaximum, hasMaxLength, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
5
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
6
6
|
import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
|
|
7
7
|
/**
|
|
8
8
|
* Derives the validator function name from a type name.
|
|
@@ -38,12 +38,15 @@ const wrongTypeCondition = (accessor, type) => {
|
|
|
38
38
|
case 'string':
|
|
39
39
|
return `typeof ${accessor} !== 'string'`;
|
|
40
40
|
case 'number':
|
|
41
|
-
case 'integer':
|
|
42
41
|
return `typeof ${accessor} !== 'number'`;
|
|
42
|
+
case 'integer':
|
|
43
|
+
return `typeof ${accessor} !== 'number' || !Number.isInteger(${accessor})`;
|
|
43
44
|
case 'boolean':
|
|
44
45
|
return `typeof ${accessor} !== 'boolean'`;
|
|
45
46
|
case 'array':
|
|
46
47
|
return `!Array.isArray(${accessor})`;
|
|
48
|
+
case 'null':
|
|
49
|
+
return `${accessor} !== null`;
|
|
47
50
|
case 'object':
|
|
48
51
|
return `typeof ${accessor} !== 'object' || ${accessor} === null || Array.isArray(${accessor})`;
|
|
49
52
|
default:
|
|
@@ -101,6 +104,28 @@ const generateStrictKeyChecks = (schema, ctx) => {
|
|
|
101
104
|
` }`,
|
|
102
105
|
];
|
|
103
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Emits presence checks for `required` keys that have no `properties` entry.
|
|
109
|
+
* Keys present in `properties` get their missing-property check from
|
|
110
|
+
* {@link generatePropertyChecks}; a required key with no schema of its own would
|
|
111
|
+
* otherwise go unchecked, so its presence is enforced here to match the
|
|
112
|
+
* interpreter and Ajv.
|
|
113
|
+
*/
|
|
114
|
+
const generateMissingRequiredChecks = (schema, ctx) => {
|
|
115
|
+
if (!isSchemaObject(schema) || !hasRequired(schema))
|
|
116
|
+
return [];
|
|
117
|
+
const props = hasProperties(schema) ? schema.properties : {};
|
|
118
|
+
const parentPath = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
119
|
+
const lines = [];
|
|
120
|
+
for (const key of schema.required) {
|
|
121
|
+
if (Object.hasOwn(props, key))
|
|
122
|
+
continue;
|
|
123
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
124
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
125
|
+
lines.push(` }`);
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
};
|
|
104
129
|
/**
|
|
105
130
|
* Generates validation lines for a single property in an object schema.
|
|
106
131
|
* Handles $ref delegation, enum checks, type checks, string/number constraints,
|
|
@@ -225,92 +250,391 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
225
250
|
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
226
251
|
lines.push(` }`);
|
|
227
252
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
253
|
+
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
254
|
+
}
|
|
255
|
+
// Keywords that can sit alongside or instead of `type`: combinators
|
|
256
|
+
// (`allOf`/`anyOf`/`oneOf`/`not`/`if`) for any schema, plus the constraint
|
|
257
|
+
// checks for a *type-less* schema (e.g. a bare `{ required: [...] }` or
|
|
258
|
+
// `{ minItems: 2 }` property). A typed schema already ran its constraints in
|
|
259
|
+
// the `hasType` branch above, so it only needs the combinators here.
|
|
260
|
+
const extraLines = hasType(propSchema)
|
|
261
|
+
? generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
|
|
262
|
+
: [
|
|
263
|
+
...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
|
|
264
|
+
...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx),
|
|
265
|
+
];
|
|
266
|
+
if (extraLines.length > 0) {
|
|
267
|
+
if (!hasType(propSchema) && isRequired) {
|
|
268
|
+
// No `type` to anchor a missing-property check, so enforce presence here.
|
|
269
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
270
|
+
lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
|
|
271
|
+
lines.push(` } else {`);
|
|
272
|
+
lines.push(...extraLines);
|
|
273
|
+
lines.push(` }`);
|
|
247
274
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const strict = hasStrictExclusiveMinimum(propSchema);
|
|
253
|
-
const op = strict ? '<=' : '<';
|
|
254
|
-
const rel = strict ? '>' : '>=';
|
|
255
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.minimum}) {`);
|
|
256
|
-
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
|
|
257
|
-
lines.push(` }`);
|
|
258
|
-
}
|
|
259
|
-
if (hasMaximum(propSchema)) {
|
|
260
|
-
const strict = hasStrictExclusiveMaximum(propSchema);
|
|
261
|
-
const op = strict ? '>=' : '>';
|
|
262
|
-
const rel = strict ? '<' : '<=';
|
|
263
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.maximum}) {`);
|
|
264
|
-
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
|
|
265
|
-
lines.push(` }`);
|
|
266
|
-
}
|
|
267
|
-
if (hasExclusiveMinimum(propSchema)) {
|
|
268
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`);
|
|
269
|
-
lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
|
|
270
|
-
lines.push(` }`);
|
|
271
|
-
}
|
|
272
|
-
if (hasExclusiveMaximum(propSchema)) {
|
|
273
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`);
|
|
274
|
-
lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
|
|
275
|
-
lines.push(` }`);
|
|
276
|
-
}
|
|
277
|
-
if (hasMultipleOf(propSchema)) {
|
|
278
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} % ${propSchema.multipleOf} !== 0) {`);
|
|
279
|
-
lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
|
|
280
|
-
lines.push(` }`);
|
|
281
|
-
}
|
|
275
|
+
else {
|
|
276
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
277
|
+
lines.push(...extraLines);
|
|
278
|
+
lines.push(` }`);
|
|
282
279
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
}
|
|
281
|
+
return lines;
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Emits the value-shape constraints for a typed value: string (pattern,
|
|
285
|
+
* min/maxLength), number/integer (bounds, multipleOf), typed/`$ref` array items,
|
|
286
|
+
* and recursion into an inline nested object. Shared by the named-property path
|
|
287
|
+
* ({@link generatePropertyChecks}) and the dynamic-key path
|
|
288
|
+
* ({@link generateValueChecks}) so both enforce identical rules. `raw` and `path`
|
|
289
|
+
* are arbitrary expressions, so the same logic serves a static `obj.key` and a
|
|
290
|
+
* `patternProperties` / `additionalProperties` value read at a runtime key.
|
|
291
|
+
*/
|
|
292
|
+
const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
293
|
+
if (!isSchemaObject(propSchema))
|
|
294
|
+
return [];
|
|
295
|
+
const sp = propSchema;
|
|
296
|
+
const lines = [];
|
|
297
|
+
// Each block is gated on the *presence of its keywords*, not a declared `type`,
|
|
298
|
+
// and every emitted check carries its own runtime-type guard (`typeof` /
|
|
299
|
+
// `Array.isArray`). So a type-less schema (e.g. an `allOf` / `anyOf` / `not`
|
|
300
|
+
// branch that is just `{ required: [...] }` or `{ minItems: 2 }`) is validated
|
|
301
|
+
// against the value's runtime type, matching the interpreter.
|
|
302
|
+
// String constraints
|
|
303
|
+
if (hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema)) {
|
|
304
|
+
if (hasPattern(propSchema)) {
|
|
305
|
+
const re = escapeRegexPattern(propSchema.pattern);
|
|
306
|
+
const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`);
|
|
307
|
+
lines.push(` if (typeof ${raw} === 'string' && !/${re}/.test(${raw})) {`);
|
|
308
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
309
|
+
lines.push(` }`);
|
|
310
|
+
}
|
|
311
|
+
if (hasMinLength(propSchema)) {
|
|
312
|
+
lines.push(` if (typeof ${raw} === 'string' && ${raw}.length < ${propSchema.minLength}) {`);
|
|
313
|
+
lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
|
|
314
|
+
lines.push(` }`);
|
|
315
|
+
}
|
|
316
|
+
if (hasMaxLength(propSchema)) {
|
|
317
|
+
lines.push(` if (typeof ${raw} === 'string' && ${raw}.length > ${propSchema.maxLength}) {`);
|
|
318
|
+
lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
|
|
319
|
+
lines.push(` }`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Number constraints
|
|
323
|
+
if (hasMinimum(propSchema) ||
|
|
324
|
+
hasMaximum(propSchema) ||
|
|
325
|
+
hasExclusiveMinimum(propSchema) ||
|
|
326
|
+
hasExclusiveMaximum(propSchema) ||
|
|
327
|
+
hasMultipleOf(propSchema)) {
|
|
328
|
+
if (hasMinimum(propSchema)) {
|
|
329
|
+
// Draft-04 `exclusiveMinimum: true` makes the paired `minimum` strict.
|
|
330
|
+
const strict = hasStrictExclusiveMinimum(propSchema);
|
|
331
|
+
const op = strict ? '<=' : '<';
|
|
332
|
+
const rel = strict ? '>' : '>=';
|
|
333
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.minimum}) {`);
|
|
334
|
+
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
|
|
335
|
+
lines.push(` }`);
|
|
336
|
+
}
|
|
337
|
+
if (hasMaximum(propSchema)) {
|
|
338
|
+
const strict = hasStrictExclusiveMaximum(propSchema);
|
|
339
|
+
const op = strict ? '>=' : '>';
|
|
340
|
+
const rel = strict ? '<' : '<=';
|
|
341
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.maximum}) {`);
|
|
342
|
+
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
|
|
343
|
+
lines.push(` }`);
|
|
344
|
+
}
|
|
345
|
+
if (hasExclusiveMinimum(propSchema)) {
|
|
346
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`);
|
|
347
|
+
lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
|
|
348
|
+
lines.push(` }`);
|
|
349
|
+
}
|
|
350
|
+
if (hasExclusiveMaximum(propSchema)) {
|
|
351
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`);
|
|
352
|
+
lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
|
|
353
|
+
lines.push(` }`);
|
|
354
|
+
}
|
|
355
|
+
if (hasMultipleOf(propSchema)) {
|
|
356
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} % ${propSchema.multipleOf} !== 0) {`);
|
|
357
|
+
lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
|
|
358
|
+
lines.push(` }`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Array with typed items
|
|
362
|
+
if (hasItems(propSchema)) {
|
|
363
|
+
const itemSchema = propSchema.items;
|
|
364
|
+
if (hasRef(itemSchema)) {
|
|
365
|
+
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
366
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
367
|
+
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
|
|
368
|
+
lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/\${_i}\`)`);
|
|
369
|
+
lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
|
|
370
|
+
lines.push(` }`);
|
|
371
|
+
lines.push(` }`);
|
|
372
|
+
}
|
|
373
|
+
else if (hasType(itemSchema)) {
|
|
374
|
+
const itemType = itemSchema.type;
|
|
375
|
+
const itemWrong = wrongTypeCondition('_item', itemType);
|
|
376
|
+
const itemLabel = typeofString(itemType);
|
|
377
|
+
if (itemWrong) {
|
|
288
378
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
289
379
|
lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
|
|
290
|
-
lines.push(` const
|
|
291
|
-
lines.push(` if (
|
|
380
|
+
lines.push(` const _item = ${raw}[_i]`);
|
|
381
|
+
lines.push(` if (${itemWrong}) errors.push({ message: 'items must be ${itemLabel}', path: \`${path.slice(1, -1)}/\${_i}\` })`);
|
|
292
382
|
lines.push(` }`);
|
|
293
383
|
lines.push(` }`);
|
|
294
384
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Array length / uniqueness. `uniqueItems` dedupes by a JSON projection — exact
|
|
388
|
+
// for primitives (what the type guard also uses); deep-but-key-ordered for
|
|
389
|
+
// objects, the same pragmatic trade-off the rest of the generator makes.
|
|
390
|
+
if (hasMinItems(propSchema) ||
|
|
391
|
+
hasMaxItems(propSchema) ||
|
|
392
|
+
(hasUniqueItems(propSchema) && propSchema.uniqueItems === true) ||
|
|
393
|
+
isSchemaObject(sp['contains']) ||
|
|
394
|
+
Array.isArray(sp['prefixItems'])) {
|
|
395
|
+
if (hasMinItems(propSchema)) {
|
|
396
|
+
lines.push(` if (Array.isArray(${raw}) && ${raw}.length < ${propSchema.minItems}) {`);
|
|
397
|
+
lines.push(` errors.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
|
|
398
|
+
lines.push(` }`);
|
|
399
|
+
}
|
|
400
|
+
if (hasMaxItems(propSchema)) {
|
|
401
|
+
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > ${propSchema.maxItems}) {`);
|
|
402
|
+
lines.push(` errors.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
|
|
403
|
+
lines.push(` }`);
|
|
404
|
+
}
|
|
405
|
+
if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
|
|
406
|
+
lines.push(` if (Array.isArray(${raw}) && new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length) {`);
|
|
407
|
+
lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
|
|
408
|
+
lines.push(` }`);
|
|
409
|
+
}
|
|
410
|
+
// `contains` — at least `minContains` (default 1) and at most `maxContains`
|
|
411
|
+
// items must match the subschema. `minContains: 0` makes any array (even
|
|
412
|
+
// empty) satisfy the lower bound.
|
|
413
|
+
if (isSchemaObject(sp['contains'])) {
|
|
414
|
+
const min = typeof sp['minContains'] === 'number' ? sp['minContains'] : 1;
|
|
415
|
+
const max = typeof sp['maxContains'] === 'number' ? sp['maxContains'] : undefined;
|
|
416
|
+
const matchExpr = generateMatchesExpr('_c', sp['contains'], suffix, ctx);
|
|
417
|
+
const bound = max !== undefined ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
|
|
418
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
419
|
+
lines.push(` const _cn = (${raw} as unknown[]).filter((_c) => ${matchExpr}).length`);
|
|
420
|
+
lines.push(` if (${bound}) {`);
|
|
421
|
+
lines.push(` errors.push({ message: 'array does not contain the required matching items', path: ${path} })`);
|
|
422
|
+
lines.push(` }`);
|
|
423
|
+
lines.push(` }`);
|
|
424
|
+
}
|
|
425
|
+
// Tuple `prefixItems` — each position validated against its own subschema; a
|
|
426
|
+
// sibling `items: false` (or draft `additionalItems: false`) caps the length.
|
|
427
|
+
const prefix = sp['prefixItems'];
|
|
428
|
+
if (Array.isArray(prefix)) {
|
|
429
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
430
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
431
|
+
const itemChecks = generateValueChecks('', `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, prefix[i], suffix, ctx);
|
|
432
|
+
if (itemChecks.length > 0) {
|
|
433
|
+
lines.push(` if (${raw}.length > ${i}) {`);
|
|
434
|
+
lines.push(...itemChecks.map((l) => ` ${l}`));
|
|
304
435
|
lines.push(` }`);
|
|
305
|
-
lines.push(` }`);
|
|
306
436
|
}
|
|
307
437
|
}
|
|
438
|
+
if (sp['items'] === false || sp['additionalItems'] === false) {
|
|
439
|
+
lines.push(` if (${raw}.length > ${prefix.length}) {`);
|
|
440
|
+
lines.push(` errors.push({ message: 'must NOT have more than ${prefix.length} items', path: ${path} })`);
|
|
441
|
+
lines.push(` }`);
|
|
442
|
+
}
|
|
443
|
+
lines.push(` }`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Inline nested object — recurse so the nested fields are actually validated.
|
|
447
|
+
// Unconditional: `generateInlineObjectChecks` self-gates (returns `[]` when the
|
|
448
|
+
// schema has no object keywords) and each check is guarded by an `isObject`
|
|
449
|
+
// runtime check, so this is a no-op for non-object schemas.
|
|
450
|
+
lines.push(...generateInlineObjectChecks(key, propSchema, raw, suffix, ctx));
|
|
451
|
+
return lines;
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* Validates a value located at a *dynamic* key (a `patternProperties` or
|
|
455
|
+
* `additionalProperties` schema value) against `propSchema`. Mirrors the
|
|
456
|
+
* optional-property branch of {@link generatePropertyChecks} — the value is
|
|
457
|
+
* always present, so each check is the same `!== undefined`-guarded form — but
|
|
458
|
+
* `raw` and `path` are caller-supplied expressions (e.g. `obj[_k]` and
|
|
459
|
+
* `` `${_path}/${_k}` ``) so the checks read a runtime key.
|
|
460
|
+
*/
|
|
461
|
+
const generateValueChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
462
|
+
if (!isSchemaObject(propSchema))
|
|
463
|
+
return [];
|
|
464
|
+
const lines = [];
|
|
465
|
+
if (hasRef(propSchema)) {
|
|
466
|
+
const vName = validatorName(refToName(propSchema.$ref, suffix));
|
|
467
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
468
|
+
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
469
|
+
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
470
|
+
lines.push(` }`);
|
|
471
|
+
return lines;
|
|
472
|
+
}
|
|
473
|
+
const instanceOf = getMjstInstanceOf(propSchema);
|
|
474
|
+
if (instanceOf) {
|
|
475
|
+
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
|
|
476
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
477
|
+
lines.push(` }`);
|
|
478
|
+
return lines;
|
|
479
|
+
}
|
|
480
|
+
const primitive = getMjstPrimitive(propSchema);
|
|
481
|
+
if (primitive) {
|
|
482
|
+
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
|
|
483
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
484
|
+
lines.push(` }`);
|
|
485
|
+
return lines;
|
|
486
|
+
}
|
|
487
|
+
if (hasConst(propSchema)) {
|
|
488
|
+
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
489
|
+
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
490
|
+
lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
|
|
491
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
492
|
+
lines.push(` }`);
|
|
493
|
+
return lines;
|
|
494
|
+
}
|
|
495
|
+
if (hasEnum(propSchema)) {
|
|
496
|
+
const allowed = JSON.stringify(propSchema.enum);
|
|
497
|
+
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
498
|
+
lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
|
|
499
|
+
lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`);
|
|
500
|
+
lines.push(` }`);
|
|
501
|
+
return lines;
|
|
502
|
+
}
|
|
503
|
+
if (hasType(propSchema)) {
|
|
504
|
+
const t = propSchema.type;
|
|
505
|
+
const wrongType = wrongTypeCondition(raw, t);
|
|
506
|
+
const typLabel = typeofString(t);
|
|
507
|
+
if (wrongType) {
|
|
508
|
+
lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
|
|
509
|
+
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
510
|
+
lines.push(` }`);
|
|
308
511
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
512
|
+
}
|
|
513
|
+
// Constraint and combinator checks run regardless of a declared `type`: they
|
|
514
|
+
// gate on keyword presence + a runtime-type guard, so a type-less subschema
|
|
515
|
+
// (a combinator branch like `{ required: [...] }` or `{ minItems: 2 }`) is
|
|
516
|
+
// still validated rather than collapsing to "matches everything".
|
|
517
|
+
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
518
|
+
lines.push(...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx));
|
|
519
|
+
return lines;
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* A boolean expression that is `true` when `raw` matches `sub`. Reuses the value
|
|
523
|
+
* checks but collects their errors into a throwaway local buffer, so the same
|
|
524
|
+
* logic that produces error messages also answers the yes/no question the
|
|
525
|
+
* combinators (`anyOf`/`oneOf`/`not`/`if`) and `contains` need.
|
|
526
|
+
*/
|
|
527
|
+
const generateMatchesExpr = (raw, sub, suffix, ctx) => {
|
|
528
|
+
if (sub === true)
|
|
529
|
+
return 'true';
|
|
530
|
+
if (sub === false)
|
|
531
|
+
return 'false';
|
|
532
|
+
if (!isSchemaObject(sub))
|
|
533
|
+
return 'true';
|
|
534
|
+
const checks = generateValueChecks('', raw, '`${_path}`', sub, suffix, ctx);
|
|
535
|
+
if (checks.length === 0)
|
|
536
|
+
return 'true';
|
|
537
|
+
// The checks push to `errors`; redirect them to the IIFE-local `_m`. The outer
|
|
538
|
+
// validator's `errors.push` → `(errors ??= [])` rewrite never sees these (they
|
|
539
|
+
// are already `_m.push`), and nested match IIFEs each shadow their own `_m`.
|
|
540
|
+
const body = checks.join('\n').replaceAll('errors.push(', '_m.push(');
|
|
541
|
+
return `((): boolean => { const _m: ValidationError[] = []\n${body}\n return _m.length === 0 })()`;
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* Emits the combinator keywords (`allOf`, `anyOf`, `oneOf`, `not`,
|
|
545
|
+
* `if`/`then`/`else`). `allOf` surfaces each branch's errors directly; the others
|
|
546
|
+
* evaluate branch membership as a boolean via {@link generateMatchesExpr}.
|
|
547
|
+
*/
|
|
548
|
+
const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
549
|
+
if (!isSchemaObject(schema))
|
|
550
|
+
return [];
|
|
551
|
+
const lines = [];
|
|
552
|
+
if (hasAllOf(schema)) {
|
|
553
|
+
for (const branch of schema.allOf)
|
|
554
|
+
lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx));
|
|
555
|
+
}
|
|
556
|
+
if (hasAnyOf(schema) && schema.anyOf.length > 0) {
|
|
557
|
+
const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
|
|
558
|
+
lines.push(` if (!(${conds.join(' || ')})) {`);
|
|
559
|
+
lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
|
|
560
|
+
lines.push(` }`);
|
|
561
|
+
}
|
|
562
|
+
if (hasOneOf(schema) && schema.oneOf.length > 0) {
|
|
563
|
+
const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
|
|
564
|
+
lines.push(` if ((${conds.join(' + ')}) !== 1) {`);
|
|
565
|
+
lines.push(` errors.push({ message: 'must match exactly one schema in oneOf', path: ${path} })`);
|
|
566
|
+
lines.push(` }`);
|
|
567
|
+
}
|
|
568
|
+
const not = schema['not'];
|
|
569
|
+
if (not !== undefined && (isSchemaObject(not) || typeof not === 'boolean')) {
|
|
570
|
+
const cond = generateMatchesExpr(raw, not, suffix, ctx);
|
|
571
|
+
lines.push(` if (${cond}) {`);
|
|
572
|
+
lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
|
|
573
|
+
lines.push(` }`);
|
|
574
|
+
}
|
|
575
|
+
const ifSchema = schema['if'];
|
|
576
|
+
if (ifSchema !== undefined && (isSchemaObject(ifSchema) || typeof ifSchema === 'boolean')) {
|
|
577
|
+
const thenSchema = schema['then'];
|
|
578
|
+
const elseSchema = schema['else'];
|
|
579
|
+
const thenLines = thenSchema !== undefined ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
|
|
580
|
+
const elseLines = elseSchema !== undefined ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
|
|
581
|
+
if (thenLines.length > 0 || elseLines.length > 0) {
|
|
582
|
+
lines.push(` if (${generateMatchesExpr(raw, ifSchema, suffix, ctx)}) {`);
|
|
583
|
+
lines.push(...thenLines);
|
|
584
|
+
lines.push(` } else {`);
|
|
585
|
+
lines.push(...elseLines);
|
|
586
|
+
lines.push(` }`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return lines;
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* Emits validation for `patternProperties` and a schema-form
|
|
593
|
+
* `additionalProperties` (the `false` form is handled by
|
|
594
|
+
* {@link generateStrictKeyChecks}). For each object key, every matching
|
|
595
|
+
* `patternProperties` subschema runs against the value; keys reached by neither
|
|
596
|
+
* `properties` nor any pattern fall through to `additionalProperties`. This
|
|
597
|
+
* mirrors the runtime interpreter, which validates these values rather than only
|
|
598
|
+
* gating extra keys.
|
|
599
|
+
*/
|
|
600
|
+
const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
|
|
601
|
+
if (!isSchemaObject(schema))
|
|
602
|
+
return [];
|
|
603
|
+
const obj = ctx.objVar;
|
|
604
|
+
const d = ctx.depth;
|
|
605
|
+
const lines = [];
|
|
606
|
+
const patternsRecord = 'patternProperties' in schema && typeof schema.patternProperties === 'object' && schema.patternProperties !== null
|
|
607
|
+
? schema.patternProperties
|
|
608
|
+
: {};
|
|
609
|
+
const patternEntries = Object.entries(patternsRecord);
|
|
610
|
+
for (const [pattern, sub] of patternEntries) {
|
|
611
|
+
const re = escapeRegexPattern(pattern);
|
|
612
|
+
const kv = `_pk${d}`;
|
|
613
|
+
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
|
|
614
|
+
if (valueChecks.length === 0)
|
|
615
|
+
continue;
|
|
616
|
+
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
617
|
+
lines.push(` if (/${re}/.test(${kv})) {`);
|
|
618
|
+
lines.push(...valueChecks.map((line) => ` ${line}`));
|
|
619
|
+
lines.push(` }`);
|
|
620
|
+
lines.push(` }`);
|
|
621
|
+
}
|
|
622
|
+
// Schema-form `additionalProperties` validates every key reached by neither a
|
|
623
|
+
// declared property nor any `patternProperties` regex.
|
|
624
|
+
if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
|
|
625
|
+
const additional = schema.additionalProperties;
|
|
626
|
+
const kv = `_ak${d}`;
|
|
627
|
+
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, additional, suffix, ctx);
|
|
628
|
+
if (valueChecks.length > 0) {
|
|
629
|
+
const known = Object.keys(hasProperties(schema) ? schema.properties : {});
|
|
630
|
+
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
631
|
+
if (known.length > 0)
|
|
632
|
+
lines.push(` if (${JSON.stringify(known)}.includes(${kv})) continue`);
|
|
633
|
+
for (const pattern of Object.keys(patternsRecord)) {
|
|
634
|
+
lines.push(` if (/${escapeRegexPattern(pattern)}/.test(${kv})) continue`);
|
|
635
|
+
}
|
|
636
|
+
lines.push(...valueChecks.map((line) => ` ${line}`));
|
|
637
|
+
lines.push(` }`);
|
|
314
638
|
}
|
|
315
639
|
}
|
|
316
640
|
return lines;
|
|
@@ -337,7 +661,13 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
337
661
|
for (const [childKey, childSchema] of Object.entries(properties)) {
|
|
338
662
|
innerLines.push(...generatePropertyChecks(childKey, childSchema, required.has(childKey), suffix, child));
|
|
339
663
|
}
|
|
664
|
+
innerLines.push(...generateMissingRequiredChecks(propSchema, child));
|
|
665
|
+
innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
|
|
340
666
|
innerLines.push(...generateStrictKeyChecks(propSchema, child));
|
|
667
|
+
innerLines.push(...generateDependentRequiredChecks(propSchema, child));
|
|
668
|
+
if (hasPropertyNames(propSchema) && isSchemaObject(propSchema.propertyNames)) {
|
|
669
|
+
innerLines.push(...generatePropertyNameChecks(propSchema.propertyNames, suffix, child));
|
|
670
|
+
}
|
|
341
671
|
if (innerLines.length === 0)
|
|
342
672
|
return [];
|
|
343
673
|
// The shape check for the property itself already ran (or the property is
|
|
@@ -355,10 +685,10 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
355
685
|
* validator). This keeps the generator in step with the interpreter, which runs
|
|
356
686
|
* the whole subschema against each key — not just the `pattern` form.
|
|
357
687
|
*/
|
|
358
|
-
const generatePropertyNameChecks = (nameSchema, suffix) => {
|
|
688
|
+
const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
|
|
359
689
|
if (!isSchemaObject(nameSchema))
|
|
360
690
|
return [];
|
|
361
|
-
const at =
|
|
691
|
+
const at = `\`${ctx.pathPrefix}/\${_name}\``;
|
|
362
692
|
const checks = [];
|
|
363
693
|
if (hasRef(nameSchema)) {
|
|
364
694
|
const vName = validatorName(refToName(nameSchema.$ref, suffix));
|
|
@@ -392,7 +722,30 @@ const generatePropertyNameChecks = (nameSchema, suffix) => {
|
|
|
392
722
|
}
|
|
393
723
|
if (checks.length === 0)
|
|
394
724
|
return [];
|
|
395
|
-
return [` for (const _name of Object.keys(
|
|
725
|
+
return [` for (const _name of Object.keys(${ctx.objVar})) {`, ...checks, ` }`];
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Emits `dependentRequired` checks: when a trigger key is present, each of its
|
|
729
|
+
* declared dependencies must be present too. Reads the object and reports at the
|
|
730
|
+
* current node via `ctx`, so it works at the root and inside nested objects.
|
|
731
|
+
*/
|
|
732
|
+
const generateDependentRequiredChecks = (schema, ctx) => {
|
|
733
|
+
if (!isSchemaObject(schema) || !hasDependentRequired(schema))
|
|
734
|
+
return [];
|
|
735
|
+
const obj = ctx.objVar;
|
|
736
|
+
const at = ctx.depth === 0 ? '_path' : `\`${ctx.pathPrefix}\``;
|
|
737
|
+
const lines = [];
|
|
738
|
+
for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
|
|
739
|
+
if (!Array.isArray(deps))
|
|
740
|
+
continue;
|
|
741
|
+
for (const dep of deps) {
|
|
742
|
+
const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
|
|
743
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) {`);
|
|
744
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
745
|
+
lines.push(` }`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return lines;
|
|
396
749
|
};
|
|
397
750
|
/**
|
|
398
751
|
* Builds the `&&` conditions that prove a single property is valid, or `null`
|
|
@@ -414,6 +767,10 @@ const guardPropConditions = (key, propSchema, objAcc) => {
|
|
|
414
767
|
hasEnum(propSchema) ||
|
|
415
768
|
hasConst(propSchema) ||
|
|
416
769
|
hasOneOf(propSchema) ||
|
|
770
|
+
hasAnyOf(propSchema) ||
|
|
771
|
+
hasAllOf(propSchema) ||
|
|
772
|
+
'not' in propSchema ||
|
|
773
|
+
'if' in propSchema ||
|
|
417
774
|
getMjstInstanceOf(propSchema) !== undefined ||
|
|
418
775
|
getMjstPrimitive(propSchema) !== undefined ||
|
|
419
776
|
hasPattern(propSchema) ||
|
|
@@ -432,13 +789,14 @@ const guardPropConditions = (key, propSchema, objAcc) => {
|
|
|
432
789
|
switch (propSchema.type) {
|
|
433
790
|
case 'string':
|
|
434
791
|
return [`typeof ${raw} === 'string'`];
|
|
435
|
-
// mjst treats `integer` like `number` (it never enforces integrality), so a
|
|
436
|
-
// `typeof === 'number'` guard matches the slow path's verdict exactly.
|
|
437
792
|
case 'number':
|
|
438
|
-
case 'integer':
|
|
439
793
|
return [`typeof ${raw} === 'number'`];
|
|
794
|
+
case 'integer':
|
|
795
|
+
return [`typeof ${raw} === 'number'`, `Number.isInteger(${raw})`];
|
|
440
796
|
case 'boolean':
|
|
441
797
|
return [`typeof ${raw} === 'boolean'`];
|
|
798
|
+
case 'null':
|
|
799
|
+
return [`${raw} === null`];
|
|
442
800
|
case 'object':
|
|
443
801
|
// Member access into the nested record is only reached after the shape
|
|
444
802
|
// check ahead of it in the `&&` chain, so the cast is always safe.
|
|
@@ -509,6 +867,12 @@ const guardObjectConditions = (schema, raw, objAcc) => {
|
|
|
509
867
|
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
510
868
|
const properties = hasProperties(schema) ? schema.properties : {};
|
|
511
869
|
const keys = Object.keys(properties);
|
|
870
|
+
// A required key with no `properties` entry has no cheap guard condition, so
|
|
871
|
+
// defer to the slow path (which checks its presence).
|
|
872
|
+
for (const key of required) {
|
|
873
|
+
if (!Object.hasOwn(properties, key))
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
512
876
|
// The object shape-check only needs `!Array.isArray` when no required field
|
|
513
877
|
// check would already reject an array (see `arrayRejectedByRequiredProp`).
|
|
514
878
|
const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? '' : ` && !Array.isArray(${raw})`;
|
|
@@ -549,37 +913,23 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
549
913
|
propertyLines.push(...checks);
|
|
550
914
|
}
|
|
551
915
|
}
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
propertyLines.push(` for (const _key of Object.keys(obj)) {`);
|
|
558
|
-
propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`);
|
|
559
|
-
propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`);
|
|
560
|
-
propertyLines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
561
|
-
propertyLines.push(` }`);
|
|
562
|
-
}
|
|
916
|
+
// Required keys with no `properties` entry still need a presence check.
|
|
917
|
+
propertyLines.push(...generateMissingRequiredChecks(schema, ctx));
|
|
918
|
+
// patternProperties values and a schema-form additionalProperties are
|
|
919
|
+
// validated here (the `false` form is handled by generateStrictKeyChecks).
|
|
920
|
+
propertyLines.push(...generatePatternAndAdditionalChecks(schema, suffix, ctx));
|
|
563
921
|
// additionalProperties: false rejects every key not declared in properties
|
|
564
922
|
propertyLines.push(...generateStrictKeyChecks(schema, ctx));
|
|
565
923
|
// dependentRequired — when a trigger property is present, its dependencies must be too.
|
|
566
|
-
|
|
567
|
-
for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
|
|
568
|
-
if (!Array.isArray(deps))
|
|
569
|
-
continue;
|
|
570
|
-
for (const dep of deps) {
|
|
571
|
-
const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
|
|
572
|
-
propertyLines.push(` if (${JSON.stringify(trigger)} in obj && !(${JSON.stringify(dep)} in obj)) {`);
|
|
573
|
-
propertyLines.push(` errors.push({ message: ${msg}, path: _path })`);
|
|
574
|
-
propertyLines.push(` }`);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
924
|
+
propertyLines.push(...generateDependentRequiredChecks(schema, ctx));
|
|
578
925
|
// propertyNames — every key (always a string) must satisfy the subschema. This
|
|
579
926
|
// mirrors the interpreter, which runs the full subschema against each key.
|
|
580
927
|
if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
|
|
581
|
-
propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix));
|
|
928
|
+
propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
|
|
582
929
|
}
|
|
930
|
+
// Combinators declared alongside the object's properties (e.g. an object with
|
|
931
|
+
// `allOf` refining it further) are validated against the object value itself.
|
|
932
|
+
propertyLines.push(...generateCombinatorChecks('', 'obj', '`${_path}`', schema, suffix, ctx));
|
|
583
933
|
// Lazily allocate the errors array so a valid input never builds one — the same
|
|
584
934
|
// allocation-free happy path the runtime interpreter uses. Each emitted
|
|
585
935
|
// `errors.push(...)` becomes a create-on-first-use push; nothing is allocated
|
|
@@ -654,12 +1004,15 @@ const rightTypeCondition = (accessor, type) => {
|
|
|
654
1004
|
case 'string':
|
|
655
1005
|
return `typeof ${accessor} === 'string'`;
|
|
656
1006
|
case 'number':
|
|
657
|
-
case 'integer':
|
|
658
1007
|
return `typeof ${accessor} === 'number'`;
|
|
1008
|
+
case 'integer':
|
|
1009
|
+
return `typeof ${accessor} === 'number' && Number.isInteger(${accessor})`;
|
|
659
1010
|
case 'boolean':
|
|
660
1011
|
return `typeof ${accessor} === 'boolean'`;
|
|
661
1012
|
case 'array':
|
|
662
1013
|
return `Array.isArray(${accessor})`;
|
|
1014
|
+
case 'null':
|
|
1015
|
+
return `${accessor} === null`;
|
|
663
1016
|
case 'object':
|
|
664
1017
|
return `typeof ${accessor} === 'object' && ${accessor} !== null && !Array.isArray(${accessor})`;
|
|
665
1018
|
default:
|
|
@@ -702,6 +1055,9 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
702
1055
|
'anyOf' in schema ||
|
|
703
1056
|
'allOf' in schema ||
|
|
704
1057
|
'not' in schema ||
|
|
1058
|
+
'if' in schema ||
|
|
1059
|
+
'contains' in schema ||
|
|
1060
|
+
'prefixItems' in schema ||
|
|
705
1061
|
getMjstInstanceOf(schema) !== undefined ||
|
|
706
1062
|
getMjstPrimitive(schema) !== undefined) {
|
|
707
1063
|
return null;
|
|
@@ -731,6 +1087,8 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
731
1087
|
case 'number':
|
|
732
1088
|
case 'integer': {
|
|
733
1089
|
const parts = [`typeof ${acc} === 'number'`];
|
|
1090
|
+
if (t === 'integer')
|
|
1091
|
+
parts.push(`Number.isInteger(${acc})`);
|
|
734
1092
|
if (hasMinimum(schema))
|
|
735
1093
|
parts.push(`!(${acc} ${hasStrictExclusiveMinimum(schema) ? '<=' : '<'} ${schema.minimum})`);
|
|
736
1094
|
if (hasMaximum(schema))
|
|
@@ -745,6 +1103,8 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
745
1103
|
}
|
|
746
1104
|
case 'boolean':
|
|
747
1105
|
return `typeof ${acc} === 'boolean'`;
|
|
1106
|
+
case 'null':
|
|
1107
|
+
return `${acc} === null`;
|
|
748
1108
|
case 'object': {
|
|
749
1109
|
const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
|
|
750
1110
|
return parts === null ? null : parts.join(' && ');
|
|
@@ -768,7 +1128,17 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
768
1128
|
* on sparse input — the guard must never accept what the slow path would reject.
|
|
769
1129
|
*/
|
|
770
1130
|
const booleanArrayExpr = (schema, acc) => {
|
|
771
|
-
const
|
|
1131
|
+
const parts = [`Array.isArray(${acc})`];
|
|
1132
|
+
// Length / uniqueness, mirroring the validator's checks exactly so the guard's
|
|
1133
|
+
// verdict matches the slow path's.
|
|
1134
|
+
if (hasMinItems(schema))
|
|
1135
|
+
parts.push(`${acc}.length >= ${schema.minItems}`);
|
|
1136
|
+
if (hasMaxItems(schema))
|
|
1137
|
+
parts.push(`${acc}.length <= ${schema.maxItems}`);
|
|
1138
|
+
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
1139
|
+
parts.push(`new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length`);
|
|
1140
|
+
}
|
|
1141
|
+
const base = parts.join(' && ');
|
|
772
1142
|
if (!hasItems(schema))
|
|
773
1143
|
return base;
|
|
774
1144
|
const items = schema.items;
|
|
@@ -942,20 +1312,18 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
942
1312
|
].join('\n');
|
|
943
1313
|
}
|
|
944
1314
|
// oneOf — try each branch, return errors from all if none match
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
})
|
|
953
|
-
.filter(Boolean)
|
|
954
|
-
.join('\n');
|
|
1315
|
+
// Top-level combinators (`allOf` / `anyOf` / `oneOf` / `not` / `if`), validated
|
|
1316
|
+
// against the input via the shared combinator generator — correct `oneOf`
|
|
1317
|
+
// (exactly one) and inline branches included, not just `$ref` branches.
|
|
1318
|
+
if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || 'not' in schema || 'if' in schema) {
|
|
1319
|
+
const ctx = createRootContext();
|
|
1320
|
+
const checks = generateCombinatorChecks('', 'input', '`${_path}`', schema, suffix, ctx);
|
|
1321
|
+
const body = checks.join('\n').replaceAll('errors.push(', '(errors ??= []).push(');
|
|
955
1322
|
return [
|
|
956
1323
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
957
|
-
|
|
958
|
-
|
|
1324
|
+
` let errors: ValidationError[] | undefined`,
|
|
1325
|
+
body,
|
|
1326
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
959
1327
|
`}`,
|
|
960
1328
|
].join('\n');
|
|
961
1329
|
}
|