@amritk/generate-validators 0.11.11 → 0.12.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 +8 -0
- package/README.md +127 -24
- package/dist/generators/assert-generatable-refs.d.ts +19 -0
- package/dist/generators/assert-generatable-refs.js +12 -0
- package/dist/generators/assert-unevaluated-generatable.d.ts +30 -0
- package/dist/generators/assert-unevaluated-generatable.js +72 -0
- package/dist/generators/build-schema.d.ts +17 -1
- package/dist/generators/build-schema.js +2 -2
- package/dist/generators/collect-emitted-refs.d.ts +26 -0
- package/dist/generators/collect-emitted-refs.js +62 -0
- package/dist/generators/collect-validator-imports.js +2 -38
- package/dist/generators/generate-files.js +5 -2
- package/dist/generators/generate-validator-function.d.ts +5 -2
- package/dist/generators/generate-validator-function.js +313 -125
- package/dist/generators/unevaluated-match.d.ts +29 -0
- package/dist/generators/unevaluated-match.js +209 -0
- package/package.json +9 -3
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { regexFlagsFor, regexLiteral } from "@amritk/helpers/escape-regex-pattern";
|
|
2
2
|
import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
|
|
3
3
|
import { multipleOfFailExpr, multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
|
|
4
4
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
5
5
|
import { safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
6
6
|
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
7
|
+
import { maxLengthFailExpr, maxLengthPassExpr, minLengthFailExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
|
|
7
8
|
import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
|
|
9
|
+
import { assertGeneratableRefs } from "./assert-generatable-refs.js";
|
|
10
|
+
import { assertUnevaluatedGeneratable, UNPROVABLE_COVERAGE_MESSAGE } from "./assert-unevaluated-generatable.js";
|
|
11
|
+
import { unevaluatedItemsExpr, unevaluatedPropertiesExpr } from "./unevaluated-match.js";
|
|
8
12
|
const validatorName = (typeName) => `validate${typeName}`;
|
|
13
|
+
const propertyRead = (objVar, key) => safeAccessor(objVar, key);
|
|
14
|
+
const PROTOTYPE_MEMBERS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
15
|
+
const protoLocalName = (key, depth) => `_own${depth}_${key}`;
|
|
16
|
+
const hasOwnCheck = (objVar, key) => {
|
|
17
|
+
if (PROTOTYPE_MEMBERS.has(key))
|
|
18
|
+
return `Object.hasOwn(${objVar}, ${JSON.stringify(key)})`;
|
|
19
|
+
return `${JSON.stringify(key)} in ${objVar}`;
|
|
20
|
+
};
|
|
21
|
+
const missingCheck = (objVar, key) => `!(${hasOwnCheck(objVar, key)})`;
|
|
9
22
|
const typeofString = (type) => {
|
|
10
23
|
if (type === "integer")
|
|
11
24
|
return "number";
|
|
@@ -17,6 +30,19 @@ const constMismatchCondition = (accessor, value) => {
|
|
|
17
30
|
}
|
|
18
31
|
return `!valuesEqual(${accessor}, ${JSON.stringify(value)})`;
|
|
19
32
|
};
|
|
33
|
+
const enumMembershipExpr = (values, acc) => {
|
|
34
|
+
if (values.length === 0)
|
|
35
|
+
return "false";
|
|
36
|
+
const parts = values.map((value) => {
|
|
37
|
+
if (typeof value === "number" && Number.isNaN(value))
|
|
38
|
+
return `Number.isNaN(${acc})`;
|
|
39
|
+
if (value !== null && typeof value === "object")
|
|
40
|
+
return `valuesEqual(${acc}, ${JSON.stringify(value)})`;
|
|
41
|
+
return `${acc} === ${JSON.stringify(value)}`;
|
|
42
|
+
});
|
|
43
|
+
return `(${parts.join(" || ")})`;
|
|
44
|
+
};
|
|
45
|
+
const FALSE_SCHEMA_MESSAGE = "boolean schema is false";
|
|
20
46
|
const SCALAR_ITEM_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
|
|
21
47
|
const schemaIsScalarOnly = (schema) => {
|
|
22
48
|
if (!isSchemaObject(schema))
|
|
@@ -59,12 +85,19 @@ const wrongTypeCondition = (accessor, type) => {
|
|
|
59
85
|
return "";
|
|
60
86
|
}
|
|
61
87
|
};
|
|
88
|
+
const declaresObjectType = (schema) => hasType(schema) && schema.type === "object";
|
|
62
89
|
const getTypeArray = (schema) => {
|
|
63
90
|
if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
|
|
64
91
|
return null;
|
|
65
92
|
return schema.type;
|
|
66
93
|
};
|
|
67
|
-
const createRootContext = () => ({
|
|
94
|
+
const createRootContext = (rootSchema) => ({
|
|
95
|
+
objVar: "obj",
|
|
96
|
+
pathPrefix: "${_path}",
|
|
97
|
+
depth: 0,
|
|
98
|
+
hoisted: [],
|
|
99
|
+
rootSchema
|
|
100
|
+
});
|
|
68
101
|
const pointerSegment = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1").replace(/[\\`$]/g, "\\$&");
|
|
69
102
|
const patternPropertySources = (schema) => {
|
|
70
103
|
if (!isSchemaObject(schema) || !("patternProperties" in schema))
|
|
@@ -87,7 +120,8 @@ const generateStrictKeyChecks = (schema, ctx) => {
|
|
|
87
120
|
let patternGuard = "";
|
|
88
121
|
if (patterns.length > 0) {
|
|
89
122
|
const patternsName = `_patterns${ctx.hoisted.length}`;
|
|
90
|
-
|
|
123
|
+
const compiled = patterns.map((p) => `new RegExp(${JSON.stringify(p)}, ${JSON.stringify(regexFlagsFor(p))})`);
|
|
124
|
+
ctx.hoisted.push(`const ${patternsName} = [${compiled.join(", ")}]`);
|
|
91
125
|
patternGuard = ` && !${patternsName}.some((re) => re.test(_key${d}))`;
|
|
92
126
|
}
|
|
93
127
|
return [
|
|
@@ -107,25 +141,57 @@ const generateMissingRequiredChecks = (schema, ctx) => {
|
|
|
107
141
|
for (const key of schema.required) {
|
|
108
142
|
if (Object.hasOwn(props, key))
|
|
109
143
|
continue;
|
|
110
|
-
lines.push(` if (
|
|
144
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
111
145
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
112
146
|
lines.push(` }`);
|
|
113
147
|
}
|
|
114
148
|
return lines;
|
|
115
149
|
};
|
|
116
150
|
const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
151
|
+
if (propSchema === false) {
|
|
152
|
+
const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
|
|
153
|
+
const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
154
|
+
if (isRequired) {
|
|
155
|
+
return [
|
|
156
|
+
` if (${missingCheck(ctx.objVar, key)}) {`,
|
|
157
|
+
` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`,
|
|
158
|
+
` } else {`,
|
|
159
|
+
` errors.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`,
|
|
160
|
+
` }`
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
return [
|
|
164
|
+
` if (${hasOwnCheck(ctx.objVar, key)}) {`,
|
|
165
|
+
` errors.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`,
|
|
166
|
+
` }`
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
|
|
170
|
+
const lines = [
|
|
171
|
+
...generatePropertyCheckLines(key, propSchema, isRequired, suffix, ctx),
|
|
172
|
+
// `unevaluated*` is a sibling of everything else the property declares, and
|
|
173
|
+
// several of the branches above return early, so it is appended here rather
|
|
174
|
+
// than emitted inside them. The checks carry their own runtime type guard, so
|
|
175
|
+
// an absent optional property is inert and needs no presence gate.
|
|
176
|
+
...generateUnevaluatedChecks(raw, `\`${ctx.pathPrefix}/${pointerSegment(key)}\``, propSchema, suffix, ctx)
|
|
177
|
+
];
|
|
178
|
+
if (lines.length === 0 || !PROTOTYPE_MEMBERS.has(key))
|
|
179
|
+
return lines;
|
|
180
|
+
return [` const ${protoLocalName(key, ctx.depth)} = ${propertyRead(ctx.objVar, key)}`, ...lines];
|
|
181
|
+
};
|
|
182
|
+
const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) => {
|
|
117
183
|
if (!isSchemaObject(propSchema)) {
|
|
118
184
|
if (isRequired && propSchema === true) {
|
|
119
185
|
const parentPath2 = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
120
186
|
return [
|
|
121
|
-
` if (
|
|
187
|
+
` if (${missingCheck(ctx.objVar, key)}) {`,
|
|
122
188
|
` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath2} })`,
|
|
123
189
|
` }`
|
|
124
190
|
];
|
|
125
191
|
}
|
|
126
192
|
return [];
|
|
127
193
|
}
|
|
128
|
-
const raw =
|
|
194
|
+
const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
|
|
129
195
|
const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
|
|
130
196
|
const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
131
197
|
const lines = [];
|
|
@@ -142,7 +208,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
142
208
|
...siblings
|
|
143
209
|
];
|
|
144
210
|
if (isRequired) {
|
|
145
|
-
lines.push(` if (
|
|
211
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
146
212
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
147
213
|
lines.push(` } else {`);
|
|
148
214
|
lines.push(...delegate);
|
|
@@ -157,7 +223,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
157
223
|
const instanceOf = getMjstInstanceOf(propSchema);
|
|
158
224
|
if (instanceOf) {
|
|
159
225
|
if (isRequired) {
|
|
160
|
-
lines.push(` if (
|
|
226
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
161
227
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
162
228
|
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
|
|
163
229
|
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
@@ -172,7 +238,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
172
238
|
const primitive = getMjstPrimitive(propSchema);
|
|
173
239
|
if (primitive) {
|
|
174
240
|
if (isRequired) {
|
|
175
|
-
lines.push(` if (
|
|
241
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
176
242
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
177
243
|
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
|
|
178
244
|
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
@@ -188,7 +254,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
188
254
|
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
189
255
|
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
190
256
|
if (isRequired) {
|
|
191
|
-
lines.push(` if (
|
|
257
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
192
258
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
193
259
|
lines.push(` } else if (${mismatch}) {`);
|
|
194
260
|
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
@@ -201,16 +267,16 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
201
267
|
return lines;
|
|
202
268
|
}
|
|
203
269
|
if (hasEnum(propSchema)) {
|
|
204
|
-
const
|
|
270
|
+
const member = enumMembershipExpr(propSchema.enum, raw);
|
|
205
271
|
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
206
272
|
if (isRequired) {
|
|
207
|
-
lines.push(` if (
|
|
273
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
208
274
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
209
|
-
lines.push(` } else if (
|
|
275
|
+
lines.push(` } else if (!${member}) {`);
|
|
210
276
|
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
211
277
|
lines.push(` }`);
|
|
212
278
|
} else {
|
|
213
|
-
lines.push(` if (${raw} !== undefined &&
|
|
279
|
+
lines.push(` if (${raw} !== undefined && !${member}) {`);
|
|
214
280
|
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
215
281
|
lines.push(` }`);
|
|
216
282
|
}
|
|
@@ -221,7 +287,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
221
287
|
const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
222
288
|
const label = typeArray.map((t) => typeofString(t)).join(" or ");
|
|
223
289
|
if (isRequired) {
|
|
224
|
-
lines.push(` if (
|
|
290
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
225
291
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
226
292
|
if (allWrong) {
|
|
227
293
|
lines.push(` } else if (${allWrong}) {`);
|
|
@@ -241,7 +307,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
241
307
|
const wrongType = wrongTypeCondition(raw, t);
|
|
242
308
|
const typLabel = typeofString(t);
|
|
243
309
|
if (isRequired) {
|
|
244
|
-
lines.push(` if (
|
|
310
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
245
311
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
246
312
|
if (wrongType) {
|
|
247
313
|
lines.push(` } else if (${wrongType}) {`);
|
|
@@ -266,7 +332,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
266
332
|
lines.push(` }`);
|
|
267
333
|
}
|
|
268
334
|
} else if (isRequired) {
|
|
269
|
-
lines.push(` if (
|
|
335
|
+
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
270
336
|
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
271
337
|
if (extraLines.length > 0) {
|
|
272
338
|
lines.push(` } else {`);
|
|
@@ -287,19 +353,19 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
287
353
|
const lines = [];
|
|
288
354
|
if (hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema)) {
|
|
289
355
|
if (hasPattern(propSchema)) {
|
|
290
|
-
const re =
|
|
356
|
+
const re = regexLiteral(propSchema.pattern);
|
|
291
357
|
const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`);
|
|
292
|
-
lines.push(` if (typeof ${raw} === 'string' &&
|
|
358
|
+
lines.push(` if (typeof ${raw} === 'string' && !${re}.test(${raw})) {`);
|
|
293
359
|
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
294
360
|
lines.push(` }`);
|
|
295
361
|
}
|
|
296
362
|
if (hasMinLength(propSchema)) {
|
|
297
|
-
lines.push(` if (typeof ${raw} === 'string' && ${raw
|
|
363
|
+
lines.push(` if (typeof ${raw} === 'string' && ${minLengthFailExpr(raw, propSchema.minLength)}) {`);
|
|
298
364
|
lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
|
|
299
365
|
lines.push(` }`);
|
|
300
366
|
}
|
|
301
367
|
if (hasMaxLength(propSchema)) {
|
|
302
|
-
lines.push(` if (typeof ${raw} === 'string' && ${raw
|
|
368
|
+
lines.push(` if (typeof ${raw} === 'string' && ${maxLengthFailExpr(raw, propSchema.maxLength)}) {`);
|
|
303
369
|
lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
|
|
304
370
|
lines.push(` }`);
|
|
305
371
|
}
|
|
@@ -307,27 +373,25 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
307
373
|
if (hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema)) {
|
|
308
374
|
if (hasMinimum(propSchema)) {
|
|
309
375
|
const strict = hasStrictExclusiveMinimum(propSchema);
|
|
310
|
-
const op = strict ? "<=" : "<";
|
|
311
376
|
const rel = strict ? ">" : ">=";
|
|
312
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${
|
|
377
|
+
lines.push(` if (typeof ${raw} === 'number' && !(${raw} ${rel} ${propSchema.minimum})) {`);
|
|
313
378
|
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
|
|
314
379
|
lines.push(` }`);
|
|
315
380
|
}
|
|
316
381
|
if (hasMaximum(propSchema)) {
|
|
317
382
|
const strict = hasStrictExclusiveMaximum(propSchema);
|
|
318
|
-
const op = strict ? ">=" : ">";
|
|
319
383
|
const rel = strict ? "<" : "<=";
|
|
320
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${
|
|
384
|
+
lines.push(` if (typeof ${raw} === 'number' && !(${raw} ${rel} ${propSchema.maximum})) {`);
|
|
321
385
|
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
|
|
322
386
|
lines.push(` }`);
|
|
323
387
|
}
|
|
324
388
|
if (hasExclusiveMinimum(propSchema)) {
|
|
325
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw}
|
|
389
|
+
lines.push(` if (typeof ${raw} === 'number' && !(${raw} > ${propSchema.exclusiveMinimum})) {`);
|
|
326
390
|
lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
|
|
327
391
|
lines.push(` }`);
|
|
328
392
|
}
|
|
329
393
|
if (hasExclusiveMaximum(propSchema)) {
|
|
330
|
-
lines.push(` if (typeof ${raw} === 'number' && ${raw}
|
|
394
|
+
lines.push(` if (typeof ${raw} === 'number' && !(${raw} < ${propSchema.exclusiveMaximum})) {`);
|
|
331
395
|
lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
|
|
332
396
|
lines.push(` }`);
|
|
333
397
|
}
|
|
@@ -339,12 +403,14 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
339
403
|
}
|
|
340
404
|
if (hasItems(propSchema)) {
|
|
341
405
|
const itemSchema = propSchema.items;
|
|
406
|
+
const prefix = sp["prefixItems"];
|
|
407
|
+
const firstTailIndex = Array.isArray(prefix) ? prefix.length : 0;
|
|
342
408
|
const iv = `_i${ctx.depth}`;
|
|
343
409
|
const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
|
|
344
410
|
if (hasRef(itemSchema)) {
|
|
345
411
|
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
346
412
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
347
|
-
lines.push(` for (let ${iv} =
|
|
413
|
+
lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
348
414
|
lines.push(` const _ir = ${vName}(${raw}[${iv}], ${itemPath})`);
|
|
349
415
|
lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
|
|
350
416
|
lines.push(` }`);
|
|
@@ -354,7 +420,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
354
420
|
const detail = generateValueChecks("", itemVar, itemPath, itemSchema, suffix, ctx, true);
|
|
355
421
|
if (detail.length > 0) {
|
|
356
422
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
357
|
-
lines.push(` for (let ${iv} =
|
|
423
|
+
lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
358
424
|
lines.push(` const ${itemVar} = ${raw}[${iv}]`);
|
|
359
425
|
lines.push(...detail.map((l) => ` ${l}`));
|
|
360
426
|
lines.push(` }`);
|
|
@@ -362,7 +428,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
362
428
|
}
|
|
363
429
|
}
|
|
364
430
|
}
|
|
365
|
-
if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true ||
|
|
431
|
+
if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || "contains" in sp || Array.isArray(sp["prefixItems"]) || sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
|
|
366
432
|
if (sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
|
|
367
433
|
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
|
|
368
434
|
lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
|
|
@@ -384,7 +450,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
384
450
|
lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
|
|
385
451
|
lines.push(` }`);
|
|
386
452
|
}
|
|
387
|
-
if (
|
|
453
|
+
if ("contains" in sp) {
|
|
388
454
|
const min = typeof sp["minContains"] === "number" ? sp["minContains"] : 1;
|
|
389
455
|
const max = typeof sp["maxContains"] === "number" ? sp["maxContains"] : void 0;
|
|
390
456
|
const matchExpr = generateMatchesExpr("_c", sp["contains"], suffix, ctx);
|
|
@@ -418,10 +484,24 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
418
484
|
lines.push(...generateInlineObjectChecks(key, propSchema, raw, suffix, ctx));
|
|
419
485
|
return lines;
|
|
420
486
|
};
|
|
421
|
-
const generateValueChecks = (
|
|
487
|
+
const generateValueChecks = (key, raw, path, propSchema, suffix, ctx, required = false) => [
|
|
488
|
+
...generateValueCheckLines(key, raw, path, propSchema, suffix, ctx, required),
|
|
489
|
+
// Appended rather than folded into the checks above because several of those
|
|
490
|
+
// branches (`$ref`, `const`, `enum`, x-mjst) return early: `unevaluated*` is a
|
|
491
|
+
// sibling of whatever else the node declares, and dropping it would leave the
|
|
492
|
+
// validator accepting documents the interpreter rejects.
|
|
493
|
+
...generateUnevaluatedChecks(raw, path, propSchema, suffix, ctx)
|
|
494
|
+
];
|
|
495
|
+
const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, required = false) => {
|
|
496
|
+
const lines = [];
|
|
497
|
+
if (propSchema === false) {
|
|
498
|
+
const report = `errors.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`;
|
|
499
|
+
if (required)
|
|
500
|
+
return [` ${report}`];
|
|
501
|
+
return [` if (${raw} !== undefined) {`, ` ${report}`, ` }`];
|
|
502
|
+
}
|
|
422
503
|
if (!isSchemaObject(propSchema))
|
|
423
504
|
return [];
|
|
424
|
-
const lines = [];
|
|
425
505
|
const presence = required ? "" : `${raw} !== undefined && `;
|
|
426
506
|
if (hasRef(propSchema)) {
|
|
427
507
|
const vName = validatorName(refToName(propSchema.$ref, suffix));
|
|
@@ -459,9 +539,8 @@ const generateValueChecks = (_key, raw, path, propSchema, suffix, ctx, required
|
|
|
459
539
|
return lines;
|
|
460
540
|
}
|
|
461
541
|
if (hasEnum(propSchema)) {
|
|
462
|
-
const allowed = JSON.stringify(propSchema.enum);
|
|
463
542
|
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
464
|
-
lines.push(` if (${presence}
|
|
543
|
+
lines.push(` if (${presence}!${enumMembershipExpr(propSchema.enum, raw)}) {`);
|
|
465
544
|
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
466
545
|
lines.push(` }`);
|
|
467
546
|
return lines;
|
|
@@ -476,11 +555,22 @@ const generateValueChecks = (_key, raw, path, propSchema, suffix, ctx, required
|
|
|
476
555
|
lines.push(` }`);
|
|
477
556
|
}
|
|
478
557
|
}
|
|
558
|
+
const typeArray = getTypeArray(propSchema);
|
|
559
|
+
if (typeArray) {
|
|
560
|
+
const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
561
|
+
if (allWrong) {
|
|
562
|
+
const label = typeArray.map((t) => typeofString(t)).join(" or ");
|
|
563
|
+
lines.push(` if (${presence}(${allWrong})) {`);
|
|
564
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
565
|
+
lines.push(` }`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
479
568
|
const valueCtx = {
|
|
480
569
|
objVar: ctx.objVar,
|
|
481
570
|
pathPrefix: path.slice(1, -1),
|
|
482
571
|
depth: ctx.depth + 1,
|
|
483
|
-
hoisted: ctx.hoisted
|
|
572
|
+
hoisted: ctx.hoisted,
|
|
573
|
+
rootSchema: ctx.rootSchema
|
|
484
574
|
};
|
|
485
575
|
lines.push(...generateConstraintChecks("", raw, path, propSchema, suffix, valueCtx));
|
|
486
576
|
lines.push(...generateCombinatorChecks("", raw, path, propSchema, suffix, valueCtx));
|
|
@@ -501,6 +591,41 @@ const generateMatchesExpr = (raw, sub, suffix, ctx) => {
|
|
|
501
591
|
${body}
|
|
502
592
|
return _m.length === 0 })()`;
|
|
503
593
|
};
|
|
594
|
+
const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => generateMatchesExpr(accessor, schema, suffix, { ...ctx, depth: ctx.depth + depth + 1 });
|
|
595
|
+
const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
|
|
596
|
+
if (!isSchemaObject(schema))
|
|
597
|
+
return [];
|
|
598
|
+
const s = schema;
|
|
599
|
+
if (!("unevaluatedProperties" in s) && !("unevaluatedItems" in s))
|
|
600
|
+
return [];
|
|
601
|
+
const match = unevaluatedMatcher(suffix, ctx);
|
|
602
|
+
const lines = [];
|
|
603
|
+
const properties = unevaluatedPropertiesExpr(raw, schema, ctx.rootSchema, ctx.depth, match);
|
|
604
|
+
if (properties === null)
|
|
605
|
+
throw new Error(UNPROVABLE_COVERAGE_MESSAGE("unevaluatedProperties"));
|
|
606
|
+
if (properties !== void 0) {
|
|
607
|
+
lines.push(` if (typeof ${raw} === 'object' && ${raw} !== null && !Array.isArray(${raw})) {`);
|
|
608
|
+
for (const statement of properties.setup)
|
|
609
|
+
lines.push(` ${statement}`);
|
|
610
|
+
lines.push(` if (!(${properties.expr})) {`);
|
|
611
|
+
lines.push(` errors.push({ message: 'must NOT have unevaluated properties', path: ${path} })`);
|
|
612
|
+
lines.push(` }`);
|
|
613
|
+
lines.push(` }`);
|
|
614
|
+
}
|
|
615
|
+
const items = unevaluatedItemsExpr(raw, schema, ctx.rootSchema, ctx.depth, match);
|
|
616
|
+
if (items === null)
|
|
617
|
+
throw new Error(UNPROVABLE_COVERAGE_MESSAGE("unevaluatedItems"));
|
|
618
|
+
if (items !== void 0) {
|
|
619
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
620
|
+
for (const statement of items.setup)
|
|
621
|
+
lines.push(` ${statement}`);
|
|
622
|
+
lines.push(` if (!(${items.expr})) {`);
|
|
623
|
+
lines.push(` errors.push({ message: 'must NOT have unevaluated items', path: ${path} })`);
|
|
624
|
+
lines.push(` }`);
|
|
625
|
+
lines.push(` }`);
|
|
626
|
+
}
|
|
627
|
+
return lines;
|
|
628
|
+
};
|
|
504
629
|
const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
505
630
|
if (!isSchemaObject(schema))
|
|
506
631
|
return [];
|
|
@@ -553,13 +678,13 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
|
|
|
553
678
|
const patternsRecord = "patternProperties" in schema && typeof schema.patternProperties === "object" && schema.patternProperties !== null ? schema.patternProperties : {};
|
|
554
679
|
const patternEntries = Object.entries(patternsRecord);
|
|
555
680
|
for (const [pattern, sub] of patternEntries) {
|
|
556
|
-
const re =
|
|
681
|
+
const re = regexLiteral(pattern);
|
|
557
682
|
const kv = `_pk${d}`;
|
|
558
683
|
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
|
|
559
684
|
if (valueChecks.length === 0)
|
|
560
685
|
continue;
|
|
561
686
|
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
562
|
-
lines.push(` if (
|
|
687
|
+
lines.push(` if (${re}.test(${kv})) {`);
|
|
563
688
|
lines.push(...valueChecks.map((line) => ` ${line}`));
|
|
564
689
|
lines.push(` }`);
|
|
565
690
|
lines.push(` }`);
|
|
@@ -574,7 +699,7 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
|
|
|
574
699
|
if (known.length > 0)
|
|
575
700
|
lines.push(` if (${JSON.stringify(known)}.includes(${kv})) continue`);
|
|
576
701
|
for (const pattern of Object.keys(patternsRecord)) {
|
|
577
|
-
lines.push(` if (
|
|
702
|
+
lines.push(` if (${regexLiteral(pattern)}.test(${kv})) continue`);
|
|
578
703
|
}
|
|
579
704
|
lines.push(...valueChecks.map((line) => ` ${line}`));
|
|
580
705
|
lines.push(` }`);
|
|
@@ -592,7 +717,8 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
592
717
|
// appending `/${key}` would emit a spurious `//` or trailing `/` in error paths.
|
|
593
718
|
pathPrefix: key === "" ? ctx.pathPrefix : `${ctx.pathPrefix}/${pointerSegment(key)}`,
|
|
594
719
|
depth: ctx.depth + 1,
|
|
595
|
-
hoisted: ctx.hoisted
|
|
720
|
+
hoisted: ctx.hoisted,
|
|
721
|
+
rootSchema: ctx.rootSchema
|
|
596
722
|
};
|
|
597
723
|
const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
|
|
598
724
|
const properties = hasProperties(propSchema) ? propSchema.properties : {};
|
|
@@ -607,7 +733,7 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
607
733
|
innerLines.push(...generateDependentSchemasChecks(propSchema, suffix, child));
|
|
608
734
|
innerLines.push(...generateDependenciesChecks(propSchema, suffix, child));
|
|
609
735
|
innerLines.push(...generateMinMaxPropertiesChecks(propSchema, child));
|
|
610
|
-
if (hasPropertyNames(propSchema)
|
|
736
|
+
if (hasPropertyNames(propSchema)) {
|
|
611
737
|
innerLines.push(...generatePropertyNameChecks(propSchema.propertyNames, suffix, child));
|
|
612
738
|
}
|
|
613
739
|
if (innerLines.length === 0)
|
|
@@ -620,14 +746,13 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
620
746
|
];
|
|
621
747
|
};
|
|
622
748
|
const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
|
|
623
|
-
if (!isSchemaObject(nameSchema))
|
|
624
|
-
return [];
|
|
625
749
|
const at = `\`${ctx.pathPrefix}/\${_name}\``;
|
|
626
750
|
const nameCtx = {
|
|
627
751
|
objVar: ctx.objVar,
|
|
628
752
|
pathPrefix: `${ctx.pathPrefix}/\${_name}`,
|
|
629
753
|
depth: ctx.depth + 1,
|
|
630
|
-
hoisted: ctx.hoisted
|
|
754
|
+
hoisted: ctx.hoisted,
|
|
755
|
+
rootSchema: ctx.rootSchema
|
|
631
756
|
};
|
|
632
757
|
const checks = generateValueChecks("", "_name", at, nameSchema, suffix, nameCtx, true);
|
|
633
758
|
if (checks.length === 0)
|
|
@@ -645,7 +770,7 @@ const generateDependentRequiredChecks = (schema, ctx) => {
|
|
|
645
770
|
continue;
|
|
646
771
|
for (const dep of deps) {
|
|
647
772
|
const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
|
|
648
|
-
lines.push(` if (${
|
|
773
|
+
lines.push(` if (${hasOwnCheck(obj, trigger)} && ${missingCheck(obj, dep)}) {`);
|
|
649
774
|
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
650
775
|
lines.push(` }`);
|
|
651
776
|
}
|
|
@@ -667,7 +792,7 @@ const generateDependentSchemasChecks = (schema, suffix, ctx) => {
|
|
|
667
792
|
continue;
|
|
668
793
|
if (sub === false) {
|
|
669
794
|
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
670
|
-
lines.push(` if (${
|
|
795
|
+
lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
|
|
671
796
|
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
672
797
|
lines.push(` }`);
|
|
673
798
|
continue;
|
|
@@ -677,7 +802,7 @@ const generateDependentSchemasChecks = (schema, suffix, ctx) => {
|
|
|
677
802
|
const checks = generateValueChecks("", obj, objPath, sub, suffix, ctx);
|
|
678
803
|
if (checks.length === 0)
|
|
679
804
|
continue;
|
|
680
|
-
lines.push(` if (${
|
|
805
|
+
lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
|
|
681
806
|
lines.push(...checks.map((line) => ` ${line}`));
|
|
682
807
|
lines.push(` }`);
|
|
683
808
|
}
|
|
@@ -699,7 +824,7 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
|
|
|
699
824
|
if (typeof key !== "string")
|
|
700
825
|
continue;
|
|
701
826
|
const msg = JSON.stringify(`must have property '${key}' when '${trigger}' is present`);
|
|
702
|
-
lines.push(` if (${
|
|
827
|
+
lines.push(` if (${hasOwnCheck(obj, trigger)} && ${missingCheck(obj, key)}) {`);
|
|
703
828
|
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
704
829
|
lines.push(` }`);
|
|
705
830
|
}
|
|
@@ -709,7 +834,7 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
|
|
|
709
834
|
continue;
|
|
710
835
|
if (value === false) {
|
|
711
836
|
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
712
|
-
lines.push(` if (${
|
|
837
|
+
lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
|
|
713
838
|
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
714
839
|
lines.push(` }`);
|
|
715
840
|
continue;
|
|
@@ -719,7 +844,7 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
|
|
|
719
844
|
const checks = generateValueChecks("", obj, objPath, value, suffix, ctx);
|
|
720
845
|
if (checks.length === 0)
|
|
721
846
|
continue;
|
|
722
|
-
lines.push(` if (${
|
|
847
|
+
lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
|
|
723
848
|
lines.push(...checks.map((line) => ` ${line}`));
|
|
724
849
|
lines.push(` }`);
|
|
725
850
|
}
|
|
@@ -755,11 +880,17 @@ const hasObjectLevelCombinator = (schema) => {
|
|
|
755
880
|
return false;
|
|
756
881
|
return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema;
|
|
757
882
|
};
|
|
883
|
+
const carriesUnevaluated = (schema) => {
|
|
884
|
+
if (!isSchemaObject(schema))
|
|
885
|
+
return false;
|
|
886
|
+
const s = schema;
|
|
887
|
+
return "unevaluatedProperties" in s && s["unevaluatedProperties"] !== true || "unevaluatedItems" in s && s["unevaluatedItems"] !== true;
|
|
888
|
+
};
|
|
758
889
|
const guardPropConditions = (key, propSchema, objAcc) => {
|
|
759
890
|
if (!isSchemaObject(propSchema))
|
|
760
891
|
return null;
|
|
761
892
|
const raw = safeAccessor(objAcc, key);
|
|
762
|
-
if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema) || "not" in propSchema || "if" in propSchema || getMjstInstanceOf(propSchema) !== void 0 || getMjstPrimitive(propSchema) !== void 0 || hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema) || hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema) || hasItems(propSchema)) {
|
|
893
|
+
if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema) || "not" in propSchema || "if" in propSchema || getMjstInstanceOf(propSchema) !== void 0 || getMjstPrimitive(propSchema) !== void 0 || hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema) || hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema) || hasItems(propSchema) || carriesUnevaluated(propSchema)) {
|
|
763
894
|
return null;
|
|
764
895
|
}
|
|
765
896
|
if (!hasType(propSchema))
|
|
@@ -807,6 +938,8 @@ const guardObjectConditions = (schema, raw, objAcc) => {
|
|
|
807
938
|
return null;
|
|
808
939
|
if (hasObjectLevelCombinator(schema))
|
|
809
940
|
return null;
|
|
941
|
+
if (carriesUnevaluated(schema))
|
|
942
|
+
return null;
|
|
810
943
|
let strict = false;
|
|
811
944
|
if (hasAdditionalProperties(schema)) {
|
|
812
945
|
if (schema.additionalProperties === false)
|
|
@@ -838,11 +971,11 @@ const guardObjectConditions = (schema, raw, objAcc) => {
|
|
|
838
971
|
}
|
|
839
972
|
return conditions;
|
|
840
973
|
};
|
|
841
|
-
const generateObjectValidator = (schema, typeName, suffix) => {
|
|
974
|
+
const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
|
|
842
975
|
const vName = validatorName(typeName);
|
|
843
976
|
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
844
977
|
const properties = hasProperties(schema) ? schema.properties : {};
|
|
845
|
-
const ctx = createRootContext();
|
|
978
|
+
const ctx = createRootContext(rootSchema);
|
|
846
979
|
const propertyLines = [];
|
|
847
980
|
for (const [key, propSchema] of Object.entries(properties)) {
|
|
848
981
|
const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix, ctx);
|
|
@@ -857,7 +990,7 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
857
990
|
propertyLines.push(...generateDependentSchemasChecks(schema, suffix, ctx));
|
|
858
991
|
propertyLines.push(...generateDependenciesChecks(schema, suffix, ctx));
|
|
859
992
|
propertyLines.push(...generateMinMaxPropertiesChecks(schema, ctx));
|
|
860
|
-
if (hasPropertyNames(schema)
|
|
993
|
+
if (hasPropertyNames(schema)) {
|
|
861
994
|
propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
|
|
862
995
|
}
|
|
863
996
|
propertyLines.push(...generateCombinatorChecks("", "obj", "`${_path}`", schema, suffix, ctx));
|
|
@@ -897,17 +1030,10 @@ const generateObjectValidator = (schema, typeName, suffix) => {
|
|
|
897
1030
|
].join("\n");
|
|
898
1031
|
};
|
|
899
1032
|
const guardName = (typeName) => `is${typeName}`;
|
|
900
|
-
const enumMembershipExpr = (values, acc) => {
|
|
901
|
-
const allPrimitive = values.length > 0 && values.every((v) => (v === null || typeof v !== "object") && typeof v !== "function") && !values.some((v) => typeof v === "number" && Number.isNaN(v));
|
|
902
|
-
if (allPrimitive) {
|
|
903
|
-
return `(${values.map((v) => `${acc} === ${JSON.stringify(v)}`).join(" || ")})`;
|
|
904
|
-
}
|
|
905
|
-
return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
|
|
906
|
-
};
|
|
907
1033
|
const booleanLeafExpr = (schema, acc) => {
|
|
908
1034
|
if (!isSchemaObject(schema))
|
|
909
1035
|
return null;
|
|
910
|
-
if (hasRef(schema) || hasConst(schema) || hasOneOf(schema) || "anyOf" in schema || "allOf" in schema || "not" in schema || "if" in schema || "contains" in schema || "prefixItems" in schema || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
|
|
1036
|
+
if (hasRef(schema) || hasConst(schema) || hasOneOf(schema) || "anyOf" in schema || "allOf" in schema || "not" in schema || "if" in schema || "contains" in schema || "prefixItems" in schema || carriesUnevaluated(schema) || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
|
|
911
1037
|
return null;
|
|
912
1038
|
}
|
|
913
1039
|
if (hasEnum(schema)) {
|
|
@@ -917,18 +1043,20 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
917
1043
|
return null;
|
|
918
1044
|
const t = schema.type;
|
|
919
1045
|
switch (t) {
|
|
920
|
-
// Each constraint is the exact negation of the validator's error condition
|
|
921
|
-
//
|
|
922
|
-
//
|
|
923
|
-
//
|
|
1046
|
+
// Each constraint is the exact negation of the validator's error condition,
|
|
1047
|
+
// so edge values — `NaN` above all, which compares `false` against every
|
|
1048
|
+
// operator — get the identical verdict on both paths. The numeric bounds are
|
|
1049
|
+
// written as the pass condition (`x >= min`) because the validator's error
|
|
1050
|
+
// condition is its negation; the length checks come from the same
|
|
1051
|
+
// `string-length-check` emitter the validator uses, for the same reason.
|
|
924
1052
|
case "string": {
|
|
925
1053
|
const parts = [`typeof ${acc} === 'string'`];
|
|
926
1054
|
if (hasPattern(schema))
|
|
927
|
-
parts.push(
|
|
1055
|
+
parts.push(`${regexLiteral(schema.pattern)}.test(${acc})`);
|
|
928
1056
|
if (hasMinLength(schema))
|
|
929
|
-
parts.push(
|
|
1057
|
+
parts.push(minLengthPassExpr(acc, schema.minLength));
|
|
930
1058
|
if (hasMaxLength(schema))
|
|
931
|
-
parts.push(
|
|
1059
|
+
parts.push(maxLengthPassExpr(acc, schema.maxLength));
|
|
932
1060
|
return parts.join(" && ");
|
|
933
1061
|
}
|
|
934
1062
|
case "number":
|
|
@@ -937,13 +1065,13 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
937
1065
|
if (t === "integer")
|
|
938
1066
|
parts.push(`Number.isInteger(${acc})`);
|
|
939
1067
|
if (hasMinimum(schema))
|
|
940
|
-
parts.push(
|
|
1068
|
+
parts.push(`${acc} ${hasStrictExclusiveMinimum(schema) ? ">" : ">="} ${schema.minimum}`);
|
|
941
1069
|
if (hasMaximum(schema))
|
|
942
|
-
parts.push(
|
|
1070
|
+
parts.push(`${acc} ${hasStrictExclusiveMaximum(schema) ? "<" : "<="} ${schema.maximum}`);
|
|
943
1071
|
if (hasExclusiveMinimum(schema))
|
|
944
|
-
parts.push(
|
|
1072
|
+
parts.push(`${acc} > ${schema.exclusiveMinimum}`);
|
|
945
1073
|
if (hasExclusiveMaximum(schema))
|
|
946
|
-
parts.push(
|
|
1074
|
+
parts.push(`${acc} < ${schema.exclusiveMaximum}`);
|
|
947
1075
|
if (hasMultipleOf(schema))
|
|
948
1076
|
parts.push(multipleOfPassExpr(acc, schema.multipleOf));
|
|
949
1077
|
return parts.join(" && ");
|
|
@@ -995,6 +1123,8 @@ const booleanObjectParts = (schema, raw, objAcc) => {
|
|
|
995
1123
|
return null;
|
|
996
1124
|
if (hasObjectLevelCombinator(schema))
|
|
997
1125
|
return null;
|
|
1126
|
+
if (carriesUnevaluated(schema))
|
|
1127
|
+
return null;
|
|
998
1128
|
let strict = false;
|
|
999
1129
|
if (hasAdditionalProperties(schema)) {
|
|
1000
1130
|
if (schema.additionalProperties === false)
|
|
@@ -1011,6 +1141,8 @@ const booleanObjectParts = (schema, raw, objAcc) => {
|
|
|
1011
1141
|
const propSchema = properties[key];
|
|
1012
1142
|
if (propSchema === void 0 || !isSchemaObject(propSchema))
|
|
1013
1143
|
return null;
|
|
1144
|
+
if (PROTOTYPE_MEMBERS.has(key))
|
|
1145
|
+
return null;
|
|
1014
1146
|
const member = safeAccessor(objAcc, key);
|
|
1015
1147
|
const expr = booleanLeafExpr(propSchema, member);
|
|
1016
1148
|
if (expr === null)
|
|
@@ -1029,16 +1161,33 @@ const booleanObjectParts = (schema, raw, objAcc) => {
|
|
|
1029
1161
|
}
|
|
1030
1162
|
return parts;
|
|
1031
1163
|
};
|
|
1164
|
+
const IMPLICIT_OBJECT_KEYWORDS = ["properties", "patternProperties", "additionalProperties"];
|
|
1165
|
+
const typeDescribesEveryAcceptedValue = (schema) => {
|
|
1166
|
+
if (!isSchemaObject(schema))
|
|
1167
|
+
return true;
|
|
1168
|
+
const s = schema;
|
|
1169
|
+
if ("type" in s || "enum" in s || "const" in s || "$ref" in s)
|
|
1170
|
+
return true;
|
|
1171
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
1172
|
+
const branches = s[keyword];
|
|
1173
|
+
if (!Array.isArray(branches))
|
|
1174
|
+
continue;
|
|
1175
|
+
if (!branches.every((branch) => typeDescribesEveryAcceptedValue(branch)))
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
return !IMPLICIT_OBJECT_KEYWORDS.some((keyword) => keyword in s);
|
|
1179
|
+
};
|
|
1032
1180
|
const generateBooleanGuard = (schema, typeName, _suffix = "") => {
|
|
1033
1181
|
const name = guardName(typeName);
|
|
1034
|
-
const
|
|
1182
|
+
const returns = typeDescribesEveryAcceptedValue(rewriteNullable(schema)) ? `input is ${typeName}` : "boolean";
|
|
1183
|
+
const fallback = `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`;
|
|
1035
1184
|
const rewritten = rewriteNullable(schema);
|
|
1036
|
-
if (
|
|
1185
|
+
if (declaresObjectType(rewritten)) {
|
|
1037
1186
|
const parts = booleanObjectParts(rewritten, "input", "obj");
|
|
1038
1187
|
if (parts === null)
|
|
1039
1188
|
return fallback;
|
|
1040
1189
|
return [
|
|
1041
|
-
`export const ${name} = (input: unknown):
|
|
1190
|
+
`export const ${name} = (input: unknown): ${returns} => {`,
|
|
1042
1191
|
` const obj = input as Record<string, unknown>`,
|
|
1043
1192
|
` return (`,
|
|
1044
1193
|
parts.map((part) => ` ${part}`).join(" &&\n"),
|
|
@@ -1049,11 +1198,18 @@ const generateBooleanGuard = (schema, typeName, _suffix = "") => {
|
|
|
1049
1198
|
const expr = booleanLeafExpr(rewritten, "input");
|
|
1050
1199
|
if (expr === null)
|
|
1051
1200
|
return fallback;
|
|
1052
|
-
return `export const ${name} = (input: unknown):
|
|
1201
|
+
return `export const ${name} = (input: unknown): ${returns} => ${expr}`;
|
|
1053
1202
|
};
|
|
1054
|
-
const generateScalarValidator = (schema, typeName, suffix) => {
|
|
1203
|
+
const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
1055
1204
|
const vName = validatorName(typeName);
|
|
1056
1205
|
if (!isSchemaObject(schema)) {
|
|
1206
|
+
if (schema === false) {
|
|
1207
|
+
return [
|
|
1208
|
+
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
1209
|
+
` return { valid: false, errors: [{ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: _path }] }`,
|
|
1210
|
+
`}`
|
|
1211
|
+
].join("\n");
|
|
1212
|
+
}
|
|
1057
1213
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
|
|
1058
1214
|
}
|
|
1059
1215
|
if (hasRef(schema)) {
|
|
@@ -1099,11 +1255,10 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1099
1255
|
].join("\n");
|
|
1100
1256
|
}
|
|
1101
1257
|
if (hasEnum(schema)) {
|
|
1102
|
-
const allowed = JSON.stringify(schema.enum);
|
|
1103
1258
|
const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
1104
1259
|
return [
|
|
1105
1260
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1106
|
-
` if (
|
|
1261
|
+
` if (!${enumMembershipExpr(schema.enum, "input")}) {`,
|
|
1107
1262
|
` return { valid: false, errors: [{ message: ${JSON.stringify(`must be one of: ${label}`)}, path: _path }] }`,
|
|
1108
1263
|
` }`,
|
|
1109
1264
|
` return true`,
|
|
@@ -1111,7 +1266,7 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1111
1266
|
].join("\n");
|
|
1112
1267
|
}
|
|
1113
1268
|
if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema) {
|
|
1114
|
-
const ctx = createRootContext();
|
|
1269
|
+
const ctx = createRootContext(rootSchema);
|
|
1115
1270
|
const checks = [];
|
|
1116
1271
|
const rootPath = "`${_path}`";
|
|
1117
1272
|
const rootTypeArray2 = getTypeArray(schema);
|
|
@@ -1123,7 +1278,6 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1123
1278
|
checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
|
|
1124
1279
|
checks.push(` }`);
|
|
1125
1280
|
}
|
|
1126
|
-
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1127
1281
|
} else if (hasType(schema)) {
|
|
1128
1282
|
const t = schema.type;
|
|
1129
1283
|
const wrongType = wrongTypeCondition("input", t);
|
|
@@ -1132,8 +1286,8 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1132
1286
|
checks.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
|
|
1133
1287
|
checks.push(` }`);
|
|
1134
1288
|
}
|
|
1135
|
-
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1136
1289
|
}
|
|
1290
|
+
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1137
1291
|
checks.push(...generateCombinatorChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1138
1292
|
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1139
1293
|
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
|
|
@@ -1149,21 +1303,33 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1149
1303
|
}
|
|
1150
1304
|
const rootTypeArray = getTypeArray(schema);
|
|
1151
1305
|
if (rootTypeArray) {
|
|
1306
|
+
const ctx = createRootContext(rootSchema);
|
|
1307
|
+
const rootPath = "`${_path}`";
|
|
1308
|
+
const checks = [];
|
|
1152
1309
|
const allWrong = rootTypeArray.map((t) => wrongTypeCondition("input", t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
1153
|
-
|
|
1154
|
-
|
|
1310
|
+
if (allWrong) {
|
|
1311
|
+
const label = rootTypeArray.map((t) => typeofString(t)).join(" or ");
|
|
1312
|
+
checks.push(` if (${allWrong}) {`);
|
|
1313
|
+
checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
|
|
1314
|
+
checks.push(` }`);
|
|
1315
|
+
}
|
|
1316
|
+
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1317
|
+
if (checks.length === 0) {
|
|
1155
1318
|
return [
|
|
1156
1319
|
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
1157
1320
|
` return true`,
|
|
1158
1321
|
`}`
|
|
1159
1322
|
].join("\n");
|
|
1160
1323
|
}
|
|
1324
|
+
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1325
|
+
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
|
|
1326
|
+
|
|
1327
|
+
` : "";
|
|
1161
1328
|
return [
|
|
1162
|
-
|
|
1163
|
-
`
|
|
1164
|
-
|
|
1165
|
-
` }`,
|
|
1166
|
-
` return true`,
|
|
1329
|
+
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1330
|
+
` let errors: ValidationError[] | undefined`,
|
|
1331
|
+
body,
|
|
1332
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1167
1333
|
`}`
|
|
1168
1334
|
].join("\n");
|
|
1169
1335
|
}
|
|
@@ -1171,7 +1337,7 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1171
1337
|
const t = schema.type;
|
|
1172
1338
|
const wrongType = wrongTypeCondition("input", t);
|
|
1173
1339
|
const typLabel = typeofString(t);
|
|
1174
|
-
const rootCtx = createRootContext();
|
|
1340
|
+
const rootCtx = createRootContext(rootSchema);
|
|
1175
1341
|
const constraintLines = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, rootCtx);
|
|
1176
1342
|
if (!wrongType) {
|
|
1177
1343
|
return [
|
|
@@ -1204,27 +1370,36 @@ const generateScalarValidator = (schema, typeName, suffix) => {
|
|
|
1204
1370
|
`}`
|
|
1205
1371
|
].join("\n");
|
|
1206
1372
|
}
|
|
1207
|
-
|
|
1373
|
+
const typelessCtx = createRootContext(rootSchema);
|
|
1374
|
+
const typelessChecks = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, typelessCtx);
|
|
1375
|
+
if (typelessChecks.length === 0) {
|
|
1376
|
+
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
|
|
1377
|
+
}
|
|
1378
|
+
const typelessHoisted = typelessCtx.hoisted.length > 0 ? `${typelessCtx.hoisted.join("\n")}
|
|
1379
|
+
|
|
1380
|
+
` : "";
|
|
1381
|
+
return [
|
|
1382
|
+
`${typelessHoisted}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1383
|
+
` let errors: ValidationError[] | undefined`,
|
|
1384
|
+
typelessChecks.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
|
|
1385
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1386
|
+
`}`
|
|
1387
|
+
].join("\n");
|
|
1208
1388
|
};
|
|
1209
|
-
const
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}
|
|
1224
|
-
for (const value of Object.values(record))
|
|
1225
|
-
visit(value);
|
|
1226
|
-
};
|
|
1227
|
-
visit(schema);
|
|
1389
|
+
const generateUnevaluatedRootValidator = (schema, typeName, suffix, rootSchema) => {
|
|
1390
|
+
const ctx = createRootContext(rootSchema);
|
|
1391
|
+
const checks = generateValueChecks("", "input", "`${_path}`", schema, suffix, ctx, true);
|
|
1392
|
+
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1393
|
+
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
|
|
1394
|
+
|
|
1395
|
+
` : "";
|
|
1396
|
+
return [
|
|
1397
|
+
`${hoistedBlock}export const ${validatorName(typeName)} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1398
|
+
` let errors: ValidationError[] | undefined`,
|
|
1399
|
+
body,
|
|
1400
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1401
|
+
`}`
|
|
1402
|
+
].join("\n");
|
|
1228
1403
|
};
|
|
1229
1404
|
const SINGLE_SUBSCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
1230
1405
|
"additionalProperties",
|
|
@@ -1240,6 +1415,13 @@ const SINGLE_SUBSCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
|
1240
1415
|
]);
|
|
1241
1416
|
const SUBSCHEMA_LIST_KEYS = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
|
|
1242
1417
|
const SUBSCHEMA_MAP_KEYS = /* @__PURE__ */ new Set(["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"]);
|
|
1418
|
+
const defineEntry = (target, key, value) => {
|
|
1419
|
+
if (key === "__proto__") {
|
|
1420
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
target[key] = value;
|
|
1424
|
+
};
|
|
1243
1425
|
const rewriteNullable = (node) => {
|
|
1244
1426
|
if (typeof node !== "object" || node === null || Array.isArray(node))
|
|
1245
1427
|
return node;
|
|
@@ -1249,37 +1431,43 @@ const rewriteNullable = (node) => {
|
|
|
1249
1431
|
if (key === "nullable")
|
|
1250
1432
|
continue;
|
|
1251
1433
|
if (SINGLE_SUBSCHEMA_KEYS.has(key)) {
|
|
1252
|
-
out
|
|
1434
|
+
defineEntry(out, key, rewriteNullable(value));
|
|
1253
1435
|
} else if (key === "items") {
|
|
1254
|
-
out
|
|
1436
|
+
defineEntry(out, key, Array.isArray(value) ? value.map(rewriteNullable) : rewriteNullable(value));
|
|
1255
1437
|
} else if (SUBSCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
1256
|
-
out
|
|
1438
|
+
defineEntry(out, key, value.map(rewriteNullable));
|
|
1257
1439
|
} else if (SUBSCHEMA_MAP_KEYS.has(key) && typeof value === "object" && value !== null) {
|
|
1258
1440
|
const mapped = {};
|
|
1259
|
-
for (const [name, sub] of Object.entries(value))
|
|
1260
|
-
mapped
|
|
1261
|
-
|
|
1441
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
1442
|
+
defineEntry(mapped, name, rewriteNullable(sub));
|
|
1443
|
+
}
|
|
1444
|
+
defineEntry(out, key, mapped);
|
|
1262
1445
|
} else if (key === "dependencies" && typeof value === "object" && value !== null) {
|
|
1263
1446
|
const mapped = {};
|
|
1264
1447
|
for (const [name, sub] of Object.entries(value)) {
|
|
1265
|
-
mapped
|
|
1448
|
+
defineEntry(mapped, name, Array.isArray(sub) ? sub : rewriteNullable(sub));
|
|
1266
1449
|
}
|
|
1267
|
-
out
|
|
1450
|
+
defineEntry(out, key, mapped);
|
|
1268
1451
|
} else {
|
|
1269
|
-
out
|
|
1452
|
+
defineEntry(out, key, value);
|
|
1270
1453
|
}
|
|
1271
1454
|
}
|
|
1272
1455
|
if (src["nullable"] === true)
|
|
1273
1456
|
return { anyOf: [{ type: "null" }, out] };
|
|
1274
1457
|
return out;
|
|
1275
1458
|
};
|
|
1276
|
-
const generateValidatorFunction = (schema, typeName, suffix = "") => {
|
|
1277
|
-
|
|
1459
|
+
const generateValidatorFunction = (schema, typeName, suffix = "", rootSchema) => {
|
|
1460
|
+
assertGeneratableRefs(schema, typeName);
|
|
1278
1461
|
const rewritten = rewriteNullable(schema);
|
|
1279
|
-
|
|
1280
|
-
|
|
1462
|
+
const document = rootSchema ?? schema;
|
|
1463
|
+
assertUnevaluatedGeneratable(rewritten, typeName, document, unevaluatedMatcher(suffix, createRootContext(document)));
|
|
1464
|
+
if (carriesUnevaluated(rewritten)) {
|
|
1465
|
+
return generateUnevaluatedRootValidator(rewritten, typeName, suffix, document);
|
|
1466
|
+
}
|
|
1467
|
+
if (declaresObjectType(rewritten)) {
|
|
1468
|
+
return generateObjectValidator(rewritten, typeName, suffix, document);
|
|
1281
1469
|
}
|
|
1282
|
-
return generateScalarValidator(rewritten, typeName, suffix);
|
|
1470
|
+
return generateScalarValidator(rewritten, typeName, suffix, document);
|
|
1283
1471
|
};
|
|
1284
1472
|
export {
|
|
1285
1473
|
generateBooleanGuard,
|