@amritk/generate-parsers 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,18 @@
1
- import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
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
- import { safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasOneOf, hasProperties, hasRef, hasRequired, hasType, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
5
+ import { missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasItems, hasMaxItems, hasMaxProperties, hasMinItems, hasMinProperties, hasOneOf, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, 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 { assertNoUnsupportedKeywords } from "./assert-supported-keywords.js";
12
12
  import { generateEnumCaseInsensitiveCoercion } from "./generate-enum-check.js";
13
- import { generateContainsCheck, generateObjectKeywordChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, inlineAllOfMembers } from "./generate-strict-assertion.js";
14
- import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName } from "./generate-type-checks.js";
13
+ import { generateBackstopAssertion, generateCompositionChecks, generateContainsCheck, generateKeyedValueChecks, generateObjectKeywordChecks, generateObjectSizeChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, inlineAllOfMembers } from "./generate-strict-assertion.js";
14
+ import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isExclusiveUnion, isInlineObjectArrayProperty, isInlineObjectProperty, shapeValidatorName } from "./generate-type-checks.js";
15
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
15
16
  import { generatePrefixItemsMap, generateValidationExpression, getPrefixItems, isCoercibleItemSchema, prefixItemsCapsLength } from "./generate-validation-expression.js";
16
17
  const generateParserName = (typeName) => {
17
18
  return `parse${typeName}`;
@@ -149,6 +150,8 @@ const generateFallbackValue = (_, propSchema, useRefImports, suffix) => {
149
150
  }
150
151
  return getDefaultValue(propSchema);
151
152
  };
153
+ const PROTOTYPE_MEMBER_NAMES = new Set(Object.getOwnPropertyNames(Object.prototype));
154
+ const declaresPrototypeMemberProperty = (schema) => hasProperties(schema) && Object.keys(schema.properties).some((key) => PROTOTYPE_MEMBER_NAMES.has(key));
152
155
  const generateFallbackObject = (schema, useRefImports, typeName, suffix, subTypeNames) => {
153
156
  if (!hasProperties(schema)) {
154
157
  return `{} as ${typeName}`;
@@ -180,7 +183,7 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
180
183
  }
181
184
  }
182
185
  result += "\n }";
183
- return result;
186
+ return declaresPrototypeMemberProperty(schema) ? `${result} as ${typeName}` : result;
184
187
  };
185
188
  const scalarDefaultLiteral = (schema) => {
186
189
  if (!isSchemaObject(schema))
@@ -208,7 +211,7 @@ const scalarDefaultLiteral = (schema) => {
208
211
  }
209
212
  return "{}";
210
213
  };
211
- const generateRefUnionDispatch = (functionName, typeName, branches, strict, ctx) => {
214
+ const generateRefUnionDispatch = (functionName, typeName, schema, branches, strict, ctx) => {
212
215
  const rootSchema = ctx.rootSchema;
213
216
  if (!ctx.useRefImports || rootSchema === void 0)
214
217
  return null;
@@ -239,17 +242,26 @@ const generateRefUnionDispatch = (functionName, typeName, branches, strict, ctx)
239
242
  if (c)
240
243
  expr = `_disc === ${JSON.stringify(c.value)} ? ${c.parser}(input) : ${expr}`;
241
244
  }
245
+ const backstop = strict ? generateBackstopAssertion("input", schema, `[${typeName}]`, strictContext(ctx)) : [];
246
+ const prelude = backstop.length > 0 ? `${backstop.join("\n")}
247
+ ` : "";
242
248
  return `export const ${functionName} = (input: unknown): ${typeName} => {
243
- const _disc = ${discExpr};
249
+ ${prelude} const _disc = ${discExpr};
244
250
  return ${expr};
245
251
  };`;
246
252
  };
253
+ const strictContext = (ctx) => ({
254
+ useRefImports: ctx?.useRefImports ?? false,
255
+ suffix: ctx?.suffix ?? "",
256
+ ...ctx?.rootSchema !== void 0 ? { rootSchema: ctx.rootSchema } : {},
257
+ ...ctx?.stripUnknown !== void 0 ? { stripUnknown: ctx.stripUnknown } : {}
258
+ });
247
259
  const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
248
260
  const functionName = generateParserName(typeName);
