@amritk/generate-parsers 0.12.2 → 0.13.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 +4 -14
- package/dist/generators/build-schema.js +32 -7
- package/dist/generators/generate-discriminated-union-validation.d.ts +9 -0
- package/dist/generators/generate-discriminated-union-validation.js +33 -22
- package/dist/generators/generate-enum-check.d.ts +8 -0
- package/dist/generators/generate-enum-check.js +14 -0
- package/dist/generators/generate-files.js +1 -0
- package/dist/generators/generate-parser-function.d.ts +6 -0
- package/dist/generators/generate-parser-function.js +167 -124
- package/dist/generators/generate-schema-checks.js +7 -2
- package/dist/generators/generate-strict-assertion.d.ts +16 -1
- package/dist/generators/generate-strict-assertion.js +111 -26
- package/dist/generators/generate-type-checks.d.ts +65 -0
- package/dist/generators/generate-type-checks.js +370 -0
- package/dist/generators/generate-validation-expression.js +61 -2
- package/dist/helpers/collect-helpers.js +3 -1
- package/dist/helpers/collect-imports.js +5 -1
- package/package.json +3 -3
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
2
2
|
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
3
|
+
import { resolveRef } from '@amritk/helpers/resolve-ref';
|
|
3
4
|
import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
|
|
4
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum,
|
|
5
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasOneOf, hasProperties, hasRef, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
5
6
|
import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
|
|
7
|
+
import { findDiscriminator } from '../helpers/find-discriminator.js';
|
|
6
8
|
import { getDefaultValue } from '../helpers/get-default-value.js';
|
|
9
|
+
import { getDiscriminatorValue } from '../helpers/get-discriminator-value.js';
|
|
7
10
|
import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
|
|
11
|
+
import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, shapeValidatorName, } from './generate-type-checks.js';
|
|
8
12
|
import { generateValidationExpression, scalarItemTypeCheck } from './generate-validation-expression.js';
|
|
9
13
|
/**
|
|
10
14
|
* Generates the parser function name from a type name.
|
|
@@ -41,7 +45,10 @@ const generateRequiredRefCall = (key, ref, suffix) => {
|
|
|
41
45
|
const generateOptionalRefCall = (key, ref, suffix) => {
|
|
42
46
|
const acc = safeAccessor('input', key);
|
|
43
47
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
44
|
-
|
|
48
|
+
// Gate on presence, not truthiness: a `&&` guard skips parsing when the value
|
|
49
|
+
// is `false`/`0`/`""`/`null`, spreading it raw. `!== undefined` still omits an
|
|
50
|
+
// absent optional property but coerces every present one, matching the main path.
|
|
51
|
+
return `...(${acc} !== undefined && { ${safeKey(key)}: ${parserName}(${acc}) })`;
|
|
45
52
|
};
|
|
46
53
|
/**
|
|
47
54
|
* Generates a validateArray call for required array properties with $ref items.
|
|
@@ -58,7 +65,8 @@ const generateRequiredArrayRefCall = (key, ref, suffix) => {
|
|
|
58
65
|
const generateOptionalArrayRefCall = (key, ref, suffix) => {
|
|
59
66
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
60
67
|
const acc = safeAccessor('input', key);
|
|
61
|
-
|
|
68
|
+
// Presence gating (not truthiness) so a falsy-but-present value is still parsed.
|
|
69
|
+
return `...(${acc} !== undefined && { ${safeKey(key)}: validateArray(${acc}, ${parserName}) })`;
|
|
62
70
|
};
|
|
63
71
|
/**
|
|
64
72
|
* Generates a validateRecord call for required object properties with additionalProperties $ref.
|
|
@@ -76,7 +84,8 @@ const generateRequiredRecordRefCall = (key, ref, suffix) => {
|
|
|
76
84
|
const generateOptionalRecordRefCall = (key, ref, suffix) => {
|
|
77
85
|
const acc = safeAccessor('input', key);
|
|
78
86
|
const parserName = generateParserName(refToName(ref, suffix));
|
|
79
|
-
|
|
87
|
+
// Presence gating (not truthiness) so a falsy-but-present value is still parsed.
|
|
88
|
+
return `...(${acc} !== undefined && { ${safeKey(key)}: validateRecord(${acc}, ${parserName}) })`;
|
|
80
89
|
};
|
|
81
90
|
/**
|
|
82
91
|
* Generates a spread entry for optional inline properties.
|
|
@@ -296,8 +305,99 @@ const scalarDefaultLiteral = (schema) => {
|
|
|
296
305
|
}
|
|
297
306
|
return '{}';
|
|
298
307
|
};
|
|
299
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Emits a dispatcher for a top-level `oneOf`/`anyOf` whose branches are `$ref`s,
|
|
310
|
+
* when the branches share a discriminator (a `const`/`enum` tag such as `kind`).
|
|
311
|
+
* Each resolved branch's discriminant selects its imported parser, so a recursive
|
|
312
|
+
* discriminated union (e.g. `Expr = Lit | BinOp`, where `BinOp` has `Expr`
|
|
313
|
+
* children) is actually validated and dispatched instead of blindly cast. Returns
|
|
314
|
+
* `null` when it can't be done (no root schema, no discriminator, a non-ref
|
|
315
|
+
* branch, or ref imports disabled), so the caller keeps its passthrough fallback.
|
|
316
|
+
*/
|
|
317
|
+
const generateRefUnionDispatch = (functionName, typeName, branches, strict, ctx) => {
|
|
318
|
+
const rootSchema = ctx.rootSchema;
|
|
319
|
+
if (!ctx.useRefImports || rootSchema === undefined)
|
|
320
|
+
return null;
|
|
321
|
+
if (branches.length === 0 || !branches.every((b) => isSchemaObject(b) && hasRef(b)))
|
|
322
|
+
return null;
|
|
323
|
+
// Resolve each branch ref so we can read its discriminant tag and derive its
|
|
324
|
+
// imported parser name.
|
|
325
|
+
const resolved = branches.map((b) => {
|
|
326
|
+
const ref = b.$ref;
|
|
327
|
+
return { parser: generateParserName(refToName(ref, ctx.suffix)), schema: resolveRef(ref, rootSchema) };
|
|
328
|
+
});
|
|
329
|
+
if (resolved.some((r) => !isSchemaObject(r.schema)))
|
|
330
|
+
return null;
|
|
331
|
+
const discriminator = findDiscriminator(resolved.map((r) => r.schema));
|
|
332
|
+
if (discriminator === null)
|
|
333
|
+
return null;
|
|
334
|
+
const cases = [];
|
|
335
|
+
for (const r of resolved) {
|
|
336
|
+
const value = getDiscriminatorValue(r.schema, discriminator);
|
|
337
|
+
if (value === null)
|
|
338
|
+
return null;
|
|
339
|
+
cases.push({ value, parser: r.parser });
|
|
340
|
+
}
|
|
341
|
+
// Read the discriminant safely: `null`/`undefined` is guarded (a property read
|
|
342
|
+
// would throw), and a non-object primitive yields `undefined` (no branch matches).
|
|
343
|
+
const access = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(discriminator)
|
|
344
|
+
? `.${discriminator}`
|
|
345
|
+
: `[${JSON.stringify(discriminator)}]`;
|
|
346
|
+
const discExpr = `input == null ? undefined : (input as Record<string, unknown>)${access}`;
|
|
347
|
+
// Fold branches into a nested ternary in declaration order:
|
|
348
|
+
// `_disc === "lit" ? parseLit(input) : _disc === "binop" ? parseBinop(input) : <fallback>`.
|
|
349
|
+
// Unmatched: strict throws (not a member of the union); non-strict coerces via the
|
|
350
|
+
// first branch's parser, preserving the coercing parser's lenient contract while
|
|
351
|
+
// still running real validation/coercion instead of a blind cast.
|
|
352
|
+
const fallback = strict
|
|
353
|
+
? `(() => { throw new Error(${JSON.stringify(`[${typeName}] value does not match any union branch`)}); })()`
|
|
354
|
+
: `${cases[0]?.parser}(input)`;
|
|
355
|
+
let expr = fallback;
|
|
356
|
+
for (let i = cases.length - 1; i >= 0; i--) {
|
|
357
|
+
const c = cases[i];
|
|
358
|
+
if (c)
|
|
359
|
+
expr = `_disc === ${JSON.stringify(c.value)} ? ${c.parser}(input) : ${expr}`;
|
|
360
|
+
}
|
|
361
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n const _disc = ${discExpr};\n return ${expr};\n};`;
|
|
362
|
+
};
|
|
363
|
+
const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
300
364
|
const functionName = generateParserName(typeName);
|
|
365
|
+
// A ref-branch discriminated union dispatches to its branch parsers in *both*
|
|
366
|
+
// modes (strict throws on an unmatched discriminant; non-strict coerces via the
|
|
367
|
+
// first branch). This runs before the `!strict` coercion block below so a strict
|
|
368
|
+
// recursive union is validated instead of blindly cast.
|
|
369
|
+
if (unionCtx && isSchemaObject(schema) && (hasOneOf(schema) || hasAnyOf(schema))) {
|
|
370
|
+
const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
|
|
371
|
+
if (branches.length > 0 && branches.every((b) => isSchemaObject(b) && hasRef(b))) {
|
|
372
|
+
const dispatch = generateRefUnionDispatch(functionName, typeName, branches, strict, unionCtx);
|
|
373
|
+
if (dispatch !== null)
|
|
374
|
+
return dispatch;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// An alias definition (a bare `$ref` with no shape of its own, e.g. a root
|
|
378
|
+
// schema that is just `$ref: '#/$defs/expr'`) delegates to the referenced
|
|
379
|
+
// parser instead of blindly casting, in both modes. Guarded against
|
|
380
|
+
// self-reference: delegating to ourselves would recurse forever.
|
|
381
|
+
if (unionCtx?.useRefImports && isSchemaObject(schema) && hasRef(schema) && !hasProperties(schema)) {
|
|
382
|
+
const refName = refToName(schema.$ref, unionCtx.suffix);
|
|
383
|
+
if (refName !== typeName) {
|
|
384
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Strict union enforcement for inline (or mixed) branches: membership is the
|
|
388
|
+
// disjunction of per-branch checks, and a non-member throws. Only emitted
|
|
389
|
+
// when every branch check is false-sound (see canEnforceUnion) — otherwise a
|
|
390
|
+
// conservative stub validator somewhere in the graph could reject valid
|
|
391
|
+
// input — and never when stripping unknown keys (see UnionParserContext).
|
|
392
|
+
if (strict && unionCtx && isSchemaObject(schema)) {
|
|
393
|
+
const branches = getUnionBranches(schema);
|
|
394
|
+
if (branches && !unionCtx.stripUnknown && canEnforceUnion(branches, unionCtx.rootSchema)) {
|
|
395
|
+
const check = generateUnionCheck('input', branches, unionCtx.useRefImports, unionCtx.suffix);
|
|
396
|
+
if (check !== null) {
|
|
397
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!(${check})) throw new Error(${JSON.stringify(`[${typeName}] value does not match any union branch`)});\n return input as ${typeName};\n};`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
301
401
|
// `const`/`enum` define the value space directly (the generated type is a
|
|
302
402
|
// literal / literal-union), so a non-member must coerce to a valid member —
|
|
303
403
|
// otherwise the returned value would not be of the declared type. This runs
|
|
@@ -322,10 +422,7 @@ const generateNonObjectParser = (typeName, schema, strict) => {
|
|
|
322
422
|
}
|
|
323
423
|
// A top-level union must validate membership: an unmatched value is not of
|
|
324
424
|
// the declared union type, so coerce it to a member-shaped default. Reuse the
|
|
325
|
-
// same union validation the property path uses.
|
|
326
|
-
// validated inline here (the membership test would silently drop it and
|
|
327
|
-
// wrongly coerce a valid ref-shaped value), so fall through to a passthrough
|
|
328
|
-
// cast rather than discarding valid input.
|
|
425
|
+
// same union validation the property path uses.
|
|
329
426
|
if (hasOneOf(schema) || hasAnyOf(schema)) {
|
|
330
427
|
const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
|
|
331
428
|
const hasRefBranch = branches.some((b) => isSchemaObject(b) && hasRef(b));
|
|
@@ -334,6 +431,9 @@ const generateNonObjectParser = (typeName, schema, strict) => {
|
|
|
334
431
|
const expr = generateValidationExpression('', schema, fallback, true, undefined, undefined, 'input', true);
|
|
335
432
|
return `export const ${functionName} = (input: unknown): ${typeName} => (${expr}) as ${typeName};`;
|
|
336
433
|
}
|
|
434
|
+
// Ref-branch union: a *discriminated* one was already dispatched above (both
|
|
435
|
+
// modes). Anything reaching here is non-discriminated, which can't be validated
|
|
436
|
+
// inline without risking dropping valid input — fall back to a passthrough cast.
|
|
337
437
|
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
|
|
338
438
|
}
|
|
339
439
|
}
|
|
@@ -356,8 +456,10 @@ const generateNonObjectParser = (typeName, schema, strict) => {
|
|
|
356
456
|
case 'string':
|
|
357
457
|
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "string" ? input as ${typeName} : "" as ${typeName};`;
|
|
358
458
|
case 'number':
|
|
359
|
-
case 'integer':
|
|
360
459
|
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" ? input as ${typeName} : 0 as ${typeName};`;
|
|
460
|
+
case 'integer':
|
|
461
|
+
// `integer` rejects non-integral numbers; a bare typeof would accept `1.5`.
|
|
462
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "number" && Number.isInteger(input) ? input as ${typeName} : 0 as ${typeName};`;
|
|
361
463
|
case 'boolean':
|
|
362
464
|
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
|
|
363
465
|
case 'array': {
|
|
@@ -393,12 +495,6 @@ const toVarName = (key) => {
|
|
|
393
495
|
const safe = key.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
394
496
|
return `_${safe}`;
|
|
395
497
|
};
|
|
396
|
-
/**
|
|
397
|
-
* Returns the shape-predicate function name for a given type name.
|
|
398
|
-
* The predicate is generated alongside each parser and tells callers
|
|
399
|
-
* whether `input` is already in the shape produced by the parser's fast path.
|
|
400
|
-
*/
|
|
401
|
-
const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
|
|
402
498
|
/**
|
|
403
499
|
* Matches an inline nested object property — an object schema written directly
|
|
404
500
|
* under `properties` with its own `properties`, rather than referenced via
|
|
@@ -466,107 +562,6 @@ const hasStrictKeys = (schema) => {
|
|
|
466
562
|
return false;
|
|
467
563
|
return true;
|
|
468
564
|
};
|
|
469
|
-
/**
|
|
470
|
-
* Generates a fast-path type check expression for a property.
|
|
471
|
-
* Returns null if the schema is too complex for a simple type check
|
|
472
|
-
* (e.g. has unions, enums that require more elaborate validation).
|
|
473
|
-
*
|
|
474
|
-
* When `useRefImports` is true, $ref properties (and arrays of $refs) are
|
|
475
|
-
* checked by calling the imported shape predicate, allowing parent parsers
|
|
476
|
-
* to fast-path through nested ref types without recursing into their parsers.
|
|
477
|
-
*/
|
|
478
|
-
const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
479
|
-
if (!isSchemaObject(schema))
|
|
480
|
-
return null;
|
|
481
|
-
// $ref via shape predicate (deep fast-path through nested types).
|
|
482
|
-
if (useRefImports && hasRef(schema)) {
|
|
483
|
-
const refName = refToName(schema.$ref, suffix);
|
|
484
|
-
return `${shapeValidatorName(refName)}(${varName})`;
|
|
485
|
-
}
|
|
486
|
-
// Skip fast path for complex schemas that need more elaborate validation
|
|
487
|
-
if (hasEnum(schema) ||
|
|
488
|
-
hasOneOf(schema) ||
|
|
489
|
-
hasAnyOf(schema) ||
|
|
490
|
-
hasAllOf(schema) ||
|
|
491
|
-
hasRef(schema) ||
|
|
492
|
-
'not' in schema) {
|
|
493
|
-
return null;
|
|
494
|
-
}
|
|
495
|
-
if (!hasType(schema))
|
|
496
|
-
return null;
|
|
497
|
-
// Array of $refs: each item must satisfy the imported shape predicate.
|
|
498
|
-
if (useRefImports &&
|
|
499
|
-
schema.type === 'array' &&
|
|
500
|
-
hasItems(schema) &&
|
|
501
|
-
isSchemaObject(schema.items) &&
|
|
502
|
-
hasRef(schema.items)) {
|
|
503
|
-
const refName = refToName(schema.items.$ref, suffix);
|
|
504
|
-
return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
|
|
505
|
-
}
|
|
506
|
-
const checks = [];
|
|
507
|
-
switch (schema.type) {
|
|
508
|
-
case 'string': {
|
|
509
|
-
checks.push(`typeof ${varName} === "string"`);
|
|
510
|
-
if (hasPattern(schema))
|
|
511
|
-
checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
|
|
512
|
-
if (hasMinLength(schema))
|
|
513
|
-
checks.push(`${varName}.length >= ${schema.minLength}`);
|
|
514
|
-
if (hasMaxLength(schema))
|
|
515
|
-
checks.push(`${varName}.length <= ${schema.maxLength}`);
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
case 'number':
|
|
519
|
-
case 'integer': {
|
|
520
|
-
checks.push(`typeof ${varName} === "number"`);
|
|
521
|
-
if (hasMinimum(schema))
|
|
522
|
-
checks.push(`${varName} >= ${schema.minimum}`);
|
|
523
|
-
if (hasMaximum(schema))
|
|
524
|
-
checks.push(`${varName} <= ${schema.maximum}`);
|
|
525
|
-
if (hasExclusiveMinimum(schema))
|
|
526
|
-
checks.push(`${varName} > ${schema.exclusiveMinimum}`);
|
|
527
|
-
if (hasExclusiveMaximum(schema))
|
|
528
|
-
checks.push(`${varName} < ${schema.exclusiveMaximum}`);
|
|
529
|
-
if (hasMultipleOf(schema))
|
|
530
|
-
checks.push(`${varName} % ${schema.multipleOf} === 0`);
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
case 'boolean':
|
|
534
|
-
checks.push(`typeof ${varName} === "boolean"`);
|
|
535
|
-
break;
|
|
536
|
-
case 'array': {
|
|
537
|
-
checks.push(`Array.isArray(${varName})`);
|
|
538
|
-
if (hasMinItems(schema))
|
|
539
|
-
checks.push(`${varName}.length >= ${schema.minItems}`);
|
|
540
|
-
if (hasMaxItems(schema))
|
|
541
|
-
checks.push(`${varName}.length <= ${schema.maxItems}`);
|
|
542
|
-
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
543
|
-
checks.push(`new Set(${varName}).size === ${varName}.length`);
|
|
544
|
-
}
|
|
545
|
-
// For a scalar item type, every element must already be well-typed to take
|
|
546
|
-
// the fast path; a mistyped element routes the array to the slow path where
|
|
547
|
-
// each element is coerced.
|
|
548
|
-
if (hasItems(schema) && !Array.isArray(schema.items)) {
|
|
549
|
-
const itemCheck = scalarItemTypeCheck(schema.items, '_it');
|
|
550
|
-
if (itemCheck)
|
|
551
|
-
checks.push(`${varName}.every((_it) => ${itemCheck})`);
|
|
552
|
-
}
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
case 'object':
|
|
556
|
-
checks.push(`isObject(${varName})`);
|
|
557
|
-
break;
|
|
558
|
-
default:
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
if (checks.length === 0)
|
|
562
|
-
return null;
|
|
563
|
-
// checks[0] is safe: we guard with checks.length === 0 above
|
|
564
|
-
let result = checks[0];
|
|
565
|
-
for (let i = 1; i < checks.length; i++) {
|
|
566
|
-
result += ' && ' + checks[i];
|
|
567
|
-
}
|
|
568
|
-
return result;
|
|
569
|
-
};
|
|
570
565
|
/**
|
|
571
566
|
* Determines if a property needs a local variable or can be inlined.
|
|
572
567
|
* Schema-object properties are always cached: the slow-path ternary chain
|
|
@@ -587,7 +582,7 @@ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
|
|
|
587
582
|
* - Fast path that returns input directly when all properties are already valid
|
|
588
583
|
* - Slow path with optimized expressions
|
|
589
584
|
*/
|
|
590
|
-
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false) => {
|
|
585
|
+
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema) => {
|
|
591
586
|
const functionName = generateParserName(typeName);
|
|
592
587
|
const exportPrefix = exported ? 'export ' : '';
|
|
593
588
|
if (!hasProperties(schema)) {
|
|
@@ -607,7 +602,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
607
602
|
const propSchema = schema.properties[key];
|
|
608
603
|
preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
|
|
609
604
|
preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown));
|
|
610
|
-
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown));
|
|
605
|
+
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema));
|
|
611
606
|
}
|
|
612
607
|
// additionalProperties: false — the per-key "is this undeclared" test inlines
|
|
613
608
|
// `!==` comparisons for a short key list and hoists a Set only for a long one
|
|
@@ -641,9 +636,17 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
641
636
|
schema.allOf.some((entry) => isSchemaObject(entry) && hasRef(entry));
|
|
642
637
|
let canFastPath = !hasAllOfRefParsers;
|
|
643
638
|
const fastPathChecks = [];
|
|
639
|
+
// `toVarName` collapses distinct keys that differ only in non-identifier chars
|
|
640
|
+
// (e.g. `a-b` and `a.b` both → `_a_b`), which would emit two `const _a_b`
|
|
641
|
+
// declarations (TS2451). Dedupe by suffixing `_` until unique — the same
|
|
642
|
+
// approach `collectInlineObjectProperties` uses — so each key gets its own var.
|
|
643
|
+
const usedVarNames = new Set();
|
|
644
644
|
for (const [key, propSchema] of properties) {
|
|
645
645
|
const isRequired = isPropertyRequired(key, schema);
|
|
646
|
-
|
|
646
|
+
let varName = toVarName(key);
|
|
647
|
+
while (usedVarNames.has(varName))
|
|
648
|
+
varName = `${varName}_`;
|
|
649
|
+
usedVarNames.add(varName);
|
|
647
650
|
propInfo.push({ key, varName, isRequired, propSchema });
|
|
648
651
|
if (!canFastPath)
|
|
649
652
|
continue;
|
|
@@ -897,7 +900,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
897
900
|
lines.push(...varDeclLines);
|
|
898
901
|
lines.push(...warnLines);
|
|
899
902
|
// The first assertion line is the `isObject` check, already done above.
|
|
900
|
-
const assertionLines = generateObjectStrictAssertion(schema, typeName
|
|
903
|
+
const assertionLines = generateObjectStrictAssertion(schema, typeName, {
|
|
904
|
+
useRefImports,
|
|
905
|
+
suffix,
|
|
906
|
+
stripUnknown,
|
|
907
|
+
...(rootSchema !== undefined ? { rootSchema } : {}),
|
|
908
|
+
}).slice(1);
|
|
901
909
|
if (shallowGuard) {
|
|
902
910
|
// stripUnknown: a well-typed input skips the assertions and goes straight
|
|
903
911
|
// to the strip build (which removes extras and recurses into sub-parsers).
|
|
@@ -1335,7 +1343,12 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1335
1343
|
(isSchemaObject(schema) && ('patternProperties' in schema || 'additionalProperties' in schema));
|
|
1336
1344
|
// Handle non-object schemas with type-appropriate validation
|
|
1337
1345
|
if (!isObjectLikeSchema && !isSchemaObject(schema)) {
|
|
1338
|
-
return generateNonObjectParser(typeName, schema, strict
|
|
1346
|
+
return generateNonObjectParser(typeName, schema, strict, {
|
|
1347
|
+
useRefImports,
|
|
1348
|
+
suffix,
|
|
1349
|
+
stripUnknown,
|
|
1350
|
+
...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
|
|
1351
|
+
});
|
|
1339
1352
|
}
|
|
1340
1353
|
// Handle schemas with both properties AND patternProperties.
|
|
1341
1354
|
// This generates a parser that handles known properties and also iterates
|
|
@@ -1348,7 +1361,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1348
1361
|
// properties AND conditional keywords use all declared properties rather than
|
|
1349
1362
|
// only the if/then fragment.
|
|
1350
1363
|
if (hasProperties(schema)) {
|
|
1351
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
|
|
1364
|
+
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema);
|
|
1352
1365
|
}
|
|
1353
1366
|
// Handle conditional schemas (if/then/else) for schemas without explicit properties.
|
|
1354
1367
|
if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
|
|
@@ -1362,7 +1375,12 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1362
1375
|
}
|
|
1363
1376
|
// Handle non-object schemas with type-appropriate validation (no properties, no conditionals)
|
|
1364
1377
|
if (!isObjectLikeSchema) {
|
|
1365
|
-
return generateNonObjectParser(typeName, schema, strict
|
|
1378
|
+
return generateNonObjectParser(typeName, schema, strict, {
|
|
1379
|
+
useRefImports,
|
|
1380
|
+
suffix,
|
|
1381
|
+
stripUnknown,
|
|
1382
|
+
...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
|
|
1383
|
+
});
|
|
1366
1384
|
}
|
|
1367
1385
|
// Handle schemas with patternProperties (but no properties)
|
|
1368
1386
|
if ('patternProperties' in schema) {
|
|
@@ -1421,6 +1439,31 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
|
|
|
1421
1439
|
const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
|
|
1422
1440
|
if (!isSchemaObject(schema))
|
|
1423
1441
|
return stub;
|
|
1442
|
+
// An alias definition (a bare `$ref` with no shape of its own) delegates to
|
|
1443
|
+
// the referenced type's validator. Guarded against self-reference.
|
|
1444
|
+
if (useRefImports && hasRef(schema) && !hasProperties(schema)) {
|
|
1445
|
+
const refName = refToName(schema.$ref, suffix);
|
|
1446
|
+
if (refName !== typeName) {
|
|
1447
|
+
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${shapeValidatorName(refName)}(input);`;
|
|
1448
|
+
}
|
|
1449
|
+
return stub;
|
|
1450
|
+
}
|
|
1451
|
+
// A pure union definition (file-level oneOf/anyOf without properties) gets a
|
|
1452
|
+
// real membership predicate when every branch is checkable. A recursive
|
|
1453
|
+
// union (e.g. `expr` whose branches $ref `expr` itself) works because the
|
|
1454
|
+
// branch checks call this very validator by name at runtime. Skipped under
|
|
1455
|
+
// stripUnknown: the branch checks carry no known-keys terms, so a `true`
|
|
1456
|
+
// could not guarantee the parser's strip build would be a no-op.
|
|
1457
|
+
if (!hasProperties(schema) && !('patternProperties' in schema) && !('if' in schema) && !stripUnknown) {
|
|
1458
|
+
const branches = getUnionBranches(schema);
|
|
1459
|
+
if (branches) {
|
|
1460
|
+
const check = generateUnionCheck('input', branches, useRefImports, suffix);
|
|
1461
|
+
if (check !== null) {
|
|
1462
|
+
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return stub;
|
|
1466
|
+
}
|
|
1424
1467
|
if (!hasProperties(schema))
|
|
1425
1468
|
return stub;
|
|
1426
1469
|
// Composition / conditional schemas can match in many shapes — bail.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
2
2
|
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
|
|
3
|
+
import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
|
|
3
4
|
import { hasAdditionalProperties, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, } from '@amritk/helpers/schema-guards';
|
|
4
5
|
import { generateEnumCheck } from './generate-enum-check.js';
|
|
5
6
|
/**
|
|
@@ -37,6 +38,10 @@ export const generateSchemaChecks = (accessor, schema) => {
|
|
|
37
38
|
case 'number':
|
|
38
39
|
case 'integer':
|
|
39
40
|
checks.push(`typeof ${accessor} === "number"`);
|
|
41
|
+
// `integer` must reject non-integral numbers (a bare typeof accepts `1.5`).
|
|
42
|
+
if (schema.type === 'integer') {
|
|
43
|
+
checks.push(`Number.isInteger(${accessor})`);
|
|
44
|
+
}
|
|
40
45
|
if (hasMinimum(schema)) {
|
|
41
46
|
checks.push(`${accessor} >= ${schema.minimum}`);
|
|
42
47
|
}
|
|
@@ -50,7 +55,7 @@ export const generateSchemaChecks = (accessor, schema) => {
|
|
|
50
55
|
checks.push(`${accessor} < ${schema.exclusiveMaximum}`);
|
|
51
56
|
}
|
|
52
57
|
if (hasMultipleOf(schema)) {
|
|
53
|
-
checks.push(
|
|
58
|
+
checks.push(multipleOfPassExpr(accessor, schema.multipleOf));
|
|
54
59
|
}
|
|
55
60
|
break;
|
|
56
61
|
case 'boolean':
|
|
@@ -73,7 +78,7 @@ export const generateSchemaChecks = (accessor, schema) => {
|
|
|
73
78
|
// Check required properties exist
|
|
74
79
|
if (hasRequired(schema) && schema.required.length > 0) {
|
|
75
80
|
for (const requiredKey of schema.required) {
|
|
76
|
-
checks.push(
|
|
81
|
+
checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
// Check minProperties
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* Context for assertions that reach beyond the property's own schema: union
|
|
4
|
+
* membership checks may call imported `validate{X}Shape` predicates ($ref
|
|
5
|
+
* branches), which requires knowing the ref-import mode, the type-name suffix,
|
|
6
|
+
* and the root document (to prove those validators are real — see
|
|
7
|
+
* canEnforceUnion). `stripUnknown` disables union enforcement entirely, since
|
|
8
|
+
* shape validators then treat undeclared keys as a mismatch while the
|
|
9
|
+
* stripUnknown contract is to drop them.
|
|
10
|
+
*/
|
|
11
|
+
export type StrictAssertionContext = {
|
|
12
|
+
readonly useRefImports?: boolean;
|
|
13
|
+
readonly suffix?: string;
|
|
14
|
+
readonly rootSchema?: Record<string, unknown>;
|
|
15
|
+
readonly stripUnknown?: boolean;
|
|
16
|
+
};
|
|
2
17
|
/**
|
|
3
18
|
* Generates strict-mode assertions for the body of an object parser.
|
|
4
19
|
* Throws on:
|
|
@@ -10,7 +25,7 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
10
25
|
* Properties with a `$ref` are validated by the nested parser's own strict
|
|
11
26
|
* check when that parser is invoked downstream.
|
|
12
27
|
*/
|
|
13
|
-
export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string) => string[];
|
|
28
|
+
export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeName: string, context?: StrictAssertionContext) => string[];
|
|
14
29
|
/**
|
|
15
30
|
* Generates a single strict-mode assertion line for a non-object scalar parser.
|
|
16
31
|
* Returns null when the schema has no type information to assert on.
|