@amritk/generate-parsers 0.17.2 → 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.
Files changed (32) hide show
  1. package/AI.md +10 -0
  2. package/README.md +147 -19
  3. package/dist/generators/assert-supported-keywords.d.ts +13 -9
  4. package/dist/generators/assert-supported-keywords.js +58 -18
  5. package/dist/generators/build-schema.d.ts +9 -1
  6. package/dist/generators/build-schema.js +5 -2
  7. package/dist/generators/generate-deep-equal-check.d.ts +24 -0
  8. package/dist/generators/generate-deep-equal-check.js +35 -0
  9. package/dist/generators/generate-discriminated-union-validation.js +1 -2
  10. package/dist/generators/generate-enum-check.d.ts +8 -3
  11. package/dist/generators/generate-enum-check.js +8 -6
  12. package/dist/generators/generate-files.d.ts +7 -0
  13. package/dist/generators/generate-files.js +23 -4
  14. package/dist/generators/generate-parser-function.d.ts +1 -1
  15. package/dist/generators/generate-parser-function.js +212 -65
  16. package/dist/generators/generate-schema-checks.js +10 -8
  17. package/dist/generators/generate-strict-assertion.d.ts +89 -5
  18. package/dist/generators/generate-strict-assertion.js +429 -58
  19. package/dist/generators/generate-type-checks.d.ts +46 -10
  20. package/dist/generators/generate-type-checks.js +112 -16
  21. package/dist/generators/generate-unique-items-check.d.ts +16 -0
  22. package/dist/generators/generate-unique-items-check.js +32 -0
  23. package/dist/generators/generate-validation-expression.d.ts +29 -4
  24. package/dist/generators/generate-validation-expression.js +15 -6
  25. package/dist/generators/subschema-match.d.ts +23 -4
  26. package/dist/generators/subschema-match.js +312 -48
  27. package/dist/generators/unevaluated-match.d.ts +19 -0
  28. package/dist/generators/unevaluated-match.js +206 -0
  29. package/dist/helpers/collect-imports.d.ts +8 -0
  30. package/dist/helpers/collect-imports.js +1 -1
  31. package/dist/helpers/get-default-value.js +45 -2
  32. package/package.json +9 -3
@@ -41,15 +41,43 @@ export declare const isInlineObjectArrayProperty: (propSchema: JSONSchema) => bo
41
41
  * turn into a membership check).
42
42
  */
43
43
  export declare const getUnionBranches: (schema: JSONSchema) => readonly JSONSchema[] | null;
44
+ /**
45
+ * True when {@link getUnionBranches} came from a `oneOf` rather than an `anyOf` —
46
+ * i.e. the value must match *exactly one* branch, not at least one.
47
+ *
48
+ * The distinction was dropped before: both spellings produced a plain
49
+ * disjunction, so `{ oneOf: [{…a}, {…b}] }` accepted `{ a: 'x', b: 1 }`, which
50
+ * matches both branches. Ajv, the runtime interpreter, and `generate-validators`
51
+ * all reject it, so the two mjst engines disagreed on the same contract.
52
+ */
53
+ export declare const isExclusiveUnion: (schema: JSONSchema) => boolean;
44
54
  /**
45
55
  * True when a schema carries a keyword the strict slow path enforces but which
46
56
  * no flat type check can mirror: `contains` (array item counting),
47
- * `dependentRequired` / `dependentSchemas` (cross-property presence), or
48
- * `propertyNames` (per-key constraints). A fast path or shape validator that
49
- * accepted such a value on a bare type check would skip the enforcement, so
50
- * both must bail when this is true — the slow path then runs the real checks.
57
+ * `dependentRequired` / `dependentSchemas` (cross-property presence),
58
+ * `propertyNames` (per-key constraints), the conditional and composition
59
+ * keywords (`if`/`then`/`else`, `not`, a constraining `allOf`), or per-key value
60
+ * schemas (`patternProperties`, a constraining `additionalProperties`). A fast
61
+ * path or shape validator that accepted such a value on a bare type check would
62
+ * skip the enforcement, so both must bail when this is true — the slow path then
63
+ * runs the real checks.
51
64
  */
52
65
  export declare const hasFastPathBlockingKeyword: (schema: JSONSchema) => boolean;
