@amritk/generate-parsers 0.12.3 → 0.14.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/dist/generators/build-schema.d.ts +6 -1
- package/dist/generators/build-schema.js +30 -5
- package/dist/generators/generate-files.d.ts +8 -0
- package/dist/generators/generate-files.js +12 -5
- package/dist/generators/generate-parser-function.d.ts +9 -1
- package/dist/generators/generate-parser-function.js +346 -192
- package/dist/generators/generate-strict-assertion.d.ts +16 -1
- package/dist/generators/generate-strict-assertion.js +71 -12
- package/dist/generators/generate-type-checks.d.ts +82 -0
- package/dist/generators/generate-type-checks.js +438 -0
- package/dist/generators/generate-validation-expression.d.ts +7 -0
- package/dist/generators/generate-validation-expression.js +17 -5
- package/dist/helpers/collect-helpers.d.ts +1 -9
- package/dist/helpers/collect-helpers.js +36 -9
- package/dist/helpers/collect-imports.d.ts +15 -0
- package/dist/helpers/collect-imports.js +36 -14
- package/dist/index.d.ts +1 -0
- package/package.json +2 -2
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
|
|
2
|
-
import {
|
|
2
|
+
import { quoteJsString } from '@amritk/helpers/quote-js-string';
|
|
3
3
|
import { refToName } from '@amritk/helpers/ref-to-name';
|
|
4
4
|
import { resolveRef } from '@amritk/helpers/resolve-ref';
|
|
5
5
|
import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
|
|
6
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum,
|
|
6
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasOneOf, hasProperties, hasRef, hasRequired, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
|
|
7
7
|
import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
|
|
8
8
|
import { findDiscriminator } from '../helpers/find-discriminator.js';
|
|
9
9
|
import { getDefaultValue } from '../helpers/get-default-value.js';
|
|
10
10
|
import { getDiscriminatorValue } from '../helpers/get-discriminator-value.js';
|
|
11
11
|
import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
|
|
12
|
-
import {
|
|
12
|
+
import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName, } from './generate-type-checks.js';
|
|
13
|
+
import { generateValidationExpression, isCoercibleItemSchema } from './generate-validation-expression.js';
|
|
13
14
|
/**
|
|
14
15
|
* Generates the parser function name from a type name.
|
|
15
16
|
* Converts "UserObject" to "parseUserObject".
|
|
@@ -350,7 +351,7 @@ const generateRefUnionDispatch = (functionName, typeName, branches, strict, ctx)
|
|
|
350
351
|
// first branch's parser, preserving the coercing parser's lenient contract while
|
|
351
352
|
// still running real validation/coercion instead of a blind cast.
|
|
352
353
|
const fallback = strict
|
|
353
|
-
? `(() => { throw new Error(${
|
|
354
|
+
? `(() => { throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)}); })()`
|
|
354
355
|
: `${cases[0]?.parser}(input)`;
|
|
355
356
|
let expr = fallback;
|
|
356
357
|
for (let i = cases.length - 1; i >= 0; i--) {
|
|
@@ -374,6 +375,30 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
374
375
|
return dispatch;
|
|
375
376
|
}
|
|
376
377
|
}
|
|
378
|
+
// An alias definition (a bare `$ref` with no shape of its own, e.g. a root
|
|
379
|
+
// schema that is just `$ref: '#/$defs/expr'`) delegates to the referenced
|
|
380
|
+
// parser instead of blindly casting, in both modes. Guarded against
|
|
381
|
+
// self-reference: delegating to ourselves would recurse forever.
|
|
382
|
+
if (unionCtx?.useRefImports && isSchemaObject(schema) && hasRef(schema) && !hasProperties(schema)) {
|
|
383
|
+
const refName = refToName(schema.$ref, unionCtx.suffix);
|
|
384
|
+
if (refName !== typeName) {
|
|
385
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Strict union enforcement for inline (or mixed) branches: membership is the
|
|
389
|
+
// disjunction of per-branch checks, and a non-member throws. Only emitted
|
|
390
|
+
// when every branch check is false-sound (see canEnforceUnion) — otherwise a
|
|
391
|
+
// conservative stub validator somewhere in the graph could reject valid
|
|
392
|
+
// input — and never when stripping unknown keys (see UnionParserContext).
|
|
393
|
+
if (strict && unionCtx && isSchemaObject(schema)) {
|
|
394
|
+
const branches = getUnionBranches(schema);
|
|
395
|
+
if (branches && !unionCtx.stripUnknown && canEnforceUnion(branches, unionCtx.rootSchema)) {
|
|
396
|
+
const check = generateUnionCheck('input', branches, unionCtx.useRefImports, unionCtx.suffix);
|
|
397
|
+
if (check !== null) {
|
|
398
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!(${check})) throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)});\n return input as ${typeName};\n};`;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
377
402
|
// `const`/`enum` define the value space directly (the generated type is a
|
|
378
403
|
// literal / literal-union), so a non-member must coerce to a valid member —
|
|
379
404
|
// otherwise the returned value would not be of the declared type. This runs
|
|
@@ -413,6 +438,50 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
413
438
|
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
|
|
414
439
|
}
|
|
415
440
|
}
|
|
441
|
+
// Root-level arrays with rich item schemas delegate every element to a real
|
|
442
|
+
// parser, so nested enums and $refs inside array items are validated instead
|
|
443
|
+
// of spread through unchecked. $ref items call the imported parser; inline
|
|
444
|
+
// object items get a private item sub-parser in the same file. (Scalar and
|
|
445
|
+
// enum items are covered below: the lax switch coerces them element-wise and
|
|
446
|
+
// generateScalarStrictAssertion enforces them in strict mode.)
|
|
447
|
+
if (isSchemaObject(schema) &&
|
|
448
|
+
hasType(schema) &&
|
|
449
|
+
schema.type === 'array' &&
|
|
450
|
+
hasItems(schema) &&
|
|
451
|
+
!Array.isArray(schema.items)) {
|
|
452
|
+
const items = schema.items;
|
|
453
|
+
const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
|
|
454
|
+
// validateArray identity-returns the input array when every element parses
|
|
455
|
+
// to itself; this parser is EXPORTED, and exported parsers never alias the
|
|
456
|
+
// value the caller passed in (matching the scalar root-array path's
|
|
457
|
+
// `[...input]` copy), so materialize a copy exactly when that happens —
|
|
458
|
+
// element references are still shared, like every other fast path.
|
|
459
|
+
const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});\n return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
|
|
460
|
+
const delegated = (itemParserName) => strict
|
|
461
|
+
? `export const ${functionName} = (input: unknown): ${typeName} => {\n ${notArrayThrow}\n${delegatedBody(itemParserName)}\n};`
|
|
462
|
+
: `export const ${functionName} = (input: unknown): ${typeName} => {\n${delegatedBody(itemParserName)}\n};`;
|
|
463
|
+
if (unionCtx?.useRefImports && isSchemaObject(items) && hasRef(items)) {
|
|
464
|
+
return delegated(generateParserName(refToName(items.$ref, unionCtx.suffix)));
|
|
465
|
+
}
|
|
466
|
+
if (isInlineObjectProperty(items)) {
|
|
467
|
+
const reserved = unionCtx?.reservedNames ?? NO_RESERVED_NAMES;
|
|
468
|
+
// Dedup against imported identifiers: a root `List` whose items carry a
|
|
469
|
+
// $ref to `#/$defs/listItem` imports parseListItem — a bare `ListItem`
|
|
470
|
+
// here would shadow it (TS2440) and self-recurse instead of delegating.
|
|
471
|
+
let itemName = `${typeName}_Item`;
|
|
472
|
+
while (reserved.has(itemName))
|
|
473
|
+
itemName = `${itemName}_`;
|
|
474
|
+
const useRefImports = unionCtx?.useRefImports ?? false;
|
|
475
|
+
const suffix = unionCtx?.suffix ?? '';
|
|
476
|
+
const stripUnknown = unionCtx?.stripUnknown ?? false;
|
|
477
|
+
const preamble = [
|
|
478
|
+
`type ${itemName} = ${typeName}[number];`,
|
|
479
|
+
generateShapeValidator(items, itemName, useRefImports, suffix, false, stripUnknown, reserved),
|
|
480
|
+
generateObjectParser(items, itemName, useRefImports, suffix, unionCtx?.logWarnings ?? false, strict, false, stripUnknown, unionCtx?.rootSchema, reserved),
|
|
481
|
+
].join('\n\n');
|
|
482
|
+
return `${preamble}\n\n${delegated(generateParserName(itemName))}`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
416
485
|
if (!isSchemaObject(schema) || !hasType(schema)) {
|
|
417
486
|
// Schema without type information cannot be validated beyond a cast
|
|
418
487
|
return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
|
|
@@ -439,8 +508,8 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
|
|
|
439
508
|
case 'boolean':
|
|
440
509
|
return `export const ${functionName} = (input: unknown): ${typeName} => typeof input === "boolean" ? input as ${typeName} : false as ${typeName};`;
|
|
441
510
|
case 'array': {
|
|
442
|
-
// Coerce each element when the item schema is a single scalar type.
|
|
443
|
-
if (hasItems(schema) && !Array.isArray(schema.items) &&
|
|
511
|
+
// Coerce each element when the item schema is a single scalar type or an enum.
|
|
512
|
+
if (hasItems(schema) && !Array.isArray(schema.items) && isCoercibleItemSchema(schema.items)) {
|
|
444
513
|
const item = schema.items;
|
|
445
514
|
const itemExpr = generateValidationExpression('', item, getDefaultValue(item), true, undefined, undefined, '_it', true);
|
|
446
515
|
return `export const ${functionName} = (input: unknown): ${typeName} => Array.isArray(input) ? (input as unknown[]).map((_it) => ${itemExpr}) as ${typeName} : [] as ${typeName};`;
|
|
@@ -471,62 +540,55 @@ const toVarName = (key) => {
|
|
|
471
540
|
const safe = key.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
472
541
|
return `_${safe}`;
|
|
473
542
|
};
|
|
543
|
+
/** PascalCases a property key for use in a synthesized sub-type name. */
|
|
544
|
+
const pascalCaseKey = (key) => key
|
|
545
|
+
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
546
|
+
.split('_')
|
|
547
|
+
.filter((part) => part.length > 0)
|
|
548
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
549
|
+
.join('');
|
|
550
|
+
/** Shared empty reserved-name set for callers with no import context. */
|
|
551
|
+
const NO_RESERVED_NAMES = new Set();
|
|
474
552
|
/**
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*/
|
|
479
|
-
const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
|
|
480
|
-
/**
|
|
481
|
-
* Matches an inline nested object property — an object schema written directly
|
|
482
|
-
* under `properties` with its own `properties`, rather than referenced via
|
|
483
|
-
* `$ref`. These get a private sub-parser (and shape predicate) in the same
|
|
484
|
-
* generated file so their fields are actually parsed; anything involving
|
|
485
|
-
* composition, conditionals, enums, or record semantics keeps the existing
|
|
486
|
-
* code paths.
|
|
487
|
-
*/
|
|
488
|
-
const isInlineObjectProperty = (propSchema) => {
|
|
489
|
-
if (!isSchemaObject(propSchema))
|
|
490
|
-
return false;
|
|
491
|
-
if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema))
|
|
492
|
-
return false;
|
|
493
|
-
if (hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema))
|
|
494
|
-
return false;
|
|
495
|
-
if ('patternProperties' in propSchema || 'not' in propSchema || 'if' in propSchema)
|
|
496
|
-
return false;
|
|
497
|
-
// additionalProperties-as-schema records go through the record paths instead
|
|
498
|
-
if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== 'boolean')
|
|
499
|
-
return false;
|
|
500
|
-
return isObjectSchema(propSchema) && hasProperties(propSchema);
|
|
501
|
-
};
|
|
502
|
-
/**
|
|
503
|
-
* Builds the property-key → synthesized-type-name map for every inline nested
|
|
504
|
-
* object property of a schema, e.g. parent `Order` + key `shipTo` →
|
|
505
|
-
* `OrderShipTo`. Both the parser and the shape validator derive the map
|
|
506
|
-
* independently, so the naming (including collision suffixes) is a pure
|
|
507
|
-
* function of the schema and parent type name.
|
|
553
|
+
* Shared empty result for the common no-inline-sub-types case, so the per-node
|
|
554
|
+
* calls (parser and validator, for every object node, on every generation)
|
|
555
|
+
* allocate nothing. Callers only read from the maps.
|
|
508
556
|
*/
|
|
509
|
-
const
|
|
510
|
-
|
|
557
|
+
const EMPTY_SUB_TYPES = { objects: new Map(), arrayItems: new Map() };
|
|
558
|
+
const collectInlineSubTypes = (schema, typeName, reservedNames = NO_RESERVED_NAMES) => {
|
|
511
559
|
if (!hasProperties(schema))
|
|
512
|
-
return
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
560
|
+
return EMPTY_SUB_TYPES;
|
|
561
|
+
const props = schema.properties;
|
|
562
|
+
// Single pass with lazy allocation: most object nodes have neither inline
|
|
563
|
+
// object properties nor inline-object array items, and this runs for every
|
|
564
|
+
// node in both the parser and the validator — so nothing is allocated until
|
|
565
|
+
// the first match, and the classification predicates run exactly once per
|
|
566
|
+
// property (an eager pre-scan would re-evaluate them for matching schemas).
|
|
567
|
+
let objects = null;
|
|
568
|
+
let arrayItems = null;
|
|
569
|
+
let used = null;
|
|
570
|
+
const claim = (base) => {
|
|
571
|
+
used ??= new Set();
|
|
572
|
+
let subName = base;
|
|
573
|
+
while (used.has(subName) || reservedNames.has(subName))
|
|
525
574
|
subName = `${subName}_`;
|
|
526
575
|
used.add(subName);
|
|
527
|
-
|
|
576
|
+
return subName;
|
|
577
|
+
};
|
|
578
|
+
for (const key in props) {
|
|
579
|
+
const propSchema = props[key];
|
|
580
|
+
if (isInlineObjectProperty(propSchema)) {
|
|
581
|
+
objects ??= new Map();
|
|
582
|
+
objects.set(key, claim(`${typeName}_${pascalCaseKey(key) || 'Value'}`));
|
|
583
|
+
}
|
|
584
|
+
else if (isInlineObjectArrayProperty(propSchema)) {
|
|
585
|
+
arrayItems ??= new Map();
|
|
586
|
+
arrayItems.set(key, claim(`${typeName}_${pascalCaseKey(key) || 'Value'}Item`));
|
|
587
|
+
}
|
|
528
588
|
}
|
|
529
|
-
|
|
589
|
+
if (objects === null && arrayItems === null)
|
|
590
|
+
return EMPTY_SUB_TYPES;
|
|
591
|
+
return { objects: objects ?? EMPTY_SUB_TYPES.objects, arrayItems: arrayItems ?? EMPTY_SUB_TYPES.arrayItems };
|
|
530
592
|
};
|
|
531
593
|
/**
|
|
532
594
|
* True when the parser must reject (strict mode) or strip (coerce mode) every
|
|
@@ -545,117 +607,42 @@ const hasStrictKeys = (schema) => {
|
|
|
545
607
|
return true;
|
|
546
608
|
};
|
|
547
609
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
610
|
+
* When every declared property is required (and every required key is
|
|
611
|
+
* declared, with a real schema), the fast-path known-keys test can be an
|
|
612
|
+
* own-key *count* comparison instead of a per-key `for..in` walk: the typed
|
|
613
|
+
* property checks already prove all N declared keys are present, so
|
|
614
|
+
* `Object.keys(input).length === N` proves there are no undeclared extras —
|
|
615
|
+
* measurably cheaper on the hot path. Returns the declared-key count, or null
|
|
616
|
+
* when the cheaper form would be unsound (an optional or non-schema property
|
|
617
|
+
* breaks the presence proof).
|
|
555
618
|
*/
|
|
556
|
-
const
|
|
557
|
-
if (!isSchemaObject(schema))
|
|
558
|
-
return null;
|
|
559
|
-
// $ref via shape predicate (deep fast-path through nested types).
|
|
560
|
-
if (useRefImports && hasRef(schema)) {
|
|
561
|
-
const refName = refToName(schema.$ref, suffix);
|
|
562
|
-
return `${shapeValidatorName(refName)}(${varName})`;
|
|
563
|
-
}
|
|
564
|
-
// Skip fast path for complex schemas that need more elaborate validation
|
|
565
|
-
if (hasEnum(schema) ||
|
|
566
|
-
hasOneOf(schema) ||
|
|
567
|
-
hasAnyOf(schema) ||
|
|
568
|
-
hasAllOf(schema) ||
|
|
569
|
-
hasRef(schema) ||
|
|
570
|
-
'not' in schema) {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
// A `const` property (the usual discriminated-union tag, e.g. `kind: "lit"`) has
|
|
574
|
-
// no `type`, but is fast-path predicable via strict equality. Handling it here is
|
|
575
|
-
// what lets a discriminated branch's shape validator be a real predicate instead
|
|
576
|
-
// of the `=> false` stub. Structural consts need deep equality, so skip those.
|
|
577
|
-
if (hasConst(schema)) {
|
|
578
|
-
const c = schema.const;
|
|
579
|
-
return c === null || typeof c !== 'object' ? `${varName} === ${JSON.stringify(c)}` : null;
|
|
580
|
-
}
|
|
581
|
-
if (!hasType(schema))
|
|
619
|
+
const exactKeyCountOf = (schema) => {
|
|
620
|
+
if (!isSchemaObject(schema) || !hasProperties(schema) || !hasRequired(schema))
|
|
582
621
|
return null;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
checks.push(`${varName}.length >= ${schema.minLength}`);
|
|
600
|
-
if (hasMaxLength(schema))
|
|
601
|
-
checks.push(`${varName}.length <= ${schema.maxLength}`);
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
case 'number':
|
|
605
|
-
case 'integer': {
|
|
606
|
-
checks.push(`typeof ${varName} === "number"`);
|
|
607
|
-
// `integer` must reject non-integral numbers on the fast path too, otherwise
|
|
608
|
-
// `1.5` would pass through uncoerced.
|
|
609
|
-
if (schema.type === 'integer')
|
|
610
|
-
checks.push(`Number.isInteger(${varName})`);
|
|
611
|
-
if (hasMinimum(schema))
|
|
612
|
-
checks.push(`${varName} >= ${schema.minimum}`);
|
|
613
|
-
if (hasMaximum(schema))
|
|
614
|
-
checks.push(`${varName} <= ${schema.maximum}`);
|
|
615
|
-
if (hasExclusiveMinimum(schema))
|
|
616
|
-
checks.push(`${varName} > ${schema.exclusiveMinimum}`);
|
|
617
|
-
if (hasExclusiveMaximum(schema))
|
|
618
|
-
checks.push(`${varName} < ${schema.exclusiveMaximum}`);
|
|
619
|
-
if (hasMultipleOf(schema))
|
|
620
|
-
checks.push(multipleOfPassExpr(varName, schema.multipleOf));
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
case 'boolean':
|
|
624
|
-
checks.push(`typeof ${varName} === "boolean"`);
|
|
625
|
-
break;
|
|
626
|
-
case 'array': {
|
|
627
|
-
checks.push(`Array.isArray(${varName})`);
|
|
628
|
-
if (hasMinItems(schema))
|
|
629
|
-
checks.push(`${varName}.length >= ${schema.minItems}`);
|
|
630
|
-
if (hasMaxItems(schema))
|
|
631
|
-
checks.push(`${varName}.length <= ${schema.maxItems}`);
|
|
632
|
-
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
633
|
-
checks.push(`new Set(${varName}).size === ${varName}.length`);
|
|
634
|
-
}
|
|
635
|
-
// For a scalar item type, every element must already be well-typed to take
|
|
636
|
-
// the fast path; a mistyped element routes the array to the slow path where
|
|
637
|
-
// each element is coerced.
|
|
638
|
-
if (hasItems(schema) && !Array.isArray(schema.items)) {
|
|
639
|
-
const itemCheck = scalarItemTypeCheck(schema.items, '_it');
|
|
640
|
-
if (itemCheck)
|
|
641
|
-
checks.push(`${varName}.every((_it) => ${itemCheck})`);
|
|
642
|
-
}
|
|
643
|
-
break;
|
|
644
|
-
}
|
|
645
|
-
case 'object':
|
|
646
|
-
checks.push(`isObject(${varName})`);
|
|
647
|
-
break;
|
|
648
|
-
default:
|
|
622
|
+
const props = schema.properties;
|
|
623
|
+
const required = schema.required;
|
|
624
|
+
// Allocation-free for the common small object: this runs for every object
|
|
625
|
+
// node, in both the parser and the validator, on every generation, and the
|
|
626
|
+
// O(n²) `includes` beats building a Set for a handful of keys. Wide schemas
|
|
627
|
+
// (generated API models) flip to a one-time Set so a 200-property object
|
|
628
|
+
// doesn't pay ~20k string comparisons per node.
|
|
629
|
+
const requiredLookup = required.length > 16 ? new Set(required) : null;
|
|
630
|
+
let declaredCount = 0;
|
|
631
|
+
for (const key in props) {
|
|
632
|
+
declaredCount++;
|
|
633
|
+
// Every declared key must be required, and its check must prove presence —
|
|
634
|
+
// all schema-object checks fail on undefined, but a true/false schema
|
|
635
|
+
// literal emits no check at all.
|
|
636
|
+
const isRequired = requiredLookup !== null ? requiredLookup.has(key) : required.includes(key);
|
|
637
|
+
if (!isRequired || !isSchemaObject(props[key]))
|
|
649
638
|
return null;
|
|
650
639
|
}
|
|
651
|
-
|
|
640
|
+
// Every declared key is required (above) and the counts match, so the
|
|
641
|
+
// required list is exactly the declared keys (a duplicated required entry
|
|
642
|
+
// would leave some declared key uncovered and fail the loop).
|
|
643
|
+
if (declaredCount === 0 || required.length !== declaredCount)
|
|
652
644
|
return null;
|
|
653
|
-
|
|
654
|
-
let result = checks[0];
|
|
655
|
-
for (let i = 1; i < checks.length; i++) {
|
|
656
|
-
result += ' && ' + checks[i];
|
|
657
|
-
}
|
|
658
|
-
return result;
|
|
645
|
+
return declaredCount;
|
|
659
646
|
};
|
|
660
647
|
/**
|
|
661
648
|
* Determines if a property needs a local variable or can be inlined.
|
|
@@ -677,7 +664,7 @@ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
|
|
|
677
664
|
* - Fast path that returns input directly when all properties are already valid
|
|
678
665
|
* - Slow path with optimized expressions
|
|
679
666
|
*/
|
|
680
|
-
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false) => {
|
|
667
|
+
const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true, stripUnknown = false, rootSchema, reservedNames = NO_RESERVED_NAMES) => {
|
|
681
668
|
const functionName = generateParserName(typeName);
|
|
682
669
|
const exportPrefix = exported ? 'export ' : '';
|
|
683
670
|
if (!hasProperties(schema)) {
|
|
@@ -686,18 +673,35 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
686
673
|
}
|
|
687
674
|
return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
|
|
688
675
|
}
|
|
689
|
-
const
|
|
676
|
+
const schemaProps = schema.properties;
|
|
677
|
+
// Keys once, values via lookup — Object.entries allocates a tuple per
|
|
678
|
+
// property and this runs for every object node on every generation.
|
|
679
|
+
const propertyKeys = Object.keys(schemaProps);
|
|
690
680
|
// Inline nested object properties get a private sub-parser (plus shape
|
|
691
681
|
// predicate and type alias) in the same file, so their fields are parsed for
|
|
692
682
|
// real instead of only passing an isObject check. The sub-parser recurses
|
|
693
|
-
// through this same generator, so nesting works to any depth.
|
|
694
|
-
|
|
683
|
+
// through this same generator, so nesting works to any depth. Array
|
|
684
|
+
// properties with inline object items get the same treatment for their
|
|
685
|
+
// element type, so nested enums and $refs *inside array items* are validated
|
|
686
|
+
// too (each element runs through the item sub-parser via validateArray).
|
|
687
|
+
const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
|
|
695
688
|
const preamble = [];
|
|
696
689
|
for (const [key, subName] of subTypeNames) {
|
|
697
|
-
const propSchema =
|
|
690
|
+
const propSchema = schemaProps[key];
|
|
698
691
|
preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
|
|
699
|
-
preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown));
|
|
700
|
-
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown));
|
|
692
|
+
preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames));
|
|
693
|
+
preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames));
|
|
694
|
+
}
|
|
695
|
+
for (const [key, subName] of subItemNames) {
|
|
696
|
+
const propSchema = schemaProps[key];
|
|
697
|
+
const itemSchema = propSchema.items;
|
|
698
|
+
// NonNullable strips the `| undefined` an optional array property carries.
|
|
699
|
+
preamble.push(`type ${subName} = NonNullable<${typeName}[${JSON.stringify(key)}]>[number];`);
|
|
700
|
+
preamble.push(generateShapeValidator(itemSchema, subName, useRefImports, suffix, false, stripUnknown, reservedNames));
|
|
701
|
+
// Hand-rolled loop instead of Array.prototype.every on the guard path — the
|
|
702
|
+
// callback protocol costs a few percent on element-heavy hot paths.
|
|
703
|
+
preamble.push(`const _every${subName} = (arr: readonly unknown[]): boolean => {\n for (let i = 0; i < arr.length; i++) if (!${shapeValidatorName(subName)}(arr[i])) return false;\n return true;\n};`);
|
|
704
|
+
preamble.push(generateObjectParser(itemSchema, subName, useRefImports, suffix, logWarnings, strict, false, stripUnknown, rootSchema, reservedNames));
|
|
701
705
|
}
|
|
702
706
|
// additionalProperties: false — the per-key "is this undeclared" test inlines
|
|
703
707
|
// `!==` comparisons for a short key list and hoists a Set only for a long one
|
|
@@ -710,13 +714,25 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
710
714
|
// `strictKeys` makes an extra a hard error (the throw block further down).
|
|
711
715
|
const strictKeys = hasStrictKeys(schema);
|
|
712
716
|
const stripKeys = strictKeys || stripUnknown;
|
|
713
|
-
|
|
714
|
-
|
|
717
|
+
// When every declared property is required, the fast-path no-extras test is
|
|
718
|
+
// the cheaper own-key count (see exactKeyCountOf) and the `_hasOnlyKnownKeys`
|
|
719
|
+
// predicate is not emitted at all — the shape validator derives the same
|
|
720
|
+
// decision from the schema, so the cross-function contract stays in sync.
|
|
721
|
+
const exactKeyCount = stripKeys ? exactKeyCountOf(schema) : null;
|
|
722
|
+
const strictKeyCheck = unknownKeyCheck(propertyKeys, `_knownKeys${typeName}`);
|
|
723
|
+
if (stripKeys && exactKeyCount === null) {
|
|
715
724
|
for (const declaration of strictKeyCheck.declarations) {
|
|
716
725
|
preamble.push(`${declaration};`);
|
|
717
726
|
}
|
|
718
727
|
preamble.push(`const _hasOnlyKnownKeys${typeName} = (input: Record<string, unknown>): boolean => {\n for (const _k in input) if (${strictKeyCheck.isUnknown('_k')}) return false;\n return true;\n};`);
|
|
719
728
|
}
|
|
729
|
+
else if (strict && strictKeys) {
|
|
730
|
+
// The unknown-key throw loop below still needs the hoisted Set (when the
|
|
731
|
+
// key list is long enough to use one).
|
|
732
|
+
for (const declaration of strictKeyCheck.declarations) {
|
|
733
|
+
preamble.push(`${declaration};`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
720
736
|
const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix, subTypeNames);
|
|
721
737
|
// First pass: gather per-property info and decide whether a fast path is
|
|
722
738
|
// possible. A fast path returns the input (or a direct strip-build) for
|
|
@@ -736,7 +752,8 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
736
752
|
// declarations (TS2451). Dedupe by suffixing `_` until unique — the same
|
|
737
753
|
// approach `collectInlineObjectProperties` uses — so each key gets its own var.
|
|
738
754
|
const usedVarNames = new Set();
|
|
739
|
-
for (const
|
|
755
|
+
for (const key of propertyKeys) {
|
|
756
|
+
const propSchema = schemaProps[key];
|
|
740
757
|
const isRequired = isPropertyRequired(key, schema);
|
|
741
758
|
let varName = toVarName(key);
|
|
742
759
|
while (usedVarNames.has(varName))
|
|
@@ -753,10 +770,15 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
753
770
|
}
|
|
754
771
|
const subName = subTypeNames.get(key);
|
|
755
772
|
// Inline nested objects fast-path through their private shape predicate, the
|
|
756
|
-
// same way $ref properties use the imported one.
|
|
757
|
-
|
|
773
|
+
// same way $ref properties use the imported one. Arrays of inline objects
|
|
774
|
+
// additionally prove every element via the private item predicate.
|
|
775
|
+
let check = subName
|
|
758
776
|
? `${shapeValidatorName(subName)}(${varName})`
|
|
759
777
|
: generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
|
|
778
|
+
const itemSubName = subItemNames.get(key);
|
|
779
|
+
if (check !== null && itemSubName) {
|
|
780
|
+
check = `${check} && _every${itemSubName}(${varName})`;
|
|
781
|
+
}
|
|
760
782
|
if (check === null) {
|
|
761
783
|
canFastPath = false;
|
|
762
784
|
}
|
|
@@ -771,9 +793,16 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
771
793
|
// fire on inputs that carry no undeclared key. For strict + additionalProperties:
|
|
772
794
|
// false the cold path rejects extras instead; folding the known-keys term into
|
|
773
795
|
// the guard lets the guard run *before* that rejection, so a valid clean input
|
|
774
|
-
// never pays for the per-property assertions.
|
|
796
|
+
// never pays for the per-property assertions. With every declared property
|
|
797
|
+
// required, the own-key count is equivalent and cheaper than the per-key walk —
|
|
798
|
+
// but only for plain objects: a crafted prototype could satisfy the typed
|
|
799
|
+
// checks through inherited properties while the own-key count still matches,
|
|
800
|
+
// so non-plain inputs route to the slow path, where the for..in walk keeps
|
|
801
|
+
// the historical inherited-key rejection.
|
|
775
802
|
if (stripKeys) {
|
|
776
|
-
fastPathChecks.push(
|
|
803
|
+
fastPathChecks.push(exactKeyCount !== null
|
|
804
|
+
? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}`
|
|
805
|
+
: `_hasOnlyKnownKeys${typeName}(input)`);
|
|
777
806
|
}
|
|
778
807
|
// The deep guard proves the whole shape (so `{ ...input }` can be returned),
|
|
779
808
|
// using the nested shape predicates and the known-keys term above.
|
|
@@ -879,6 +908,21 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
879
908
|
}
|
|
880
909
|
continue;
|
|
881
910
|
}
|
|
911
|
+
// Arrays of inline objects run every element through the private item
|
|
912
|
+
// sub-parser, the same way arrays of $refs delegate via validateArray.
|
|
913
|
+
// In strict mode the item parser throws on a bad element; in coerce mode
|
|
914
|
+
// it repairs the element to a valid instance.
|
|
915
|
+
const itemSubName = subItemNames.get(key);
|
|
916
|
+
if (itemSubName) {
|
|
917
|
+
const itemParserName = generateParserName(itemSubName);
|
|
918
|
+
if (isRequired) {
|
|
919
|
+
objectLines.push(` ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}),`);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: validateArray(${accessor}, ${itemParserName}) }),`);
|
|
923
|
+
}
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
882
926
|
// Handle direct $ref properties via imported parsers
|
|
883
927
|
if (shouldUseRefImport(propSchema, useRefImports)) {
|
|
884
928
|
const ref = propSchema.$ref;
|
|
@@ -967,6 +1011,18 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
967
1011
|
lines.push(` if (${deepGuard}) return { ...input } as ${typeName};`);
|
|
968
1012
|
return;
|
|
969
1013
|
}
|
|
1014
|
+
// A *private* (nested-object / array-item) parser whose deep guard proved
|
|
1015
|
+
// the input is exactly the declared shape — typed checks plus the
|
|
1016
|
+
// no-undeclared-keys term, recursively via sub-predicates — can hand the
|
|
1017
|
+
// input back by reference instead of allocating a literal. That is the
|
|
1018
|
+
// same sharing the parent's own fast-path literal performs (`items:
|
|
1019
|
+
// _items`), and it is what keeps clean array elements allocation-free.
|
|
1020
|
+
// Exported root parsers keep returning a fresh object so callers never
|
|
1021
|
+
// alias the value they passed in.
|
|
1022
|
+
if (!exported) {
|
|
1023
|
+
lines.push(` if (${deepGuard}) return input as ${typeName};`);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
970
1026
|
const fields = [];
|
|
971
1027
|
for (const { key, varName, isRequired, propSchema } of propInfo) {
|
|
972
1028
|
if (!isSchemaObject(propSchema)) {
|
|
@@ -995,15 +1051,52 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
995
1051
|
lines.push(...varDeclLines);
|
|
996
1052
|
lines.push(...warnLines);
|
|
997
1053
|
// The first assertion line is the `isObject` check, already done above.
|
|
998
|
-
const assertionLines = generateObjectStrictAssertion(schema, typeName
|
|
1054
|
+
const assertionLines = generateObjectStrictAssertion(schema, typeName, {
|
|
1055
|
+
useRefImports,
|
|
1056
|
+
suffix,
|
|
1057
|
+
stripUnknown,
|
|
1058
|
+
...(rootSchema !== undefined ? { rootSchema } : {}),
|
|
1059
|
+
}).slice(1);
|
|
999
1060
|
if (shallowGuard) {
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1061
|
+
// A private sub-parser hands input that is already exactly the declared
|
|
1062
|
+
// shape (no extras at any level) back by reference, so a clean array
|
|
1063
|
+
// element or nested object costs no allocation. The deep guard is
|
|
1064
|
+
// evaluated as the shallow guard plus only its *residual* terms (deeper
|
|
1065
|
+
// per-property checks and the no-extras term), so a carries-extras input
|
|
1066
|
+
// never runs the same typed checks twice before taking the strip build.
|
|
1067
|
+
if (!exported && deepGuard) {
|
|
1068
|
+
const residual = [];
|
|
1069
|
+
for (let i = 0; i < fastPathChecks.length; i++) {
|
|
1070
|
+
const deep = fastPathChecks[i];
|
|
1071
|
+
const shallow = shallowChecks[i];
|
|
1072
|
+
if (deep === shallow)
|
|
1073
|
+
continue;
|
|
1074
|
+
// When the deep check extends the shallow one by conjunction (e.g.
|
|
1075
|
+
// `Array.isArray(_x) && _everyItem(_x)`), the shallow guard already
|
|
1076
|
+
// proved the prefix — re-evaluating it per clean parse was pure
|
|
1077
|
+
// waste on the hot path, so only the extension joins the residual.
|
|
1078
|
+
residual.push(shallow !== undefined && deep.startsWith(`${shallow} && `) ? deep.slice(shallow.length + 4) : deep);
|
|
1079
|
+
}
|
|
1080
|
+
lines.push(` if (${shallowGuard}) {`);
|
|
1081
|
+
lines.push(residual.length > 0
|
|
1082
|
+
? ` if (${residual.join(' && ')}) return input as ${typeName};`
|
|
1083
|
+
: ` return input as ${typeName};`);
|
|
1084
|
+
lines.push(` } else {`);
|
|
1085
|
+
for (const assertionLine of assertionLines) {
|
|
1086
|
+
lines.push(` ${assertionLine}`);
|
|
1087
|
+
}
|
|
1088
|
+
lines.push(` }`);
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
// stripUnknown: a well-typed input skips the assertions and goes
|
|
1092
|
+
// straight to the strip build (which removes extras and recurses into
|
|
1093
|
+
// sub-parsers).
|
|
1094
|
+
lines.push(` if (!(${shallowGuard})) {`);
|
|
1095
|
+
for (const assertionLine of assertionLines) {
|
|
1096
|
+
lines.push(` ${assertionLine}`);
|
|
1097
|
+
}
|
|
1098
|
+
lines.push(` }`);
|
|
1005
1099
|
}
|
|
1006
|
-
lines.push(` }`);
|
|
1007
1100
|
emitReturn(lines, buildObjectLines(true));
|
|
1008
1101
|
}
|
|
1009
1102
|
else {
|
|
@@ -1014,6 +1107,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
|
|
|
1014
1107
|
lines.push(assertionLine);
|
|
1015
1108
|
}
|
|
1016
1109
|
if (strictKeys) {
|
|
1110
|
+
// for..in (not Object.keys) deliberately: this loop is cold — the fast
|
|
1111
|
+
// path already proved the key set — but swapping in the keys-array
|
|
1112
|
+
// iterator here once regressed the *hot* path several percent on CI:
|
|
1113
|
+
// the extra dead-path bytecode changed the engine's inlining of the
|
|
1114
|
+
// whole parser. The fast-path no-extras test uses own-key semantics;
|
|
1115
|
+
// an inherited-key mismatch merely lands here and keeps the historical
|
|
1116
|
+
// for..in rejection.
|
|
1017
1117
|
lines.push(` for (const _k in input) {`);
|
|
1018
1118
|
lines.push(` if (${strictKeyCheck.isUnknown('_k')}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
|
|
1019
1119
|
lines.push(` }`);
|
|
@@ -1436,7 +1536,10 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1436
1536
|
return generateNonObjectParser(typeName, schema, strict, {
|
|
1437
1537
|
useRefImports,
|
|
1438
1538
|
suffix,
|
|
1539
|
+
stripUnknown,
|
|
1540
|
+
logWarnings,
|
|
1439
1541
|
...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
|
|
1542
|
+
...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
|
|
1440
1543
|
});
|
|
1441
1544
|
}
|
|
1442
1545
|
// Handle schemas with both properties AND patternProperties.
|
|
@@ -1450,7 +1553,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1450
1553
|
// properties AND conditional keywords use all declared properties rather than
|
|
1451
1554
|
// only the if/then fragment.
|
|
1452
1555
|
if (hasProperties(schema)) {
|
|
1453
|
-
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown);
|
|
1556
|
+
return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames);
|
|
1454
1557
|
}
|
|
1455
1558
|
// Handle conditional schemas (if/then/else) for schemas without explicit properties.
|
|
1456
1559
|
if (isSchemaObject(schema) && 'if' in schema && 'then' in schema && 'else' in schema) {
|
|
@@ -1467,7 +1570,10 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1467
1570
|
return generateNonObjectParser(typeName, schema, strict, {
|
|
1468
1571
|
useRefImports,
|
|
1469
1572
|
suffix,
|
|
1573
|
+
stripUnknown,
|
|
1574
|
+
logWarnings,
|
|
1470
1575
|
...(options?.rootSchema !== undefined ? { rootSchema: options.rootSchema } : {}),
|
|
1576
|
+
...(options?.reservedNames !== undefined ? { reservedNames: options.reservedNames } : {}),
|
|
1471
1577
|
});
|
|
1472
1578
|
}
|
|
1473
1579
|
// Handle schemas with patternProperties (but no properties)
|
|
@@ -1521,12 +1627,37 @@ export const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeN
|
|
|
1521
1627
|
* in additionalProperties). The stub is safe: parents calling it will fall
|
|
1522
1628
|
* through to the slow path, matching the pre-deep-fast-path behavior.
|
|
1523
1629
|
*/
|
|
1524
|
-
export const generateShapeValidator = (schema, typeName, useRefImports, suffix = '', exported = true, stripUnknown = false) => {
|
|
1630
|
+
export const generateShapeValidator = (schema, typeName, useRefImports, suffix = '', exported = true, stripUnknown = false, reservedNames = NO_RESERVED_NAMES) => {
|
|
1525
1631
|
const fnName = shapeValidatorName(typeName);
|
|
1526
1632
|
const exportPrefix = exported ? 'export ' : '';
|
|
1527
1633
|
const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
|
|
1528
1634
|
if (!isSchemaObject(schema))
|
|
1529
1635
|
return stub;
|
|
1636
|
+
// An alias definition (a bare `$ref` with no shape of its own) delegates to
|
|
1637
|
+
// the referenced type's validator. Guarded against self-reference.
|
|
1638
|
+
if (useRefImports && hasRef(schema) && !hasProperties(schema)) {
|
|
1639
|
+
const refName = refToName(schema.$ref, suffix);
|
|
1640
|
+
if (refName !== typeName) {
|
|
1641
|
+
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${shapeValidatorName(refName)}(input);`;
|
|
1642
|
+
}
|
|
1643
|
+
return stub;
|
|
1644
|
+
}
|
|
1645
|
+
// A pure union definition (file-level oneOf/anyOf without properties) gets a
|
|
1646
|
+
// real membership predicate when every branch is checkable. A recursive
|
|
1647
|
+
// union (e.g. `expr` whose branches $ref `expr` itself) works because the
|
|
1648
|
+
// branch checks call this very validator by name at runtime. Skipped under
|
|
1649
|
+
// stripUnknown: the branch checks carry no known-keys terms, so a `true`
|
|
1650
|
+
// could not guarantee the parser's strip build would be a no-op.
|
|
1651
|
+
if (!hasProperties(schema) && !('patternProperties' in schema) && !('if' in schema) && !stripUnknown) {
|
|
1652
|
+
const branches = getUnionBranches(schema);
|
|
1653
|
+
if (branches) {
|
|
1654
|
+
const check = generateUnionCheck('input', branches, useRefImports, suffix);
|
|
1655
|
+
if (check !== null) {
|
|
1656
|
+
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return stub;
|
|
1660
|
+
}
|
|
1530
1661
|
if (!hasProperties(schema))
|
|
1531
1662
|
return stub;
|
|
1532
1663
|
// Composition / conditional schemas can match in many shapes — bail.
|
|
@@ -1540,18 +1671,26 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
|
|
|
1540
1671
|
'else' in schema) {
|
|
1541
1672
|
return stub;
|
|
1542
1673
|
}
|
|
1543
|
-
const
|
|
1674
|
+
const schemaProps = schema.properties;
|
|
1675
|
+
const propertyKeys = Object.keys(schemaProps);
|
|
1544
1676
|
// Same deterministic naming as the parser's sub-parser generation, so the
|
|
1545
1677
|
// referenced private shape predicates exist in the same file.
|
|
1546
|
-
const subTypeNames =
|
|
1678
|
+
const { objects: subTypeNames, arrayItems: subItemNames } = collectInlineSubTypes(schema, typeName, reservedNames);
|
|
1547
1679
|
// When the parser strips extras (additionalProperties: false or stripUnknown),
|
|
1548
1680
|
// an input carrying an undeclared key is *not* fast-path eligible — the parser
|
|
1549
1681
|
// would strip it rather than return `{ ...input }` — so the shape only matches
|
|
1550
|
-
// when every key is declared.
|
|
1551
|
-
//
|
|
1552
|
-
|
|
1682
|
+
// when every key is declared. With every declared property required, the
|
|
1683
|
+
// no-extras test is the cheaper own-key count appended after the typed checks
|
|
1684
|
+
// (which prove all N keys present); otherwise it is the `_hasOnlyKnownKeys`
|
|
1685
|
+
// walk the parser emits under the same `stripKeys` condition.
|
|
1686
|
+
const validatorStripKeys = hasStrictKeys(schema) || stripUnknown;
|
|
1687
|
+
const validatorKeyCount = validatorStripKeys ? exactKeyCountOf(schema) : null;
|
|
1688
|
+
const strictKeysGuard = validatorStripKeys && validatorKeyCount === null
|
|
1689
|
+
? `\n if (!_hasOnlyKnownKeys${typeName}(input)) return false;`
|
|
1690
|
+
: '';
|
|
1553
1691
|
const checks = [];
|
|
1554
|
-
for (const
|
|
1692
|
+
for (const key of propertyKeys) {
|
|
1693
|
+
const propSchema = schemaProps[key];
|
|
1555
1694
|
// Records of refs require iterating every value — too expensive for the fast path.
|
|
1556
1695
|
if (shouldUseRecordRefImport(propSchema, useRefImports)) {
|
|
1557
1696
|
return stub;
|
|
@@ -1559,9 +1698,16 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
|
|
|
1559
1698
|
const accessor = safeAccessor('input', key);
|
|
1560
1699
|
const isRequired = isPropertyRequired(key, schema);
|
|
1561
1700
|
const subName = subTypeNames.get(key);
|
|
1562
|
-
|
|
1701
|
+
let check = subName
|
|
1563
1702
|
? `${shapeValidatorName(subName)}(${accessor})`
|
|
1564
1703
|
: generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
|
|
1704
|
+
// Arrays of inline objects prove every element via the private item loop
|
|
1705
|
+
// helper (emitted alongside the item sub-parser), matching the parser's
|
|
1706
|
+
// fast-path guard.
|
|
1707
|
+
const itemSubName = subItemNames.get(key);
|
|
1708
|
+
if (check !== null && itemSubName) {
|
|
1709
|
+
check = `${check} && _every${itemSubName}(${accessor})`;
|
|
1710
|
+
}
|
|
1565
1711
|
if (check === null) {
|
|
1566
1712
|
return stub;
|
|
1567
1713
|
}
|
|
@@ -1572,6 +1718,14 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
|
|
|
1572
1718
|
checks.push(`(${accessor} === undefined || ${check})`);
|
|
1573
1719
|
}
|
|
1574
1720
|
}
|
|
1721
|
+
// The count form is sound only in conjunction with the typed checks above
|
|
1722
|
+
// (they prove every declared key present) and only for plain objects (a
|
|
1723
|
+
// crafted prototype could satisfy those checks through inherited
|
|
1724
|
+
// properties), so it joins the chain last, prototype-guarded like the
|
|
1725
|
+
// parser's own fast path.
|
|
1726
|
+
if (validatorKeyCount !== null) {
|
|
1727
|
+
checks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${validatorKeyCount}`);
|
|
1728
|
+
}
|
|
1575
1729
|
if (checks.length === 0) {
|
|
1576
1730
|
if (strictKeysGuard) {
|
|
1577
1731
|
return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
|