249
261
  if (unionCtx && isSchemaObject(schema) && (hasOneOf(schema) || hasAnyOf(schema))) {
250
262
  const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
251
263
  if (branches.length > 0 && branches.every((b) => isSchemaObject(b) && hasRef(b))) {
252
- const dispatch = generateRefUnionDispatch(functionName, typeName, branches, strict, unionCtx);
264
+ const dispatch = generateRefUnionDispatch(functionName, typeName, schema, branches, strict, unionCtx);
253
265
  if (dispatch !== null)
254
266
  return dispatch;
255
267
  }
@@ -257,16 +269,28 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
257
269
  if (unionCtx?.useRefImports && isSchemaObject(schema) && hasRef(schema) && !hasProperties(schema)) {
258
270
  const refName = refToName(schema.$ref, unionCtx.suffix);
259
271
  if (refName !== typeName) {
260
- return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
272
+ const assertion = strict ? generateBackstopAssertion("input", schema, `[${typeName}]`, strictContext(unionCtx)) : [];
273
+ if (assertion.length === 0) {
274
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${generateParserName(refName)}(input) as ${typeName};`;
275
+ }
276
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
277
+ ${assertion.join("\n")}
278
+ return ${generateParserName(refName)}(input) as ${typeName};
279
+ };`;
261
280
  }
262
281
  }
263
282
  if (strict && unionCtx && isSchemaObject(schema)) {
264
283
  const branches = getUnionBranches(schema);
265
284
  if (branches && !unionCtx.stripUnknown && canEnforceUnion(branches, unionCtx.rootSchema)) {
266
- const check = generateUnionCheck("input", branches, unionCtx.useRefImports, unionCtx.suffix);
285
+ const check = generateUnionCheck("input", branches, unionCtx.useRefImports, unionCtx.suffix, isExclusiveUnion(schema));
267
286
  if (check !== null) {
287
+ const backstop = generateBackstopAssertion("input", schema, `[${typeName}]`, strictContext(unionCtx));
288
+ const assertions = [
289
+ ` if (!(${check})) throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)});`,
290
+ ...backstop
291
+ ].join("\n");
268
292
  return `export const ${functionName} = (input: unknown): ${typeName} => {
269
- if (!(${check})) throw new Error(${quoteJsString(`[${typeName}] value does not match any union branch`)});
293
+ ${assertions}
270
294
  return input as ${typeName};
271
295
  };`;
272
296
  }
@@ -296,8 +320,8 @@ const generateNonObjectParser = (typeName, schema, strict, unionCtx) => {
296
320
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
297
321
  }
298
322
  }
299
- if (strict && isSchemaObject(schema) && !hasType(schema) && (hasConst(schema) || hasEnum(schema))) {
300
- const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
323
+ if (strict && isSchemaObject(schema) && !hasType(schema) && (hasConst(schema) || hasEnum(schema) || hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || "not" in schema || "if" in schema)) {
324
+ const assertion = generateScalarStrictAssertion(schema, typeName, strictContext(unionCtx));
301
325
  if (assertion !== null) {
302
326
  return `export const ${functionName} = (input: unknown): ${typeName} => {
303
327
  ${assertion}
@@ -308,7 +332,26 @@ ${assertion}
308
332
  if (isSchemaObject(schema) && hasType(schema) && schema.type === "array" && hasItems(schema) && !Array.isArray(schema.items)) {
309
333
  const items = schema.items;
310
334
  const notArrayThrow = `if (!Array.isArray(input)) throw new Error(\`[${typeName}] expected array, got \${input === null ? "null" : typeof input}\`);`;
311
- const strictArrayChecks = strict ? generateContainsCheck("input", schema, `[${typeName}]`).join("\n") : "";
335
+ const strictArrayChecks = strict ? [
336
+ // `minItems`/`maxItems` were dropped on this path entirely: a root
337
+ // array of objects or `$ref`s bypasses generateScalarStrictAssertion,
338
+ // so `{ type: 'array', minItems: 2, items: { type: 'object' } }`
339
+ // accepted a one-element array.
340
+ ...hasMinItems(schema) ? [
341
+ ` if (input.length < ${schema.minItems}) throw new Error(${quoteJsString(`[${typeName}] must have at least ${schema.minItems} items`)});`
342
+ ] : [],
343
+ ...hasMaxItems(schema) ? [
344
+ ` if (input.length > ${schema.maxItems}) throw new Error(${quoteJsString(`[${typeName}] must have at most ${schema.maxItems} items`)});`
345
+ ] : [],
346
+ ...generateContainsCheck("input", schema, `[${typeName}]`),
347
+ ...hasUniqueItems(schema) && schema.uniqueItems === true ? [
348
+ ` if (!(${generateUniqueItemsCheck("input", schema)})) throw new Error(${quoteJsString(`[${typeName}] must NOT have duplicate items`)});`
349
+ ] : [],
350
+ // This path delegates elements to another parser and so never reaches
351
+ // generateScalarStrictAssertion — `unevaluatedItems` and friends would
352
+ // be lost with it.
353
+ ...generateBackstopAssertion("input", schema, `[${typeName}]`, strictContext(unionCtx))
354
+ ].join("\n") : "";
312
355
  const delegatedBody = (itemParserName) => ` const _parsed = validateArray(input, ${itemParserName});
313
356
  return (_parsed === input ? [..._parsed] : _parsed) as ${typeName};`;
314
357
  const delegated = (itemParserName) => strict ? `export const ${functionName} = (input: unknown): ${typeName} => {
@@ -342,13 +385,22 @@ ${delegated(generateParserName(itemName))}`;
342
385
  }
343
386
  const isMultiType = isSchemaObject(schema) && Array.isArray(schema.type);
344
387
  if (!isSchemaObject(schema) || !hasType(schema) && !isMultiType) {
388
+ if (strict) {
389
+ const assertion = generateScalarStrictAssertion(schema, typeName, strictContext(unionCtx));
390
+ if (assertion !== null) {
391
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
392
+ ${assertion}
393
+ return input as ${typeName};
394
+ };`;
395
+ }
396
+ }
345
397
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
346
398
  }
347
399
  if (!strict && schema.type === "null") {
348
400
  return `export const ${functionName} = (input: unknown): ${typeName} => null as ${typeName};`;
349
401
  }
350
402
  if (strict) {
351
- const assertion = generateScalarStrictAssertion(schema, typeName, unionCtx?.rootSchema);
403
+ const assertion = generateScalarStrictAssertion(schema, typeName, strictContext(unionCtx));
352
404
  if (assertion === null) {
353
405
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
354
406
  }
@@ -385,17 +437,50 @@ ${assertion}
385
437
  return `export const ${functionName} = (input: unknown): ${typeName} => input as ${typeName};`;
386
438
  }
387
439
  };
388
- const strictObjectKeywordPrelude = (schema, typeName) => {
440
+ const strictObjectKeywordPrelude = (schema, typeName, context = {}) => {
389
441
  if (!isSchemaObject(schema))
390
442
  return "";
391
- const lines = generateObjectKeywordChecks("input", schema, `[${typeName}]`, false);
443
+ const lines = [
444
+ ...generateObjectKeywordChecks("input", schema, `[${typeName}]`, false, context),
445
+ // `required` and the size bounds are the object-level constraints these
446
+ // specialized parsers never had a place for: `{ type: 'object', required:
447
+ // ['a'] }` and `{ type: 'object', minProperties: 2 }` carry no `properties`,
448
+ // so they land on the empty-object / record paths, which asserted only
449
+ // `isObject`. Keys that *are* declared are skipped — the per-property
450
+ // assertions of the combined parser already demand them.
451
+ ...requiredWithoutPropertyLines(schema, typeName),
452
+ ...generateObjectSizeChecks("input", schema, `[${typeName}]`, false),
453
+ // Values behind `patternProperties` / a schema-valued `additionalProperties`:
454
+ // these parsers build their result by copying or coercing, so nothing
455
+ // rejected a wrongly-typed pattern-matched value before this.
456
+ ...generateKeyedValueChecks("input", schema, `[${typeName}]`, context),
457
+ // Composition on a property-less object: these parsers delegate nothing, so
458
+ // `{ type: 'object', allOf: [{ $ref: … }] }` had no check at all.
459
+ ...generateCompositionChecks("input", schema, `[${typeName}]`, context),
460
+ // Whatever no specialized check above reaches (`unevaluated*`, a `$ref` with
461
+ // constraining siblings): these parsers never run generateObjectStrictAssertion,
462
+ // so the backstop has to be spliced in here too.
463
+ ...generateBackstopAssertion("input", schema, `[${typeName}]`, context)
464
+ ];
392
465
  return lines.length > 0 ? `
393
466
  ${lines.join("\n")}` : "";
394
467
  };
395
- const generateEmptyObjectParser = (typeName, strict, schema) => {
468
+ const requiredWithoutPropertyLines = (schema, typeName) => {
469
+ if (!isSchemaObject(schema) || !hasRequired(schema))
470
+ return [];
471
+ const declared = hasProperties(schema) ? schema.properties : {};
472
+ const lines = [];
473
+ for (const key of new Set(schema.required)) {
474
+ if (key in declared)
475
+ continue;
476
+ lines.push(` if (${missingCheck("input", key)}) throw new Error(${quoteJsString(`[${typeName}] missing required property '${key}'`)});`);
477
+ }
478
+ return lines;
479
+ };
480
+ const generateEmptyObjectParser = (typeName, strict, schema, context = {}) => {
396
481
  const functionName = generateParserName(typeName);
397
482
  if (strict) {
398
- const prelude = schema !== void 0 ? strictObjectKeywordPrelude(schema, typeName) : "";
483
+ const prelude = schema !== void 0 ? strictObjectKeywordPrelude(schema, typeName, context) : "";
399
484
  return `export const ${functionName} = (input: unknown): ${typeName} => {
400
485
  if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${prelude}
401
486
  return { ...input } as ${typeName};
@@ -476,7 +561,7 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
476
561
  if (!hasProperties(schema)) {
477
562
  if (strict) {
478
563
  return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {
479
- if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${strictObjectKeywordPrelude(schema, typeName)}
564
+ if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);${strictObjectKeywordPrelude(schema, typeName, { useRefImports, suffix, stripUnknown, ...rootSchema !== void 0 ? { rootSchema } : {} })}
480
565
  return input as ${typeName};
481
566
  };`;
482
567
  }
@@ -566,6 +651,16 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
566
651
  fastPathAccessorChecks.push(`(${accessor} === undefined || ${accessorCheck})`);
567
652
  }
568
653
  }
654
+ if (canFastPath) {
655
+ if (hasMinProperties(schema)) {
656
+ fastPathChecks.push(`Object.keys(input).length >= ${schema.minProperties}`);
657
+ fastPathAccessorChecks.push(`Object.keys(input).length >= ${schema.minProperties}`);
658
+ }
659
+ if (hasMaxProperties(schema)) {
660
+ fastPathChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
661
+ fastPathAccessorChecks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
662
+ }
663
+ }
569
664
  if (stripKeys) {
570
665
  fastPathChecks.push(exactKeyCount !== null ? `Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${exactKeyCount}` : `_hasOnlyKnownKeys${typeName}(input)`);
571
666
  }
@@ -700,7 +795,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
700
795
  continue;
701
796
  }
702
797
  if (!isSchemaObject(propSchema)) {
703
- if (isRequired) {
798
+ if (propSchema === true) {
799
+ if (isRequired) {
800
+ objectLines.push(` ${safeLiteralKey(key)}: ${accessor},`);
801
+ } else if (stripKeys) {
802
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeLiteralKey(key)}: ${accessor} }),`);
803
+ }
804
+ } else if (isRequired) {
704
805
  objectLines.push(` ${safeLiteralKey(key)}: undefined,`);
705
806
  }
706
807
  continue;
@@ -739,8 +840,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
739
840
  const fields = [];
740
841
  for (const { key, varName, isRequired, propSchema } of propInfo) {
741
842
  if (!isSchemaObject(propSchema)) {
742
- if (isRequired)
843
+ if (propSchema === true) {
844
+ const acc = safeAccessor("input", key);
845
+ fields.push(isRequired ? ` ${safeLiteralKey(key)}: ${acc},` : ` ...(${acc} !== undefined && { ${safeLiteralKey(key)}: ${acc} }),`);
846
+ } else if (isRequired) {
743
847
  fields.push(` ${safeLiteralKey(key)}: undefined,`);
848
+ }
744
849
  continue;
745
850
  }
746
851
  const accessor = shouldCacheVariable(propSchema, canFastPath, useRefImports) ? varName : safeAccessor("input", key);
@@ -826,19 +931,19 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
826
931
  ${fn}`;
827
932
  };
828
933
  const safeResultAssign = (keyVar, valueExpr) => `if (${keyVar} === "__proto__") { Object.defineProperty(result, ${keyVar}, { value: ${valueExpr}, writable: true, enumerable: true, configurable: true }); } else { (result as Record<string, unknown>)[${keyVar}] = ${valueExpr}; }`;
829
- const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
934
+ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict, context = {}) => {
830
935
  const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
831
936
  const knownKeyCheck = unknownKeyCheck(declaredKeys, "_knownKeys");
832
937
  const refPattern = useRefImports ? patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps)) : void 0;
833
938
  const loopLines = [` if (${knownKeyCheck.isKnown("key")}) continue;`];
834
939
  if (refPattern) {
835
940
  const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
836
- loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
941
+ loopLines.push(` if (${regexLiteral(refPattern[0])}.test(key)) {`);
837
942
  loopLines.push(` ${safeResultAssign("key", `${parserName}(input[key])`)}`);
838
943
  loopLines.push(` continue;`);
839
944
  loopLines.push(` }`);
840
945
  }
841
- const keepConditions = patterns.filter(([p]) => !(refPattern && p === refPattern[0])).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
946
+ const keepConditions = patterns.filter(([p]) => !(refPattern && p === refPattern[0])).map(([p]) => `${regexLiteral(p)}.test(key)`);
842
947
  if (keepConditions.length > 0) {
843
948
  loopLines.push(` if (${keepConditions.join(" || ")}) {`);
844
949
  loopLines.push(` ${safeResultAssign("key", "input[key]")}`);
@@ -853,7 +958,7 @@ ${propertyLines.join("\n")}
853
958
  }` : "{}";
854
959
  const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};
855
960
  `).join("");
856
- const assertionPrelude = strict ? generateObjectStrictAssertion(schema, typeName, { useRefImports, suffix }).slice(1) : [];
961
+ const assertionPrelude = strict ? generateObjectStrictAssertion(schema, typeName, context).slice(1) : [];
857
962
  const keywordPrelude = assertionPrelude.length > 0 ? `
858
963
  ${assertionPrelude.join("\n")}` : "";
859
964
  return `export const ${functionName} = (input: unknown): ${typeName} => {
@@ -867,7 +972,7 @@ ${loopLines.join("\n")}
867
972
  return result;
868
973
  };`;
869
974
  };
870
- const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false) => {
975
+ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown = false, caseInsensitive = false, context = {}) => {
871
976
  const functionName = generateParserName(typeName);
872
977
  const entries = generatePropertyEntries(schema, useRefImports, suffix, caseInsensitive);
873
978
  const propertyLines = entries.map((entry) => {
@@ -877,26 +982,26 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
877
982
  return ` ${safeLiteralKey(entry.key)}: ${entry.value},`;
878
983
  });
879
984
  if (!isSchemaObject(schema) || !("patternProperties" in schema)) {
880
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
985
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
881
986
  }
882
987
  const patternProps = schema.patternProperties;
883
988
  const patterns = Object.entries(patternProps);
884
989
  const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps));