66
+ /**
67
+ * The membership check for an array-form `type` (the multi-type / nullable
68
+ * idiom, e.g. `["string","null"]`): the disjunction of the per-type checks.
69
+ * Returns `undefined` when the schema has no array `type`, and `null` when it
70
+ * has one that cannot be checked soundly — a type name we do not model, or a
71
+ * constraint keyword a bare `typeof` would skip.
72
+ *
73
+ * Without this every nullable property fell through to `hasType`, which is false
74
+ * for an array `type`, and produced no check at all: no fast path (so the shape
75
+ * validator degraded to the `=> false` stub) and, in strict mode, nothing to
76
+ * throw on.
77
+ */
78
+ export declare const multiTypeCheck: (varName: string, schema: JSONSchema, options?: {
79
+ readonly ignoreConstraints?: boolean;
80
+ }) => string | null | undefined;
53
81
  /**
54
82
  * Generates a fast-path type check expression for a property.
55
83
  * Returns null if the schema is too complex for a simple type check.
@@ -68,13 +96,21 @@ export declare const generatePropertyTypeCheck: (varName: string, schema: JSONSc
68
96
  */
69
97
  export declare const generateInlineObjectCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
70
98
  /**
71
- * Generates a union membership check: true when `varName` matches at least one
72
- * branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
73
- * everything else goes through {@link generatePropertyTypeCheck} ($refs call
74
- * the imported shape validator, consts/enums/scalars check directly). Returns
75
- * null when any branch cannot be checked.
99
+ * Generates a union membership check. Inline object branches get a per-property
100
+ * check; everything else goes through {@link generatePropertyTypeCheck} ($refs
101
+ * call the imported shape validator, consts/enums/scalars check directly).
102
+ * Returns null when any branch cannot be checked.
103
+ *
104
+ * `exclusive` (a `oneOf` — see {@link isExclusiveUnion}) counts the matching
105
+ * branches and demands exactly one, instead of the `anyOf` disjunction. The count
106
+ * form is *tighter* than the disjunction it replaces — anything it accepts, the
107
+ * disjunction accepted too — so a true-sound caller (a fast path, a shape
108
+ * validator) can only get safer: a value it now declines merely takes the slow
109
+ * path, which re-validates. A caller that *throws* on a false result needs each
110
+ * branch check to be exact in both directions, which is precisely what
111
+ * {@link canEnforceUnion} certifies.
76
112
  */
