@amritk/generate-parsers 0.18.0 → 0.19.1
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/AI.md +10 -0
- package/README.md +120 -8
- package/dist/generators/assert-supported-keywords.d.ts +13 -9
- package/dist/generators/assert-supported-keywords.js +58 -18
- package/dist/generators/build-schema.d.ts +9 -1
- package/dist/generators/build-schema.js +2 -2
- package/dist/generators/generate-deep-equal-check.d.ts +24 -0
- package/dist/generators/generate-deep-equal-check.js +35 -0
- package/dist/generators/generate-enum-check.d.ts +8 -3
- package/dist/generators/generate-enum-check.js +8 -6
- package/dist/generators/generate-parser-function.d.ts +1 -1
- package/dist/generators/generate-parser-function.js +201 -57
- package/dist/generators/generate-schema-checks.js +10 -8
- package/dist/generators/generate-strict-assertion.d.ts +82 -5
- package/dist/generators/generate-strict-assertion.js +381 -53
- package/dist/generators/generate-type-checks.d.ts +31 -10
- package/dist/generators/generate-type-checks.js +65 -16
- package/dist/generators/generate-unique-items-check.d.ts +16 -0
- package/dist/generators/generate-unique-items-check.js +32 -0
- package/dist/generators/generate-validation-expression.d.ts +16 -0
- package/dist/generators/generate-validation-expression.js +10 -4
- package/dist/generators/subschema-match.d.ts +23 -4
- package/dist/generators/subschema-match.js +312 -48
- package/dist/generators/unevaluated-match.d.ts +19 -0
- package/dist/generators/unevaluated-match.js +206 -0
- package/dist/helpers/get-default-value.js +22 -1
- package/package.json +6 -3
|
@@ -41,13 +41,26 @@ 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),
|
|
48
|
-
* `propertyNames` (per-key constraints)
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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;
|
|
53
66
|
/**
|
|
@@ -83,13 +96,21 @@ export declare const generatePropertyTypeCheck: (varName: string, schema: JSONSc
|
|
|
83
96
|
*/
|
|
84
97
|
export declare const generateInlineObjectCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
|
|
85
98
|
/**
|
|
86
|
-
* Generates a union membership check
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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.
|
|
91
112
|
*/
|
|
92
|
-
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;
|
|
93
114
|
/**
|
|
94
115
|
* True when the union membership check emitted by {@link generateUnionCheck}
|
|
95
116
|
* is also *false-sound* — a false result definitely means the value is invalid
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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))
|
|
@@ -44,11 +46,30 @@ const getUnionBranches = (schema) => {
|
|
|
44
46
|
return schema.anyOf;
|
|
45
47
|
return null;
|
|
46
48
|
};
|
|
49
|
+
const isExclusiveUnion = (schema) => isSchemaObject(schema) && !hasAllOf(schema) && !("not" in schema) && hasOneOf(schema) && schema.oneOf.length > 0;
|
|
47
50
|
const hasFastPathBlockingKeyword = (schema) => {
|
|
48
51
|
if (!isSchemaObject(schema))
|
|
49
52
|
return false;
|
|
50
53
|
const record = schema;
|
|
51
|
-
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";
|
|
52
73
|
};
|
|
53
74
|
const CONSTRAINT_KEYWORDS = [
|
|
54
75
|
"pattern",
|
|
@@ -91,10 +112,8 @@ const multiTypeCheck = (varName, schema, options = {}) => {
|
|
|
91
112
|
const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
92
113
|
if (!isSchemaObject(schema))
|
|
93
114
|
return null;
|
|
94
|
-
|
|
95
|
-
if ("contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record) {
|
|
115
|
+
if (hasFastPathBlockingKeyword(schema))
|
|
96
116
|
return null;
|
|
97
|
-
}
|
|
98
117
|
if (useRefImports && hasRef(schema)) {
|
|
99
118
|
const refName = refToName(schema.$ref, suffix);
|
|
100
119
|
return `${shapeValidatorName(refName)}(${varName})`;
|
|
@@ -106,7 +125,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
106
125
|
}
|
|
107
126
|
const branches = getUnionBranches(schema);
|
|
108
127
|
if (branches) {
|
|
109
|
-
return generateUnionCheck(varName, branches, useRefImports, suffix);
|
|
128
|
+
return generateUnionCheck(varName, branches, useRefImports, suffix, isExclusiveUnion(schema));
|
|
110
129
|
}
|
|
111
130
|
if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema) {
|
|
112
131
|
return null;
|
|
@@ -129,7 +148,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
129
148
|
case "string": {
|
|
130
149
|
checks.push(`typeof ${varName} === "string"`);
|
|
131
150
|
if (hasPattern(schema))
|
|
132
|
-
checks.push(
|
|
151
|
+
checks.push(`${regexLiteral(schema.pattern)}.test(${varName})`);
|
|
133
152
|
if (hasMinLength(schema))
|
|
134
153
|
checks.push(`${varName}.length >= ${schema.minLength}`);
|
|
135
154
|
if (hasMaxLength(schema))
|
|
@@ -163,13 +182,20 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
163
182
|
if (hasMaxItems(schema))
|
|
164
183
|
checks.push(`${varName}.length <= ${schema.maxItems}`);
|
|
165
184
|
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
166
|
-
checks.push(
|
|
185
|
+
checks.push(generateUniqueItemsCheck(varName, schema));
|
|
167
186
|
}
|
|
168
187
|
if (hasItems(schema) && !Array.isArray(schema.items)) {
|
|
169
188
|
const items = schema.items;
|
|
170
189
|
const itemCheck = scalarItemTypeCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
|
|
171
|
-
if (itemCheck)
|
|
172
|
-
checks.push(
|
|
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
|
+
}
|
|
173
199
|
}
|
|
174
200
|
const prefix = getPrefixItems(schema);
|
|
175
201
|
if (prefix) {
|
|
@@ -186,6 +212,10 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
186
212
|
}
|
|
187
213
|
case "object":
|
|
188
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}`);
|
|
189
219
|
break;
|
|
190
220
|
default:
|
|
191
221
|
return null;
|
|
@@ -218,15 +248,19 @@ const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
|
|
|
218
248
|
required.delete(key);
|
|
219
249
|
}
|
|
220
250
|
for (const key of required) {
|
|
221
|
-
checks.push(
|
|
251
|
+
checks.push(hasOwnCheck(varName, key));
|
|
222
252
|
}
|
|
223
253
|
if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
|
|
224
254
|
const allowedKeys = JSON.stringify(Object.keys(schema.properties));
|
|
225
255
|
checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
|
|
226
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}`);
|
|
227
261
|
return checks.join(" && ");
|
|
228
262
|
};
|
|
229
|
-
const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
|
|
263
|
+
const generateUnionCheck = (varName, branches, useRefImports, suffix, exclusive = false) => {
|
|
230
264
|
if (branches.length === 0)
|
|
231
265
|
return null;
|
|
232
266
|
const parts = [];
|
|
@@ -238,7 +272,11 @@ const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
|
|
|
238
272
|
return null;
|
|
239
273
|
parts.push(`(${check})`);
|
|
240
274
|
}
|
|
241
|
-
|
|
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(" || ")})`;
|
|
242
280
|
};
|
|
243
281
|
const canEnforceUnion = (branches, rootSchema) => {
|
|
244
282
|
const visiting = /* @__PURE__ */ new Set();
|
|
@@ -302,6 +340,13 @@ const canTrustShapeProperty = (propSchema, rootSchema, visiting) => {
|
|
|
302
340
|
}
|
|
303
341
|
return canTrustPropertyCheck(propSchema, rootSchema, visiting);
|
|
304
342
|
};
|
|
343
|
+
const KEYWORDS_INLINE_CHECK_IGNORES = [
|
|
344
|
+
"propertyNames",
|
|
345
|
+
"dependentRequired",
|
|
346
|
+
"dependentSchemas",
|
|
347
|
+
"contains",
|
|
348
|
+
"unevaluatedProperties"
|
|
349
|
+
];
|
|
305
350
|
const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
|
|
306
351
|
if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
|
|
307
352
|
return false;
|
|
@@ -311,6 +356,9 @@ const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
|
|
|
311
356
|
return false;
|
|
312
357
|
if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== "boolean")
|
|
313
358
|
return false;
|
|
359
|
+
const record = schema;
|
|
360
|
+
if (KEYWORDS_INLINE_CHECK_IGNORES.some((keyword) => keyword in record))
|
|
361
|
+
return false;
|
|
314
362
|
return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
|
|
315
363
|
};
|
|
316
364
|
const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
|
|
@@ -353,6 +401,7 @@ export {
|
|
|
353
401
|
generateUnionCheck,
|
|
354
402
|
getUnionBranches,
|
|
355
403
|
hasFastPathBlockingKeyword,
|
|
404
|
+
isExclusiveUnion,
|
|
356
405
|
isInlineObjectArrayProperty,
|
|
357
406
|
isInlineObjectProperty,
|
|
358
407
|
multiTypeCheck,
|
|
@@ -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
|
+
};
|
|
@@ -21,6 +21,22 @@ export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
|
|
|
21
21
|
* here rather than left unchecked.
|
|
22
22
|
*/
|
|
23
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;
|
|
24
40
|
/**
|
|
25
41
|
* True when a sibling `items: false` (2020-12) or `additionalItems: false`
|
|
26
42
|
* (draft) caps a tuple's length — any element past the declared prefix is
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
@@ -46,6 +46,11 @@ const getPrefixItems = (schema) => {
|
|
|
46
46
|
return prefix;
|
|
47
47
|
return Array.isArray(schema.items) ? schema.items : null;
|
|
48
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}))`;
|
|
53
|
+
};
|
|
49
54
|
const prefixItemsCapsLength = (schema) => {
|
|
50
55
|
if (!isSchemaObject(schema))
|
|
51
56
|
return false;
|
|
@@ -206,7 +211,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
|
|
|
206
211
|
case "string": {
|
|
207
212
|
checks.push(`typeof ${accessor} === "string"`);
|
|
208
213
|
if (hasPattern(schema)) {
|
|
209
|
-
checks.push(
|
|
214
|
+
checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
|
|
210
215
|
}
|
|
211
216
|
if (hasMinLength(schema)) {
|
|
212
217
|
checks.push(`${accessor}.length >= ${schema.minLength}`);
|
|
@@ -262,7 +267,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
|
|
|
262
267
|
checks.push(`isObject(${accessor})`);
|
|
263
268
|
if (hasRequired(schema) && schema.required.length > 0) {
|
|
264
269
|
for (const requiredKey of schema.required) {
|
|
265
|
-
checks.push(
|
|
270
|
+
checks.push(hasOwnCheck(accessor, requiredKey));
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
if (hasMinProperties(schema)) {
|
|
@@ -325,6 +330,7 @@ const generatePrefixItemsMap = (accessor, prefix, capLength, rootSchema, visited
|
|
|
325
330
|
return capLength ? `${mapped}.slice(0, ${prefix.length})` : mapped;
|
|
326
331
|
};
|
|
327
332
|
export {
|
|
333
|
+
everyTailItem,
|
|
328
334
|
generatePrefixItemsMap,
|
|
329
335
|
generateValidationExpression,
|
|
330
336
|
getPrefixItems,
|
|
@@ -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
|
|
21
|
-
*
|
|
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
|
|
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;
|