885
990
  const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
886
991
  if (strictKeys) {
887
- return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
992
+ return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict, context);
888
993
  }
889
994
  if (stripUnknown && !strict) {
890
- return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false);
995
+ return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, false, context);
891
996
  }
892
997
  if (!refPattern || !useRefImports) {
893
- return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
998
+ return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
894
999
  }
895
1000
  const [pattern, patternSchema] = refPattern;
896
1001
  const ref = patternSchema.$ref;
897
1002
  const parserName = generateParserName(refToName(ref, suffix));
898
1003
  const assignmentCode = `(result as Record<string, unknown>)[key] = ${parserName}(value);`;
899
- const escapedPattern = escapeRegexPattern(pattern);
1004
+ const patternLiteral = regexLiteral(pattern);
900
1005
  const inputSpread = " ...input,";
901
1006
  let objectProperties = inputSpread;
902
1007
  if (propertyLines.length > 0) {
@@ -905,7 +1010,7 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
905
1010
  }
906
1011
  }
907
1012
  const notObjectBranch = strict ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);` : ` return {} as unknown as ${typeName};`;
908
- const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : "";
1013
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName, context) : "";
909
1014
  return `export const ${functionName} = (input: unknown): ${typeName} => {
910
1015
  if (!isObject(input)) {
911
1016
  ${notObjectBranch}
@@ -914,7 +1019,7 @@ ${notObjectBranch}
914
1019
  ${objectProperties}
915
1020
  } as unknown as ${typeName};
916
1021
  for (const key in input) {
917
- if (/${escapedPattern}/.test(key)) {
1022
+ if (${patternLiteral}.test(key)) {
918
1023
  const value = input[key];
919
1024
  ${assignmentCode}
920
1025
  }
@@ -922,13 +1027,13 @@ ${objectProperties}
922
1027
  return result;
923
1028
  };`;
924
1029
  };
925
- const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1030
+ const generateAdditionalPropertiesParser = (schema, typeName, useRefImports, suffix, strict, context = {}) => {
926
1031
  const functionName = generateParserName(typeName);
927
1032
  const additionalProps = schema.additionalProperties;
928
1033
  if (!additionalProps) {
929
- return generateEmptyObjectParser(typeName, strict, schema);
1034
+ return generateEmptyObjectParser(typeName, strict, schema, context);
930
1035
  }
931
- const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : "";
1036
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName, context) : "";
932
1037
  if (useRefImports && isSchemaObject(additionalProps) && hasRef(additionalProps)) {
933
1038
  const ref = additionalProps.$ref;
934
1039
  const parserName = generateParserName(refToName(ref, suffix));
@@ -996,15 +1101,15 @@ const generateInlineValueParser = (schema, strict = false, label = "") => {
996
1101
  return null;
997
1102
  }
998
1103
  };
999
- const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict) => {
1104
+ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix, strict, context = {}) => {
1000
1105
  const functionName = generateParserName(typeName);
1001
1106
  if (!("patternProperties" in schema) || typeof schema.patternProperties !== "object") {
1002
- return generateEmptyObjectParser(typeName, strict, schema);
1107
+ return generateEmptyObjectParser(typeName, strict, schema, context);
1003
1108
  }
1004
1109
  const patternProps = schema.patternProperties;
1005
1110
  const patterns = Object.entries(patternProps);
1006
1111
  if (patterns.length === 0) {
1007
- return generateEmptyObjectParser(typeName, strict, schema);
1112
+ return generateEmptyObjectParser(typeName, strict, schema, context);
1008
1113
  }
1009
1114
  const [pattern, patternSchema] = patterns[0];
1010
1115
  let patternAssignment = "(result as Record<string, unknown>)[key] = value;";
@@ -1018,19 +1123,19 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
1018
1123
  patternAssignment = "";
1019
1124
  patternValueExpr = null;
1020
1125
  }
1021
- const escapedPattern = escapeRegexPattern(pattern);
1126
+ const patternLiteral = regexLiteral(pattern);
1022
1127
  const notObjectBranch = strict ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);` : ` return {} as unknown as ${typeName};`;
1023
- const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName) : "";
1128
+ const keywordPrelude = strict ? strictObjectKeywordPrelude(schema, typeName, context) : "";
1024
1129
  const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
1025
1130
  if (strictKeys) {
1026
- const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
1131
+ const loopLines = [` if (${patternLiteral}.test(key)) {`];
1027
1132
  if (patternValueExpr) {
1028
1133
  loopLines.push(` const value = input[key];`);
1029
1134
  loopLines.push(` ${safeResultAssign("key", patternValueExpr)}`);
1030
1135
  }
1031
1136
  loopLines.push(` continue;`);
1032
1137
  loopLines.push(` }`);
1033
- const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
1138
+ const keepConditions = patterns.slice(1).map(([p]) => `${regexLiteral(p)}.test(key)`);
1034
1139
  if (keepConditions.length > 0) {
1035
1140
  loopLines.push(` if (${keepConditions.join(" || ")}) {`);
1036
1141
  loopLines.push(` ${safeResultAssign("key", "input[key]")}`);
@@ -1058,7 +1163,7 @@ ${notObjectBranch}
1058
1163
  ...input,
1059
1164
  } as unknown as ${typeName};
1060
1165
  for (const key in input) {
1061
- if (/${escapedPattern}/.test(key)) {
1166
+ if (${patternLiteral}.test(key)) {
1062
1167
  const value = input[key];
1063
1168
  ${patternAssignment}
1064
1169
  }
@@ -1169,6 +1274,12 @@ const selectParserStrategy = (schema, typeName, options) => {
1169
1274
  const stripUnknown = options?.stripUnknown ?? false;
1170
1275
  const caseInsensitive = options?.caseInsensitive ?? false;
1171
1276
  const suffix = options?.typeSuffix ?? "";
1277
+ const context = {
1278
+ useRefImports,
1279
+ suffix,
1280
+ stripUnknown,
1281
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {}
1282
+ };
1172
1283
  if (!options?.isRoot && typeName === `Schema${suffix}`) {
1173
1284
  return generateSchemaObjectParser(typeName);
1174
1285
  }
@@ -1185,17 +1296,39 @@ const selectParserStrategy = (schema, typeName, options) => {
1185
1296
  });
1186
1297
  }
1187
1298
  if (hasProperties(schema) && isSchemaObject(schema) && "patternProperties" in schema) {
1188
- return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive);
1299
+ return generateCombinedObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, stripUnknown, caseInsensitive, context);
1300
+ }
1301
+ if (strict && isSchemaObject(schema) && Array.isArray(schema.type) && schema.type.includes("null")) {
1302
+ return generateNonObjectParser(typeName, schema, strict, {
1303
+ useRefImports,
1304
+ suffix,
1305
+ stripUnknown,
1306
+ caseInsensitive,
1307
+ logWarnings,
1308
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1309
+ ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1310
+ });
1189
1311
  }
1190
1312
  if (hasProperties(schema)) {
1191
1313
  return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, options?.rootSchema, options?.reservedNames, caseInsensitive, options?.shapeValidatorSource);
1192
1314
  }