77
- export declare const generateUnionCheck: (varName: string, branches: readonly JSONSchema[], useRefImports: boolean, suffix: string) => string | null;
113
+ export declare const generateUnionCheck: (varName: string, branches: readonly JSONSchema[], useRefImports: boolean, suffix: string, exclusive?: boolean) => string | null;
78
114
  /**
79
115
  * True when the union membership check emitted by {@link generateUnionCheck}
80
116
  * is also *false-sound* — a false result definitely means the value is invalid
@@ -1,17 +1,21 @@
1
- import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
3
3
  import { refToName } from "@amritk/helpers/ref-to-name";
4
4
  import { resolveRef } from "@amritk/helpers/resolve-ref";
5
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
5
+ import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
7
  import { generateEnumCheck } from "./generate-enum-check.js";
8
- import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
8
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
9
+ import { everyTailItem, getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck, singleTypeCheck } from "./generate-validation-expression.js";
10
+ import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
9
11
  const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
10
12
  const isInlineObjectProperty = (propSchema) => {
11
13
  if (!isSchemaObject(propSchema))
12
14
  return false;
13
15
  if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema))
14
16
  return false;
17
+ if (Array.isArray(propSchema.type))
18
+ return false;
15
19
  if (hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema))
16
20
  return false;
17
21
  if ("patternProperties" in propSchema || "not" in propSchema)
@@ -42,19 +46,74 @@ const getUnionBranches = (schema) => {
42
46
  return schema.anyOf;
43
47
  return null;
44
48
  };
49
+ const isExclusiveUnion = (schema) => isSchemaObject(schema) && !hasAllOf(schema) && !("not" in schema) && hasOneOf(schema) && schema.oneOf.length > 0;
45
50
  const hasFastPathBlockingKeyword = (schema) => {
46
51
  if (!isSchemaObject(schema))
47
52
  return false;
48
53
  const record = schema;
49
- return "contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record;
54
+ return "contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record || // Composition and conditionals: the strict assertions now enforce these, but
55
+ // a guard built from per-property type checks cannot mirror them — it said
56
+ // "already in shape" for `{ kind: 'a' }` under `if: {kind: 'a'}, then:
57
+ // {required: ['n']}` and returned before the conditional was ever tested.
58
+ "if" in record || "then" in record || "else" in record || "not" in record || "patternProperties" in record || // `unevaluated*` is answered against the whole node's annotation coverage,
59
+ // which a per-property guard has no way to reproduce.
60
+ "unevaluatedProperties" in record && record["unevaluatedProperties"] !== true || "unevaluatedItems" in record && record["unevaluatedItems"] !== true || // A `$ref` guard calls the target's shape validator, which knows nothing
61
+ // about the siblings 2020-12 still applies: `{ $ref: …, minProperties: 2 }`
62
+ // was waved through on the target's shape alone.
63
+ hasConstrainingRefSibling(schema) || hasConstrainingAllOf(schema) || hasConstrainingAdditionalProperties(schema);
64
+ };
65
+ const hasConstrainingAllOf = (schema) => hasAllOf(schema) && schema.allOf.some((member) => !(isSchemaObject(member) && hasRef(member)));
66
+ const hasConstrainingAdditionalProperties = (schema) => {
67
+ if (!hasAdditionalProperties(schema))
68
+ return false;
69
+ const additional = schema.additionalProperties;
70
+ if (typeof additional === "boolean")
71
+ return false;
72
+ return subschemaMatchExpr("_x", additional) !== "true";
73
+ };
74
+ const CONSTRAINT_KEYWORDS = [
75
+ "pattern",
76
+ "minLength",
77
+ "maxLength",
78
+ "minimum",
79
+ "maximum",
80
+ "exclusiveMinimum",
81
+ "exclusiveMaximum",
82
+ "multipleOf",
83
+ "minItems",
84
+ "maxItems",
85
+ "uniqueItems",
86
+ "items",
87
+ "prefixItems",
88
+ "properties",
89
+ "required",
90
+ "additionalProperties",
91
+ "patternProperties",
92
+ "minProperties",
93
+ "maxProperties"
94
+ ];
95
+ const multiTypeCheck = (varName, schema, options = {}) => {
96
+ if (!isSchemaObject(schema) || !Array.isArray(schema.type))
97
+ return void 0;
98
+ const record = schema;
99
+ if (!options.ignoreConstraints && CONSTRAINT_KEYWORDS.some((keyword) => keyword in record))
100
+ return null;
101
+ const checks = [];
102
+ for (const type of schema.type) {
103
+ const check = singleTypeCheck(varName, type);
104
+ if (check === null)
105
+ return null;
106
+ checks.push(`(${check})`);
107
+ }
108
+ if (checks.length === 0)
109
+ return null;
110
+ return checks.length === 1 ? checks[0] : `(${checks.join(" || ")})`;
50
111
  };
51
112
  const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
52
113
  if (!isSchemaObject(schema))
53
114
  return null;
54
- const record = schema;
55
- if ("contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record) {
115
+ if (hasFastPathBlockingKeyword(schema))
56
116
  return null;
57
- }
58
117
  if (useRefImports && hasRef(schema)) {
59
118
  const refName = refToName(schema.$ref, suffix);
60
119
  return `${shapeValidatorName(refName)}(${varName})`;
@@ -66,7 +125,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
66
125
  }
67
126
  const branches = getUnionBranches(schema);
68
127
  if (branches) {
69
- return generateUnionCheck(varName, branches, useRefImports, suffix);
128
+ return generateUnionCheck(varName, branches, useRefImports, suffix, isExclusiveUnion(schema));
70
129
  }
71
130
  if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema) {
72
131
  return null;
@@ -75,6 +134,9 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
75
134
  const c = schema.const;
76
135
  return c === null || typeof c !== "object" ? `${varName} === ${JSON.stringify(c)}` : null;
77
136
  }
137
+ const multiType = multiTypeCheck(varName, schema);
138
+ if (multiType !== void 0)
139
+ return multiType;
78
140
  if (!hasType(schema))
79
141
  return null;
80
142
  if (useRefImports && schema.type === "array" && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
@@ -86,7 +148,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
86
148
  case "string": {
87
149
  checks.push(`typeof ${varName} === "string"`);
88
150
  if (hasPattern(schema))
89
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
151
+ checks.push(`${regexLiteral(schema.pattern)}.test(${varName})`);
90
152
  if (hasMinLength(schema))
91
153
  checks.push(`${varName}.length >= ${schema.minLength}`);
92
154
  if (hasMaxLength(schema))
@@ -120,13 +182,20 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
120
182
  if (hasMaxItems(schema))
121
183
  checks.push(`${varName}.length <= ${schema.maxItems}`);
122
184
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
123
- checks.push(`new Set(${varName}).size === ${varName}.length`);
185
+ checks.push(generateUniqueItemsCheck(varName, schema));
124
186
  }
125
187
  if (hasItems(schema) && !Array.isArray(schema.items)) {
126
188
  const items = schema.items;
127
189
  const itemCheck = scalarItemTypeCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
128
- if (itemCheck)
129
- checks.push(`${varName}.every((_it) => ${itemCheck})`);
190
+ if (itemCheck) {
191
+ checks.push(everyTailItem(varName, itemCheck, schema));
192
+ } else if (!(isSchemaObject(items) && (hasRef(items) || isInlineObjectProperty(items)))) {
193
+ const match = subschemaMatchExpr("_it", items);
194
+ if (match === null)
195
+ return null;
196
+ if (match !== "true")
197
+ checks.push(everyTailItem(varName, match, schema));
198
+ }
130
199
  }
131
200
  const prefix = getPrefixItems(schema);
132
201
  if (prefix) {
@@ -143,6 +212,10 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
143
212
  }
144
213
  case "object":
145
214
  checks.push(`isObject(${varName})`);
215
+ if (hasMinProperties(schema))
216
+ checks.push(`Object.keys(${varName}).length >= ${schema.minProperties}`);
217
+ if (hasMaxProperties(schema))
218
+ checks.push(`Object.keys(${varName}).length <= ${schema.maxProperties}`);
146
219
  break;
147
220
  default:
148
221
  return null;
@@ -175,15 +248,19 @@ const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
175
248
  required.delete(key);
176
249
  }
177
250
  for (const key of required) {
178
- checks.push(`${JSON.stringify(key)} in ${varName}`);
251
+ checks.push(hasOwnCheck(varName, key));
179
252
  }
180
253
  if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
181
254
  const allowedKeys = JSON.stringify(Object.keys(schema.properties));
182
255
  checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
183
256
  }
257
+ if (hasMinProperties(schema))
258
+ checks.push(`Object.keys(${varName}).length >= ${schema.minProperties}`);
259
+ if (hasMaxProperties(schema))
260
+ checks.push(`Object.keys(${varName}).length <= ${schema.maxProperties}`);
184
261
  return checks.join(" && ");
185
262
  };
186
- const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
263
+ const generateUnionCheck = (varName, branches, useRefImports, suffix, exclusive = false) => {
187
264
  if (branches.length === 0)
188
265
  return null;
189
266
  const parts = [];
@@ -195,7 +272,11 @@ const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
195
272
  return null;
196
273
  parts.push(`(${check})`);
197
274
  }
198
- return parts.length === 1 ? parts[0] : `(${parts.join(" || ")})`;
275
+ if (parts.length === 1)
276
+ return parts[0];
277
+ if (exclusive)
278
+ return `((${parts.map((part) => `(${part} ? 1 : 0)`).join(" + ")}) === 1)`;
279
+ return `(${parts.join(" || ")})`;
199
280
  };
200
281
  const canEnforceUnion = (branches, rootSchema) => {
201
282
  const visiting = /* @__PURE__ */ new Set();
