@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.
- package/AI.md +10 -0
- package/README.md +147 -19
- 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 +5 -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-discriminated-union-validation.js +1 -2
- package/dist/generators/generate-enum-check.d.ts +8 -3
- package/dist/generators/generate-enum-check.js +8 -6
- package/dist/generators/generate-files.d.ts +7 -0
- package/dist/generators/generate-files.js +23 -4
- package/dist/generators/generate-parser-function.d.ts +1 -1
- package/dist/generators/generate-parser-function.js +212 -65
- package/dist/generators/generate-schema-checks.js +10 -8
- package/dist/generators/generate-strict-assertion.d.ts +89 -5
- package/dist/generators/generate-strict-assertion.js +429 -58
- package/dist/generators/generate-type-checks.d.ts +46 -10
- package/dist/generators/generate-type-checks.js +112 -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 +29 -4
- package/dist/generators/generate-validation-expression.js +15 -6
- 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/collect-imports.d.ts +8 -0
- package/dist/helpers/collect-imports.js +1 -1
- package/dist/helpers/get-default-value.js +45 -2
- 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),
|
|
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;
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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 {
|
|
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))
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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;
|