1315
+ if (strict && isSchemaObject(schema) && ("if" in schema || "then" in schema || "else" in schema)) {
1316
+ return generateNonObjectParser(typeName, schema, strict, {
1317
+ useRefImports,
1318
+ suffix,
1319
+ stripUnknown,
1320
+ caseInsensitive,
1321
+ logWarnings,
1322
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1323
+ ...options?.reservedNames !== void 0 ? { reservedNames: options.reservedNames } : {}
1324
+ });
1325
+ }
1193
1326
  if (isSchemaObject(schema) && "if" in schema && "then" in schema && "else" in schema) {
1194
1327
  return generateConditionalParser(schema, typeName, suffix, stripUnknown, caseInsensitive);
1195
1328
  }
1196
1329
  const conditionalObjectSchema = getConditionalObjectSchema(schema);
1197
1330
  if (conditionalObjectSchema) {
1198
- return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, void 0, NO_RESERVED_NAMES, caseInsensitive);
1331
+ return generateObjectParser(conditionalObjectSchema, typeName, useRefImports, suffix, logWarnings, strict, true, stripUnknown, context.rootSchema, NO_RESERVED_NAMES, caseInsensitive);
1199
1332
  }
1200
1333
  if (!isObjectLikeSchema) {
1201
1334
  return generateNonObjectParser(typeName, schema, strict, {
@@ -1209,23 +1342,30 @@ const selectParserStrategy = (schema, typeName, options) => {
1209
1342
  });
1210
1343
  }
1211
1344
  if ("patternProperties" in schema) {
1212
- return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1345
+ return generatePatternPropertiesParser(schema, typeName, useRefImports, suffix, strict, context);
1213
1346
  }
1214
1347
  if ("additionalProperties" in schema && !hasProperties(schema)) {
1215
1348
  const additionalProps = schema.additionalProperties;
1216
1349
  if (additionalProps === true || additionalProps === false) {
1217
- return generateEmptyObjectParser(typeName, strict, schema);
1350
+ return generateEmptyObjectParser(typeName, strict, schema, context);
1218
1351
  }
1219
- return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict);
1352
+ return generateAdditionalPropertiesParser(schema, typeName, useRefImports, suffix, strict, context);
1220
1353
  }