@@ -225,6 +306,9 @@ const canTrustPropertyCheck = (schema, rootSchema, visiting) => {
225
306
  return false;
226
307
  if (hasConst(schema))
227
308
  return schema.const === null || typeof schema.const !== "object";
309
+ const multiType = multiTypeCheck("_x", schema);
310
+ if (multiType !== void 0)
311
+ return multiType !== null;
228
312
  if (!hasType(schema))
229
313
  return false;
230
314
  if (schema.type === "array" && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
@@ -256,6 +340,13 @@ const canTrustShapeProperty = (propSchema, rootSchema, visiting) => {
256
340
  }
257
341
  return canTrustPropertyCheck(propSchema, rootSchema, visiting);
258
342
  };
343
+ const KEYWORDS_INLINE_CHECK_IGNORES = [
344
+ "propertyNames",
345
+ "dependentRequired",
346
+ "dependentSchemas",
347
+ "contains",
348
+ "unevaluatedProperties"
349
+ ];
259
350
  const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
260
351
  if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
261
352
  return false;
@@ -265,6 +356,9 @@ const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
265
356
  return false;
266
357
  if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== "boolean")
267
358
  return false;
359
+ const record = schema;
360
+ if (KEYWORDS_INLINE_CHECK_IGNORES.some((keyword) => keyword in record))
361
+ return false;
268
362
  return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