1221
1354
  if ("type" in schema && schema.type === "object" && !hasProperties(schema)) {
1222
- return generateEmptyObjectParser(typeName, strict, schema);
1355
+ return generateEmptyObjectParser(typeName, strict, schema, context);
1223
1356
  }
1224
- return generateEmptyObjectParser(typeName, strict, schema);
1357
+ return generateEmptyObjectParser(typeName, strict, schema, context);
1225
1358
  };
1226
- const generateParserFunction = (schema, typeName, options) => {
1227
- if (options?.strict)
1228
- assertNoUnsupportedKeywords(schema, typeName);
1359
+ const generateParserFunction = (schema, typeName, rawOptions) => {
1360
+ const options = rawOptions?.strict && rawOptions.rootSchema === void 0 && isSchemaObject(schema) ? { ...rawOptions, rootSchema: schema } : rawOptions;
1361
+ if (options?.strict) {
1362
+ assertNoUnsupportedKeywords(schema, typeName, {
1363
+ useRefImports: options.useRefImports ?? false,
1364
+ suffix: options.typeSuffix ?? "",
1365
+ ...options.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
1366
+ ...options.stripUnknown !== void 0 ? { stripUnknown: options.stripUnknown } : {}
1367
+ });
1368
+ }
1229
1369
  return selectParserStrategy(schema, typeName, options);
1230
1370
  };
1231
1371
  const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeName);