269
363
  };
270
364
  const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
@@ -307,7 +401,9 @@ export {
307
401
  generateUnionCheck,
308
402
  getUnionBranches,
309
403
  hasFastPathBlockingKeyword,
404
+ isExclusiveUnion,
310
405
  isInlineObjectArrayProperty,
311
406
  isInlineObjectProperty,
407
+ multiTypeCheck,
312
408
  shapeValidatorName
313
409
  };
@@ -0,0 +1,16 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Builds the boolean expression that is TRUE when every element of `accessor` is
4
+ * distinct — the `uniqueItems: true` contract.
5
+ *
6
+ * Scalar-only arrays take the cheap native `Set` (exact for primitives, and
7
+ * allocation is a single pass). Anything that might hold an object or an array
8
+ * falls back to the canonical-JSON projection above, because JSON Schema compares
9
+ * items by *structural* equality and neither a plain `Set` (reference equality)
10
+ * nor a raw `JSON.stringify` key (order sensitive) answers that question.
11
+ *
12
+ * @param accessor - Expression yielding the array; the caller must already have
13
+ * proven `Array.isArray`.
14
+ * @param schema - The array schema, read for its item types.
15
+ */
16
+ export declare const generateUniqueItemsCheck: (accessor: string, schema: JSONSchema) => string;
@@ -0,0 +1,32 @@
1
+ import { hasType, isSchemaObject } from "@amritk/helpers/schema-guards";
2
+ const SCALAR_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
3
+ const schemaIsScalarOnly = (schema) => {
4
+ if (schema === void 0 || !isSchemaObject(schema) || !hasType(schema))
5
+ return false;
6
+ const type = schema["type"];
7
+ const types = Array.isArray(type) ? type : [type];
8
+ return types.length > 0 && types.every((t) => typeof t === "string" && SCALAR_TYPES.has(t));
9
+ };
10
+ const arrayItemsAreScalarOnly = (schema) => {
11
+ if (!isSchemaObject(schema))
12
+ return false;
13
+ const record = schema;
14
+ const prefix = record["prefixItems"];
15
+ if (Array.isArray(prefix)) {
16
+ if (!prefix.every((position) => schemaIsScalarOnly(position)))
17
+ return false;
18
+ const tail = "items" in record ? record["items"] : record["additionalItems"];
19
+ return tail === false ? true : schemaIsScalarOnly(tail);
20
+ }
21
+ if (Array.isArray(record["items"])) {
22
+ if (!record["items"].every((position) => schemaIsScalarOnly(position)))
23
+ return false;
24
+ return record["additionalItems"] === false ? true : schemaIsScalarOnly(record["additionalItems"]);
25
+ }
26
+ return schemaIsScalarOnly(record["items"]);
27
+ };
28
+ const CANONICAL_JSON = 'JSON.stringify(_u, (_k, _v) => (_v !== null && typeof _v === "object" && !Array.isArray(_v)) ? Object.fromEntries(Object.keys(_v).sort().map((_sk) => [_sk, (_v as Record<string, unknown>)[_sk]])) : _v)';
29
+ const generateUniqueItemsCheck = (accessor, schema) => arrayItemsAreScalarOnly(schema) ? `new Set(${accessor}).size === ${accessor}.length` : `new Set((${accessor} as unknown[]).map((_u) => ${CANONICAL_JSON})).size === ${accessor}.length`;
30
+ export {
31
+ generateUniqueItemsCheck
32
+ };
@@ -14,18 +14,43 @@ export declare const scalarItemTypeCheck: (itemSchema: JSONSchema, accessor: str
14
14
  */
15
15
  export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
16
16
  /**
17
- * The tuple `prefixItems` array when the schema declares one, else `null`.
18
- * `prefixItems` (JSON Schema 2020-12) positions each element against its own
19
- * subschema; the draft array-form `items` is handled by the existing item
20
- * paths, not here.
17
+ * The tuple positions a schema declares, else `null`. `prefixItems` (JSON
18
+ * Schema 2020-12) positions each element against its own subschema; draft-07
19
+ * spelled the same construct as an array-valued `items`, which every other item
20
+ * path skips (they all read `items` as a single schema), so it is normalized
21
+ * here rather than left unchecked.
21
22
  */
22
23
  export declare const getPrefixItems: (schema: JSONSchema) => readonly JSONSchema[] | null;
24
+ /**
25
+ * Wraps a per-element boolean check into the `.every(...)` call that proves it
26
+ * for a whole array, skipping the positions `prefixItems` already owns.
27
+ *
28
+ * In 2020-12 `items` is only the *tail* schema: it applies from
29
+ * `prefixItems.length` onward. Checking every element against it made
30
+ * `{ prefixItems: [{type:'string'}], items: {type:'number'} }` reject
31
+ * `["a", 1, 2]` — a value Ajv accepts, and one the `[string?, ...number[]]` type
32
+ * this generator emits admits, so the strict parser threw on its own declared
33
+ * type. The index guard is short-circuited rather than sliced so the check stays
34
+ * allocation-free on the hot path.
35
+ *
36
+ * The draft-07 tuple spelling needs no guard: there `items` *is* the tuple and
37
+ * the tail lives in `additionalItems`, which no item check reads.
38
+ */
39
+ export declare const everyTailItem: (accessor: string, itemCheck: string, schema: JSONSchema) => string;
23
40
  /**
24
41
  * True when a sibling `items: false` (2020-12) or `additionalItems: false`
25
42
  * (draft) caps a tuple's length — any element past the declared prefix is
26
43
  * disallowed, so the coercer drops it and strict mode rejects it.
27
44
  */
28
45
  export declare const prefixItemsCapsLength: (schema: JSONSchema) => boolean;
46
+ /**
47
+ * A boolean type check for a single JSON Schema primitive type name. Unlike
48
+ * {@link scalarItemTypeCheck} this also covers `array`/`object` (as inline shape
49
+ * checks, so no `isObject` import is assumed) and enforces `integer` with
50
+ * `Number.isInteger` — a bare `typeof === "number"` accepts `1.5`. Used to build
51
+ * the disjunction for an array-form `type` (e.g. `["string","null"]`).
52
+ */
53
+ export declare const singleTypeCheck: (accessor: string, type: string) => string | null;
29
54
  /**
30
55
  * Generates a validation expression that checks if a value matches the expected type.
31
56
  * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
@@ -1,8 +1,8 @@
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
4
  import { resolveRef } from "@amritk/helpers/resolve-ref";
5
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
5
+ import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
6
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
7
7
  import { findDiscriminator } from "../helpers/find-discriminator.js";
8
8
  import { getDefaultValue } from "../helpers/get-default-value.js";
@@ -42,7 +42,14 @@ const getPrefixItems = (schema) => {
42
42
  if (!isSchemaObject(schema))
43
43
  return null;
44
44
  const prefix = schema["prefixItems"];
45
- return Array.isArray(prefix) ? prefix : null;
45
+ if (Array.isArray(prefix))
46
+ return prefix;
47
+ return Array.isArray(schema.items) ? schema.items : null;
48
+ };
49
+ const everyTailItem = (accessor, itemCheck, schema) => {
50
+ const prefix = isSchemaObject(schema) ? schema["prefixItems"] : void 0;
51
+ const tailStart = Array.isArray(prefix) ? prefix.length : 0;
52
+ return tailStart === 0 ? `${accessor}.every((_it) => ${itemCheck})` : `${accessor}.every((_it, _ix) => _ix < ${tailStart} || (${itemCheck}))`;
46
53
  };
47
54
  const prefixItemsCapsLength = (schema) => {
48
55
  if (!isSchemaObject(schema))
@@ -204,7 +211,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
204
211
  case "string": {
205
212
  checks.push(`typeof ${accessor} === "string"`);
206
213
  if (hasPattern(schema)) {
207
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${accessor})`);
214
+ checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
208
215
  }
209
216
  if (hasMinLength(schema)) {
210
217
  checks.push(`${accessor}.length >= ${schema.minLength}`);
@@ -260,7 +267,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
260
267
  checks.push(`isObject(${accessor})`);
261
268
  if (hasRequired(schema) && schema.required.length > 0) {
262
269
  for (const requiredKey of schema.required) {
263
- checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
270
+ checks.push(hasOwnCheck(accessor, requiredKey));
264
271
  }
265
272
  }
266
273
  if (hasMinProperties(schema)) {
@@ -323,10 +330,12 @@ const generatePrefixItemsMap = (accessor, prefix, capLength, rootSchema, visited
323
330
  return capLength ? `${mapped}.slice(0, ${prefix.length})` : mapped;
324
331
  };
325
332
  export {
333
+ everyTailItem,
326
334
  generatePrefixItemsMap,
327
335
  generateValidationExpression,
328
336
  getPrefixItems,
329
337
  isCoercibleItemSchema,
330
338
  prefixItemsCapsLength,
331
- scalarItemTypeCheck
339
+ scalarItemTypeCheck,
340
+ singleTypeCheck
332
341
  };
@@ -1,4 +1,15 @@
1
1
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * What the matcher needs beyond the schema node itself: the root document (to
4
+ * resolve `$ref`), the recursion depth (which seeds collision-free loop variable
5
+ * names and caps expression growth), and the `$ref` pointers already open on this
6
+ * path (a cycle cannot be inlined, so it bails rather than recursing forever).
7
+ */
8
+ export type MatchContext = {
9
+ readonly rootSchema?: Record<string, unknown>;
10
+ readonly depth?: number;
11
+ readonly seen?: ReadonlySet<string>;
12
+ };
2
13
  /**
3
14
  * Builds a boolean expression that is `true` *exactly* when `accessor` matches
4
15
  * `schema` — sound in **both** directions (true ⇒ valid, false ⇒ invalid) — or
@@ -17,13 +28,21 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
17
28
  * types), so a type-less schema guards each family by its runtime type rather
18
29
  * than ignoring it — the subtlety `propertyNames: { maxLength: 3 }` depends on.
19
30
  *
20
- * `depth` seeds the `.every` element variable so nested array matches never
21
- * shadow one another.
31
+ * `ctx.depth` seeds the loop variable names so nested walks never shadow one
32
+ * another, and caps how far the expression may grow.
33
+ */
34
+ export declare const subschemaMatchExpr: (accessor: string, schema: JSONSchema, ctx?: MatchContext) => string | null;
35
+ /**
36
+ * True when a `$ref` node carries a keyword that constrains the value on its own.
37
+ * 2020-12 applies a `$ref`'s siblings alongside the target, so delegating the
38
+ * whole node to the referenced parser (which knows nothing about them) drops
39
+ * every one of them — and a fast path built from the target's shape validator
40
+ * skips them too.
22
41
  */
23
- export declare const subschemaMatchExpr: (accessor: string, schema: JSONSchema, depth?: number) => string | null;
42
+ export declare const hasConstrainingRefSibling: (schema: JSONSchema) => boolean;
24
43
  /**
25
44
  * Whether {@link subschemaMatchExpr} can build an exact matcher for `schema`.
26
45
  * The generation-time guard uses this to decide whether a `contains`,
27
46
  * `propertyNames`, or `dependentSchemas` subschema is enforceable.
28
47
  */
29
- export declare const canMatchSubschema: (schema: JSONSchema) => boolean;
48
+ export declare const canMatchSubschema: (schema: JSONSchema, rootSchema?: Record<string, unknown>) => boolean;