@@ -1247,7 +1387,7 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
1247
1387
  if (!hasProperties(schema) && !("patternProperties" in schema) && !("if" in schema) && !stripUnknown) {
1248
1388
  const branches = getUnionBranches(schema);
1249
1389
  if (branches) {
1250
- const check = generateUnionCheck("input", branches, useRefImports, suffix);
1390
+ const check = generateUnionCheck("input", branches, useRefImports, suffix, isExclusiveUnion(schema));
1251
1391
  if (check !== null) {
1252
1392
  return `${exportPrefix}const ${fnName} = (input: unknown): boolean => ${check};`;
1253
1393
  }
@@ -1289,6 +1429,10 @@ const generateShapeValidator = (schema, typeName, useRefImports, suffix = "", ex
1289
1429
  checks.push(`(${accessor} === undefined || ${check})`);
1290
1430
  }
1291
1431
  }
1432
+ if (hasMinProperties(schema))
1433
+ checks.push(`Object.keys(input).length >= ${schema.minProperties}`);
1434
+ if (hasMaxProperties(schema))
1435
+ checks.push(`Object.keys(input).length <= ${schema.maxProperties}`);
1292
1436
  if (validatorKeyCount !== null) {
1293
1437
  checks.push(`Object.getPrototypeOf(input) === Object.prototype && Object.keys(input).length === ${validatorKeyCount}`);
1294
1438
  }
@@ -1,8 +1,10 @@
1
- import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
3
3
  import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
+ import { hasOwnCheck } from "@amritk/helpers/safe-accessor";
4
5
  import { hasAdditionalProperties, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
5
6
  import { generateEnumCheck } from "./generate-enum-check.js";
7
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
6
8
  const OBJECT_KEYWORDS = [
7
9
  "properties",
8
10
  "required",
@@ -61,7 +63,7 @@ const generateInferredChecks = (accessor, schema, type) => {
61
63
  checks.push(`typeof ${accessor} === "object" && ${accessor} !== null && !Array.isArray(${accessor})`);
62
64
  if (hasRequired(schema) && schema.required.length > 0) {
63
65
  for (const requiredKey of schema.required) {
64
- checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
66
+ checks.push(hasOwnCheck(accessor, requiredKey));
65
67
  }
66
68
  }
67
69
  if (hasMinProperties(schema)) {
@@ -84,13 +86,13 @@ const generateInferredChecks = (accessor, schema, type) => {
84
86
  checks.push(`${accessor}.length <= ${schema.maxItems}`);
85
87
  }
86
88
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
87
- checks.push(`new Set(${accessor}).size === ${accessor}.length`);
89
+ checks.push(generateUniqueItemsCheck(accessor, schema));
88
90
  }
89
91
  break;
90
92
  case "string":
91
93
  checks.push(`typeof ${accessor} === "string"`);
92
94
  if (hasPattern(schema)) {
93
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${accessor})`);
95
+ checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
94
96
  }
95
97
  if (hasMinLength(schema)) {
96
98
  checks.push(`${accessor}.length >= ${schema.minLength}`);
@@ -145,7 +147,7 @@ const generateSchemaChecks = (accessor, schema) => {
145
147
  }
146
148
  checks.push(...generateInferredChecks(accessor, schema, inferred));
147
149
  if (hasEnum(schema) && schema.enum.length > 0) {
148
- checks.push(`${JSON.stringify(schema.enum)}.includes(${accessor} as never)`);
150
+ checks.push(generateEnumCheck(accessor, schema.enum));
149
151
  }
150
152
  return checks;
151
153
  }
@@ -153,7 +155,7 @@ const generateSchemaChecks = (accessor, schema) => {
153
155
  case "string": {
154
156
  checks.push(`typeof ${accessor} === "string"`);
155
157
  if (hasPattern(schema)) {
156
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${accessor})`);
158
+ checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
157
159
  }
158
160
  if (hasMinLength(schema)) {
159
161
  checks.push(`${accessor}.length >= ${schema.minLength}`);
@@ -197,14 +199,14 @@ const generateSchemaChecks = (accessor, schema) => {
197
199
  checks.push(`${accessor}.length <= ${schema.maxItems}`);
198
200
  }
199
201
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
200
- checks.push(`new Set(${accessor}).size === ${accessor}.length`);
202
+ checks.push(generateUniqueItemsCheck(accessor, schema));
201
203
  }
202
204
  break;
203
205
  case "object": {
204
206
  checks.push(`typeof ${accessor} === "object" && ${accessor} !== null && !Array.isArray(${accessor})`);
205
207
  if (hasRequired(schema) && schema.required.length > 0) {
206
208
  for (const requiredKey of schema.required) {
207
- checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
209
+ checks.push(hasOwnCheck(accessor, requiredKey));
208
210
  }
209
211
  }
210
212
  if (hasMinProperties(schema)) {