@oh-my-pi/omptype 18.16.0 → 18.18.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/CHANGELOG.md +18 -0
- package/README.md +33 -0
- package/dist/js/compile.js +25 -3
- package/dist/js/from-json-schema.js +61 -216
- package/dist/js/interp.js +2 -0
- package/dist/js/jsonschema/codegen.js +514 -0
- package/dist/js/jsonschema/interpret.js +729 -0
- package/dist/js/jsonschema/pattern.js +221 -0
- package/dist/js/jsonschema/plan.js +448 -0
- package/dist/js/jsonschema/resolve.js +321 -0
- package/dist/js/jsonschema/runtime.js +129 -0
- package/dist/js/jsonschema/validator.js +106 -0
- package/dist/js/typebox.js +5 -5
- package/dist/types/from-json-schema.d.ts +8 -3
- package/dist/types/ir.d.ts +2 -0
- package/dist/types/jsonschema/codegen.d.ts +6 -0
- package/dist/types/jsonschema/interpret.d.ts +48 -0
- package/dist/types/jsonschema/pattern.d.ts +17 -0
- package/dist/types/jsonschema/plan.d.ts +128 -0
- package/dist/types/jsonschema/resolve.d.ts +43 -0
- package/dist/types/jsonschema/runtime.d.ts +30 -0
- package/dist/types/jsonschema/validator.d.ts +33 -0
- package/package.json +2 -5
- package/src/compile.ts +24 -3
- package/src/from-json-schema.ts +64 -213
- package/src/interp.ts +1 -0
- package/src/ir.ts +2 -0
- package/src/jsonschema/codegen.ts +495 -0
- package/src/jsonschema/interpret.ts +724 -0
- package/src/jsonschema/pattern.ts +202 -0
- package/src/jsonschema/plan.ts +538 -0
- package/src/jsonschema/resolve.ts +333 -0
- package/src/jsonschema/runtime.ts +117 -0
- package/src/jsonschema/validator.ts +113 -0
- package/src/typebox.ts +9 -5
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan interpreter: validates values directly against a {@link Plan} graph.
|
|
3
|
+
* It serves a schema's first calls (before the JIT in `codegen.ts` takes
|
|
4
|
+
* over), runtimes that forbid code generation, error explanations, and the
|
|
5
|
+
* annotation tracking behind `unevaluatedProperties`/`unevaluatedItems`.
|
|
6
|
+
*
|
|
7
|
+
* {@link validate} takes two optional collectors:
|
|
8
|
+
* - `sink` gathers {@link Issue}s with paths (explain mode); validation then
|
|
9
|
+
* keeps going past the first failure so every issue surfaces.
|
|
10
|
+
* - `ann` receives evaluated property names and item indices. Only in-place
|
|
11
|
+
* applicators (`allOf`, `anyOf`, `$ref`, ...) forward it; child instances
|
|
12
|
+
* start fresh.
|
|
13
|
+
*/
|
|
14
|
+
import { MISSING } from "../errors.js";
|
|
15
|
+
import { F_APPLICATORS, F_ARRAY, F_CONST, F_DEPENDENT, F_ENUM, F_KEYS, F_NUMBER, F_PROPERTIES, F_REFS, F_STRING, F_UNEVALUATED, T_ANY, T_ARRAY, T_BOOLEAN, T_INTEGER, T_NULL, T_NUMBER, T_OBJECT, T_STRING, } from "./plan.js";
|
|
16
|
+
import { codePointLength, isMultipleOf, jsonEqual, jsonIncludes, uniqueItems } from "./runtime.js";
|
|
17
|
+
/** Issue collector for explain mode. */
|
|
18
|
+
export class Sink {
|
|
19
|
+
issues = [];
|
|
20
|
+
add(expected, data, key) {
|
|
21
|
+
this.issues.push({ path: key === undefined ? [] : [key], expected, data });
|
|
22
|
+
}
|
|
23
|
+
/** Prefix the paths of issues recorded since `from` with `key`. */
|
|
24
|
+
prefix(from, key) {
|
|
25
|
+
const issues = this.issues;
|
|
26
|
+
for (let index = from; index < issues.length; index++)
|
|
27
|
+
issues[index].path.unshift(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const hasOwn = Object.hasOwn;
|
|
31
|
+
const isArray = Array.isArray;
|
|
32
|
+
function newAnn() {
|
|
33
|
+
return { props: undefined, items: 0, contained: undefined };
|
|
34
|
+
}
|
|
35
|
+
function mergeAnn(into, from) {
|
|
36
|
+
if (from.props === true)
|
|
37
|
+
into.props = true;
|
|
38
|
+
else if (from.props !== undefined && into.props !== true) {
|
|
39
|
+
if (into.props === undefined)
|
|
40
|
+
into.props = new Set(from.props);
|
|
41
|
+
else
|
|
42
|
+
for (const key of from.props)
|
|
43
|
+
into.props.add(key);
|
|
44
|
+
}
|
|
45
|
+
if (from.items > into.items)
|
|
46
|
+
into.items = from.items;
|
|
47
|
+
if (from.contained !== undefined) {
|
|
48
|
+
if (into.contained === undefined)
|
|
49
|
+
into.contained = new Set(from.contained);
|
|
50
|
+
else
|
|
51
|
+
for (const index of from.contained)
|
|
52
|
+
into.contained.add(index);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function annotateProp(ann, key) {
|
|
56
|
+
if (ann.props === true)
|
|
57
|
+
return;
|
|
58
|
+
if (ann.props === undefined)
|
|
59
|
+
ann.props = new Set([key]);
|
|
60
|
+
else
|
|
61
|
+
ann.props.add(key);
|
|
62
|
+
}
|
|
63
|
+
function isObject(value) {
|
|
64
|
+
return typeof value === "object" && value !== null && !isArray(value);
|
|
65
|
+
}
|
|
66
|
+
/** Own-property read; `inherited` names (`constructor`, `__proto__`, ...) need the own check. */
|
|
67
|
+
function ownValue(object, key, inherited) {
|
|
68
|
+
return inherited && !hasOwn(object, key) ? undefined : object[key];
|
|
69
|
+
}
|
|
70
|
+
const TYPE_NAMES = [
|
|
71
|
+
[T_OBJECT, "an object"],
|
|
72
|
+
[T_ARRAY, "an array"],
|
|
73
|
+
[T_STRING, "a string"],
|
|
74
|
+
[T_NUMBER, "a number"],
|
|
75
|
+
[T_INTEGER, "an integer"],
|
|
76
|
+
[T_BOOLEAN, "boolean"],
|
|
77
|
+
[T_NULL, "null"],
|
|
78
|
+
];
|
|
79
|
+
/** Human-readable expectation for a `T_*` mask. */
|
|
80
|
+
export function describeTypes(types) {
|
|
81
|
+
if (types === 0)
|
|
82
|
+
return "never";
|
|
83
|
+
const names = [];
|
|
84
|
+
for (const [flag, name] of TYPE_NAMES) {
|
|
85
|
+
if (!(types & flag))
|
|
86
|
+
continue;
|
|
87
|
+
if (flag === T_INTEGER && types & T_NUMBER)
|
|
88
|
+
continue;
|
|
89
|
+
names.push(name);
|
|
90
|
+
}
|
|
91
|
+
return names.join(" or ");
|
|
92
|
+
}
|
|
93
|
+
function describeValue(value) {
|
|
94
|
+
return JSON.stringify(value) ?? String(value);
|
|
95
|
+
}
|
|
96
|
+
/** What a missing or mismatched value of `plan` was expected to be. */
|
|
97
|
+
function expectedOf(schema) {
|
|
98
|
+
const plan = unwrapRef(schema);
|
|
99
|
+
if (plan.hasConst)
|
|
100
|
+
return describeValue(plan.constValue);
|
|
101
|
+
if (plan.enumValues !== undefined)
|
|
102
|
+
return plan.enumValues.map(describeValue).join(" or ");
|
|
103
|
+
if (plan.types !== T_ANY)
|
|
104
|
+
return describeTypes(plan.types);
|
|
105
|
+
return "present";
|
|
106
|
+
}
|
|
107
|
+
function matchesTypes(value, types) {
|
|
108
|
+
switch (typeof value) {
|
|
109
|
+
case "string":
|
|
110
|
+
return (types & T_STRING) !== 0;
|
|
111
|
+
case "number":
|
|
112
|
+
return (types & T_NUMBER) !== 0
|
|
113
|
+
? Number.isFinite(value)
|
|
114
|
+
: (types & T_INTEGER) !== 0 && Number.isInteger(value);
|
|
115
|
+
case "boolean":
|
|
116
|
+
return (types & T_BOOLEAN) !== 0;
|
|
117
|
+
case "object":
|
|
118
|
+
if (value === null)
|
|
119
|
+
return (types & T_NULL) !== 0;
|
|
120
|
+
return (types & (isArray(value) ? T_ARRAY : T_OBJECT)) !== 0;
|
|
121
|
+
default:
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function isPrimitive(value) {
|
|
126
|
+
return value === null || (typeof value !== "object" && typeof value !== "function");
|
|
127
|
+
}
|
|
128
|
+
/** Follow plans that are a bare single `$ref`. */
|
|
129
|
+
export function unwrapRef(plan) {
|
|
130
|
+
let current = plan;
|
|
131
|
+
for (let depth = 0; depth < 32 && current.refs?.length === 1 && isRefOnly(current); depth++) {
|
|
132
|
+
current = current.refs[0];
|
|
133
|
+
}
|
|
134
|
+
return current;
|
|
135
|
+
}
|
|
136
|
+
/** True when the plan's only constraint is its `refs`. */
|
|
137
|
+
export function isRefOnly(plan) {
|
|
138
|
+
return plan.flags === F_REFS && plan.types === T_ANY;
|
|
139
|
+
}
|
|
140
|
+
function tagValues(plan) {
|
|
141
|
+
if (plan.hasConst)
|
|
142
|
+
return isPrimitive(plan.constValue) ? [plan.constValue] : undefined;
|
|
143
|
+
if (plan.enumValues !== undefined && plan.enumValues.every(isPrimitive))
|
|
144
|
+
return [...plan.enumValues];
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
/** Find a {@link Discriminator} for union branches, if one exists. */
|
|
148
|
+
export function findDiscriminator(branches) {
|
|
149
|
+
if (branches.length < 2)
|
|
150
|
+
return undefined;
|
|
151
|
+
const plans = branches.map(unwrapRef);
|
|
152
|
+
const first = plans[0];
|
|
153
|
+
if (first.types !== T_OBJECT || first.properties === undefined || first.required === undefined)
|
|
154
|
+
return undefined;
|
|
155
|
+
for (const candidate of first.properties) {
|
|
156
|
+
if (candidate.inherited)
|
|
157
|
+
continue;
|
|
158
|
+
const key = candidate.key;
|
|
159
|
+
const map = new Map();
|
|
160
|
+
let ok = true;
|
|
161
|
+
for (let index = 0; index < plans.length && ok; index++) {
|
|
162
|
+
const plan = plans[index];
|
|
163
|
+
if (plan.types !== T_OBJECT || plan.required?.includes(key) !== true) {
|
|
164
|
+
ok = false;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
const property = plan.properties?.find(entry => entry.key === key);
|
|
168
|
+
const values = property === undefined ? undefined : tagValues(unwrapRef(property.plan));
|
|
169
|
+
if (values === undefined) {
|
|
170
|
+
ok = false;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
for (const value of values) {
|
|
174
|
+
if (map.has(value)) {
|
|
175
|
+
ok = false;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
map.set(value, index);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (ok)
|
|
182
|
+
return { key, branches: map };
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const kAnyOf = Symbol("omptype.jsonschema.anyOfTag");
|
|
187
|
+
const kOneOf = Symbol("omptype.jsonschema.oneOfTag");
|
|
188
|
+
/**
|
|
189
|
+
* Whether `value` satisfies `plan`.
|
|
190
|
+
*
|
|
191
|
+
* Infinite `$ref` cycles that never descend into the value (`{"$ref": "#"}`)
|
|
192
|
+
* overflow the stack, as they would in any evaluator.
|
|
193
|
+
*/
|
|
194
|
+
export function validate(plan, value, sink, ann) {
|
|
195
|
+
if (plan.empty)
|
|
196
|
+
return true;
|
|
197
|
+
if (plan.types !== T_ANY && !matchesTypes(value, plan.types)) {
|
|
198
|
+
sink?.add(describeTypes(plan.types), value);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const flags = plan.flags;
|
|
202
|
+
if (flags === 0)
|
|
203
|
+
return true;
|
|
204
|
+
if (flags & F_UNEVALUATED)
|
|
205
|
+
return validateUnevaluated(plan, value, sink, ann);
|
|
206
|
+
return validateKeywords(plan, flags, value, sink, ann);
|
|
207
|
+
}
|
|
208
|
+
function validateKeywords(plan, flags, value, sink, ann) {
|
|
209
|
+
let ok = true;
|
|
210
|
+
if (flags & F_CONST &&
|
|
211
|
+
!(isPrimitive(plan.constValue) ? value === plan.constValue : jsonEqual(value, plan.constValue))) {
|
|
212
|
+
if (sink === undefined)
|
|
213
|
+
return false;
|
|
214
|
+
sink.add(describeValue(plan.constValue), value);
|
|
215
|
+
ok = false;
|
|
216
|
+
}
|
|
217
|
+
if (flags & F_ENUM) {
|
|
218
|
+
const values = plan.enumValues;
|
|
219
|
+
if (!(isPrimitive(value) ? values.includes(value) : jsonIncludes(values, value))) {
|
|
220
|
+
if (sink === undefined)
|
|
221
|
+
return false;
|
|
222
|
+
sink.add(values.map(describeValue).join(" or "), value);
|
|
223
|
+
ok = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let valid = true;
|
|
227
|
+
switch (typeof value) {
|
|
228
|
+
case "number":
|
|
229
|
+
if (flags & F_NUMBER)
|
|
230
|
+
valid = validateNumber(plan, value, sink);
|
|
231
|
+
break;
|
|
232
|
+
case "string":
|
|
233
|
+
if (flags & F_STRING)
|
|
234
|
+
valid = validateString(plan, value, sink);
|
|
235
|
+
break;
|
|
236
|
+
case "object":
|
|
237
|
+
if (value === null)
|
|
238
|
+
break;
|
|
239
|
+
if (isArray(value)) {
|
|
240
|
+
if (flags & F_ARRAY)
|
|
241
|
+
valid = validateArray(plan, value, sink, ann);
|
|
242
|
+
}
|
|
243
|
+
else if (flags & (F_PROPERTIES | F_KEYS | F_DEPENDENT)) {
|
|
244
|
+
valid = validateObject(plan, flags, value, sink, ann);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
if (!valid) {
|
|
249
|
+
if (sink === undefined)
|
|
250
|
+
return false;
|
|
251
|
+
ok = false;
|
|
252
|
+
}
|
|
253
|
+
if (flags & F_REFS) {
|
|
254
|
+
for (const ref of plan.refs) {
|
|
255
|
+
if (validate(ref, value, sink, ann))
|
|
256
|
+
continue;
|
|
257
|
+
if (sink === undefined)
|
|
258
|
+
return false;
|
|
259
|
+
ok = false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!(flags & F_APPLICATORS))
|
|
263
|
+
return ok;
|
|
264
|
+
if (plan.allOf !== undefined) {
|
|
265
|
+
for (const branch of plan.allOf) {
|
|
266
|
+
if (validate(branch, value, sink, ann))
|
|
267
|
+
continue;
|
|
268
|
+
if (sink === undefined)
|
|
269
|
+
return false;
|
|
270
|
+
ok = false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (plan.anyOf !== undefined && !validateUnion(plan, plan.anyOf, false, value, sink, ann)) {
|
|
274
|
+
if (sink === undefined)
|
|
275
|
+
return false;
|
|
276
|
+
ok = false;
|
|
277
|
+
}
|
|
278
|
+
if (plan.oneOf !== undefined && !validateUnion(plan, plan.oneOf, true, value, sink, ann)) {
|
|
279
|
+
if (sink === undefined)
|
|
280
|
+
return false;
|
|
281
|
+
ok = false;
|
|
282
|
+
}
|
|
283
|
+
if (plan.not !== undefined && validate(plan.not, value)) {
|
|
284
|
+
if (sink === undefined)
|
|
285
|
+
return false;
|
|
286
|
+
sink.add("not matching the negated schema", value);
|
|
287
|
+
ok = false;
|
|
288
|
+
}
|
|
289
|
+
if (plan.if !== undefined && !validateConditional(plan, plan.if, value, sink, ann)) {
|
|
290
|
+
if (sink === undefined)
|
|
291
|
+
return false;
|
|
292
|
+
ok = false;
|
|
293
|
+
}
|
|
294
|
+
return ok;
|
|
295
|
+
}
|
|
296
|
+
function validateNumber(plan, value, sink) {
|
|
297
|
+
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf } = plan;
|
|
298
|
+
let ok = true;
|
|
299
|
+
if (minimum !== undefined && value < minimum) {
|
|
300
|
+
if (sink === undefined)
|
|
301
|
+
return false;
|
|
302
|
+
sink.add(`at least ${minimum}`, value);
|
|
303
|
+
ok = false;
|
|
304
|
+
}
|
|
305
|
+
if (exclusiveMinimum !== undefined && value <= exclusiveMinimum) {
|
|
306
|
+
if (sink === undefined)
|
|
307
|
+
return false;
|
|
308
|
+
sink.add(`more than ${exclusiveMinimum}`, value);
|
|
309
|
+
ok = false;
|
|
310
|
+
}
|
|
311
|
+
if (maximum !== undefined && value > maximum) {
|
|
312
|
+
if (sink === undefined)
|
|
313
|
+
return false;
|
|
314
|
+
sink.add(`at most ${maximum}`, value);
|
|
315
|
+
ok = false;
|
|
316
|
+
}
|
|
317
|
+
if (exclusiveMaximum !== undefined && value >= exclusiveMaximum) {
|
|
318
|
+
if (sink === undefined)
|
|
319
|
+
return false;
|
|
320
|
+
sink.add(`less than ${exclusiveMaximum}`, value);
|
|
321
|
+
ok = false;
|
|
322
|
+
}
|
|
323
|
+
if (multipleOf !== undefined && !isMultipleOf(value, multipleOf)) {
|
|
324
|
+
if (sink === undefined)
|
|
325
|
+
return false;
|
|
326
|
+
sink.add(`divisible by ${multipleOf}`, value);
|
|
327
|
+
ok = false;
|
|
328
|
+
}
|
|
329
|
+
return ok;
|
|
330
|
+
}
|
|
331
|
+
function validateString(plan, value, sink) {
|
|
332
|
+
const { minLength, maxLength, pattern, format } = plan;
|
|
333
|
+
let ok = true;
|
|
334
|
+
if (minLength !== undefined && value.length < minLength * 2 && codePointLength(value) < minLength) {
|
|
335
|
+
if (sink === undefined)
|
|
336
|
+
return false;
|
|
337
|
+
sink.add(`at least length ${minLength}`, value);
|
|
338
|
+
ok = false;
|
|
339
|
+
}
|
|
340
|
+
if (maxLength !== undefined && value.length > maxLength && codePointLength(value) > maxLength) {
|
|
341
|
+
if (sink === undefined)
|
|
342
|
+
return false;
|
|
343
|
+
sink.add(`at most length ${maxLength}`, value);
|
|
344
|
+
ok = false;
|
|
345
|
+
}
|
|
346
|
+
if (pattern !== undefined && !pattern.test(value)) {
|
|
347
|
+
if (sink === undefined)
|
|
348
|
+
return false;
|
|
349
|
+
sink.add(`matching /${pattern.source}/`, value);
|
|
350
|
+
ok = false;
|
|
351
|
+
}
|
|
352
|
+
if (format !== undefined && !format(value)) {
|
|
353
|
+
if (sink === undefined)
|
|
354
|
+
return false;
|
|
355
|
+
sink.add(`a string in ${plan.formatName} format`, value);
|
|
356
|
+
ok = false;
|
|
357
|
+
}
|
|
358
|
+
return ok;
|
|
359
|
+
}
|
|
360
|
+
function validateArray(plan, value, sink, ann) {
|
|
361
|
+
const { minItems, maxItems, prefixItems, items, contains, maxContains } = plan;
|
|
362
|
+
const n = value.length;
|
|
363
|
+
let ok = true;
|
|
364
|
+
if (minItems !== undefined && n < minItems) {
|
|
365
|
+
if (sink === undefined)
|
|
366
|
+
return false;
|
|
367
|
+
sink.add(`an array of at least length ${minItems}`, value);
|
|
368
|
+
ok = false;
|
|
369
|
+
}
|
|
370
|
+
if (maxItems !== undefined && n > maxItems) {
|
|
371
|
+
if (sink === undefined)
|
|
372
|
+
return false;
|
|
373
|
+
sink.add(`an array of at most length ${maxItems}`, value);
|
|
374
|
+
ok = false;
|
|
375
|
+
}
|
|
376
|
+
const prefixLength = prefixItems?.length ?? 0;
|
|
377
|
+
if (prefixItems !== undefined) {
|
|
378
|
+
const end = n < prefixLength ? n : prefixLength;
|
|
379
|
+
for (let index = 0; index < end; index++) {
|
|
380
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
381
|
+
if (validate(prefixItems[index], value[index], sink))
|
|
382
|
+
continue;
|
|
383
|
+
if (sink === undefined)
|
|
384
|
+
return false;
|
|
385
|
+
sink.prefix(from, index);
|
|
386
|
+
ok = false;
|
|
387
|
+
}
|
|
388
|
+
if (ann !== undefined && end > ann.items)
|
|
389
|
+
ann.items = end;
|
|
390
|
+
}
|
|
391
|
+
if (items !== undefined) {
|
|
392
|
+
if (!items.empty) {
|
|
393
|
+
for (let index = prefixLength; index < n; index++) {
|
|
394
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
395
|
+
if (validate(items, value[index], sink))
|
|
396
|
+
continue;
|
|
397
|
+
if (sink === undefined)
|
|
398
|
+
return false;
|
|
399
|
+
sink.prefix(from, index);
|
|
400
|
+
ok = false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (ann !== undefined)
|
|
404
|
+
ann.items = Infinity;
|
|
405
|
+
}
|
|
406
|
+
if (contains !== undefined) {
|
|
407
|
+
const atLeast = plan.minContains ?? 1;
|
|
408
|
+
let count = 0;
|
|
409
|
+
for (let index = 0; index < n; index++) {
|
|
410
|
+
if (!validate(contains, value[index]))
|
|
411
|
+
continue;
|
|
412
|
+
count++;
|
|
413
|
+
if (ann !== undefined)
|
|
414
|
+
(ann.contained ??= new Set()).add(index);
|
|
415
|
+
else if (count >= atLeast && maxContains === undefined)
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
if (count < atLeast) {
|
|
419
|
+
if (sink === undefined)
|
|
420
|
+
return false;
|
|
421
|
+
sink.add(`an array containing at least ${atLeast} matching item${atLeast === 1 ? "" : "s"}`, value);
|
|
422
|
+
ok = false;
|
|
423
|
+
}
|
|
424
|
+
if (maxContains !== undefined && count > maxContains) {
|
|
425
|
+
if (sink === undefined)
|
|
426
|
+
return false;
|
|
427
|
+
sink.add(`an array containing at most ${maxContains} matching items`, value);
|
|
428
|
+
ok = false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (plan.uniqueItems && !uniqueItems(value)) {
|
|
432
|
+
if (sink === undefined)
|
|
433
|
+
return false;
|
|
434
|
+
sink.add("an array of unique items", value);
|
|
435
|
+
ok = false;
|
|
436
|
+
}
|
|
437
|
+
return ok;
|
|
438
|
+
}
|
|
439
|
+
function declares(plan, key) {
|
|
440
|
+
const properties = plan.properties;
|
|
441
|
+
if (properties === undefined)
|
|
442
|
+
return false;
|
|
443
|
+
for (const property of properties)
|
|
444
|
+
if (property.key === key)
|
|
445
|
+
return true;
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
function validateObject(plan, flags, object, sink, ann) {
|
|
449
|
+
let ok = true;
|
|
450
|
+
const { required, properties } = plan;
|
|
451
|
+
if (required !== undefined) {
|
|
452
|
+
for (const key of required) {
|
|
453
|
+
if (object[key] !== undefined && hasOwn(object, key))
|
|
454
|
+
continue;
|
|
455
|
+
if (sink === undefined)
|
|
456
|
+
return false;
|
|
457
|
+
const property = properties?.find(entry => entry.key === key);
|
|
458
|
+
sink.add(property === undefined ? "present" : expectedOf(property.plan), MISSING, key);
|
|
459
|
+
ok = false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (properties !== undefined) {
|
|
463
|
+
for (const { key, plan: property, inherited } of properties) {
|
|
464
|
+
const item = ownValue(object, key, inherited);
|
|
465
|
+
if (item === undefined)
|
|
466
|
+
continue;
|
|
467
|
+
if (ann !== undefined)
|
|
468
|
+
annotateProp(ann, key);
|
|
469
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
470
|
+
if (validate(property, item, sink))
|
|
471
|
+
continue;
|
|
472
|
+
if (sink === undefined)
|
|
473
|
+
return false;
|
|
474
|
+
sink.prefix(from, key);
|
|
475
|
+
ok = false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (flags & F_KEYS) {
|
|
479
|
+
const { patternProperties, additionalProperties, propertyNames, minProperties, maxProperties } = plan;
|
|
480
|
+
let count = 0;
|
|
481
|
+
for (const key in object) {
|
|
482
|
+
count++;
|
|
483
|
+
if (propertyNames !== undefined) {
|
|
484
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
485
|
+
if (!validate(propertyNames, key, sink)) {
|
|
486
|
+
if (sink === undefined)
|
|
487
|
+
return false;
|
|
488
|
+
sink.prefix(from, key);
|
|
489
|
+
ok = false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (patternProperties === undefined && additionalProperties === undefined)
|
|
493
|
+
continue;
|
|
494
|
+
let matched = declares(plan, key);
|
|
495
|
+
if (patternProperties !== undefined) {
|
|
496
|
+
for (const { pattern, plan: patternPlan } of patternProperties) {
|
|
497
|
+
if (!pattern.test(key))
|
|
498
|
+
continue;
|
|
499
|
+
matched = true;
|
|
500
|
+
if (ann !== undefined)
|
|
501
|
+
annotateProp(ann, key);
|
|
502
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
503
|
+
if (validate(patternPlan, object[key], sink))
|
|
504
|
+
continue;
|
|
505
|
+
if (sink === undefined)
|
|
506
|
+
return false;
|
|
507
|
+
sink.prefix(from, key);
|
|
508
|
+
ok = false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (matched || additionalProperties === undefined)
|
|
512
|
+
continue;
|
|
513
|
+
if (additionalProperties.types === 0) {
|
|
514
|
+
if (sink === undefined)
|
|
515
|
+
return false;
|
|
516
|
+
sink.add("removed", object[key], key);
|
|
517
|
+
ok = false;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
521
|
+
if (validate(additionalProperties, object[key], sink))
|
|
522
|
+
continue;
|
|
523
|
+
if (sink === undefined)
|
|
524
|
+
return false;
|
|
525
|
+
sink.prefix(from, key);
|
|
526
|
+
ok = false;
|
|
527
|
+
}
|
|
528
|
+
if (additionalProperties !== undefined && ann !== undefined)
|
|
529
|
+
ann.props = true;
|
|
530
|
+
if (minProperties !== undefined && count < minProperties) {
|
|
531
|
+
if (sink === undefined)
|
|
532
|
+
return false;
|
|
533
|
+
sink.add(`an object with at least ${minProperties} properties`, object);
|
|
534
|
+
ok = false;
|
|
535
|
+
}
|
|
536
|
+
if (maxProperties !== undefined && count > maxProperties) {
|
|
537
|
+
if (sink === undefined)
|
|
538
|
+
return false;
|
|
539
|
+
sink.add(`an object with at most ${maxProperties} properties`, object);
|
|
540
|
+
ok = false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (!(flags & F_DEPENDENT))
|
|
544
|
+
return ok;
|
|
545
|
+
if (plan.dependentRequired !== undefined) {
|
|
546
|
+
for (const dependency of plan.dependentRequired) {
|
|
547
|
+
if (object[dependency.key] === undefined || !hasOwn(object, dependency.key))
|
|
548
|
+
continue;
|
|
549
|
+
for (const key of dependency.required) {
|
|
550
|
+
if (object[key] !== undefined && hasOwn(object, key))
|
|
551
|
+
continue;
|
|
552
|
+
if (sink === undefined)
|
|
553
|
+
return false;
|
|
554
|
+
sink.add(`present when ${dependency.key} is present`, MISSING, key);
|
|
555
|
+
ok = false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (plan.dependentSchemas !== undefined) {
|
|
560
|
+
for (const { key, plan: dependency, inherited } of plan.dependentSchemas) {
|
|
561
|
+
if (ownValue(object, key, inherited) === undefined)
|
|
562
|
+
continue;
|
|
563
|
+
if (validate(dependency, object, sink, ann))
|
|
564
|
+
continue;
|
|
565
|
+
if (sink === undefined)
|
|
566
|
+
return false;
|
|
567
|
+
ok = false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return ok;
|
|
571
|
+
}
|
|
572
|
+
function validateUnion(plan, branches, exclusive, value, sink, ann) {
|
|
573
|
+
const tagged = plan;
|
|
574
|
+
const slot = exclusive ? kOneOf : kAnyOf;
|
|
575
|
+
let discriminator = tagged[slot];
|
|
576
|
+
if (discriminator === undefined) {
|
|
577
|
+
discriminator = findDiscriminator(branches) ?? null;
|
|
578
|
+
tagged[slot] = discriminator;
|
|
579
|
+
}
|
|
580
|
+
if (discriminator !== null) {
|
|
581
|
+
if (!isObject(value)) {
|
|
582
|
+
sink?.add("an object", value);
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const tag = value[discriminator.key];
|
|
586
|
+
const index = discriminator.branches.get(tag);
|
|
587
|
+
if (index !== undefined)
|
|
588
|
+
return validate(branches[index], value, sink, ann);
|
|
589
|
+
sink?.add([...discriminator.branches.keys()].map(describeValue).join(" or "), tag === undefined ? MISSING : tag, discriminator.key);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (exclusive) {
|
|
593
|
+
let matched = -1;
|
|
594
|
+
let matchedAnn;
|
|
595
|
+
for (let index = 0; index < branches.length; index++) {
|
|
596
|
+
const local = ann === undefined ? undefined : newAnn();
|
|
597
|
+
if (!validate(branches[index], value, undefined, local))
|
|
598
|
+
continue;
|
|
599
|
+
if (matched >= 0) {
|
|
600
|
+
sink?.add("exactly one matching oneOf schema", value);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
matched = index;
|
|
604
|
+
matchedAnn = local;
|
|
605
|
+
}
|
|
606
|
+
if (matched >= 0) {
|
|
607
|
+
if (ann !== undefined && matchedAnn !== undefined)
|
|
608
|
+
mergeAnn(ann, matchedAnn);
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
else if (ann === undefined) {
|
|
613
|
+
for (const branch of branches)
|
|
614
|
+
if (validate(branch, value))
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
let ok = false;
|
|
619
|
+
for (const branch of branches) {
|
|
620
|
+
const local = newAnn();
|
|
621
|
+
if (!validate(branch, value, undefined, local))
|
|
622
|
+
continue;
|
|
623
|
+
ok = true;
|
|
624
|
+
mergeAnn(ann, local);
|
|
625
|
+
}
|
|
626
|
+
if (ok)
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
if (sink !== undefined)
|
|
630
|
+
explainBranches(branches, value, sink);
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
function validateConditional(plan, condition, value, sink, ann) {
|
|
634
|
+
const { ifThen: then, ifElse: otherwise } = plan;
|
|
635
|
+
if (ann === undefined) {
|
|
636
|
+
if (then === undefined && otherwise === undefined)
|
|
637
|
+
return true;
|
|
638
|
+
if (validate(condition, value))
|
|
639
|
+
return then === undefined || validate(then, value, sink);
|
|
640
|
+
return otherwise === undefined || validate(otherwise, value, sink);
|
|
641
|
+
}
|
|
642
|
+
const local = newAnn();
|
|
643
|
+
if (validate(condition, value, undefined, local)) {
|
|
644
|
+
mergeAnn(ann, local);
|
|
645
|
+
return then === undefined || validate(then, value, sink, ann);
|
|
646
|
+
}
|
|
647
|
+
return otherwise === undefined || validate(otherwise, value, sink, ann);
|
|
648
|
+
}
|
|
649
|
+
/** Plans with `unevaluated*`: run every other keyword collecting annotations, then check what is left. */
|
|
650
|
+
function validateUnevaluated(plan, value, sink, ann) {
|
|
651
|
+
const local = newAnn();
|
|
652
|
+
let ok = validateKeywords(plan, plan.flags, value, sink, local);
|
|
653
|
+
if (!ok && sink === undefined)
|
|
654
|
+
return false;
|
|
655
|
+
const properties = plan.unevaluatedProperties;
|
|
656
|
+
if (properties !== undefined && isObject(value)) {
|
|
657
|
+
const evaluated = local.props;
|
|
658
|
+
if (evaluated !== true) {
|
|
659
|
+
for (const key in value) {
|
|
660
|
+
if (evaluated?.has(key) === true)
|
|
661
|
+
continue;
|
|
662
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
663
|
+
if (validate(properties, value[key], sink))
|
|
664
|
+
continue;
|
|
665
|
+
if (sink === undefined)
|
|
666
|
+
return false;
|
|
667
|
+
if (properties.types === 0) {
|
|
668
|
+
sink.issues.length = from;
|
|
669
|
+
sink.add("removed", value[key], key);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
sink.prefix(from, key);
|
|
673
|
+
}
|
|
674
|
+
ok = false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
local.props = true;
|
|
678
|
+
}
|
|
679
|
+
const items = plan.unevaluatedItems;
|
|
680
|
+
if (items !== undefined && isArray(value)) {
|
|
681
|
+
for (let index = local.items; index < value.length; index++) {
|
|
682
|
+
if (local.contained?.has(index) === true)
|
|
683
|
+
continue;
|
|
684
|
+
const from = sink === undefined ? 0 : sink.issues.length;
|
|
685
|
+
if (validate(items, value[index], sink))
|
|
686
|
+
continue;
|
|
687
|
+
if (sink === undefined)
|
|
688
|
+
return false;
|
|
689
|
+
sink.prefix(from, index);
|
|
690
|
+
ok = false;
|
|
691
|
+
}
|
|
692
|
+
local.items = Infinity;
|
|
693
|
+
}
|
|
694
|
+
if (ok && ann !== undefined)
|
|
695
|
+
mergeAnn(ann, local);
|
|
696
|
+
return ok;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Report why no union branch matched: the branch that got deepest into the
|
|
700
|
+
* value explains it best; failures at the value itself become one
|
|
701
|
+
* alternatives issue (`a string or null`).
|
|
702
|
+
*/
|
|
703
|
+
function explainBranches(branches, value, sink) {
|
|
704
|
+
let best;
|
|
705
|
+
let bestDepth = -1;
|
|
706
|
+
const shallow = [];
|
|
707
|
+
for (const branch of branches) {
|
|
708
|
+
const local = new Sink();
|
|
709
|
+
validate(branch, value, local);
|
|
710
|
+
let depth = 0;
|
|
711
|
+
for (const issue of local.issues)
|
|
712
|
+
if (issue.path.length > depth)
|
|
713
|
+
depth = issue.path.length;
|
|
714
|
+
if (depth === 0) {
|
|
715
|
+
for (const issue of local.issues)
|
|
716
|
+
if (!shallow.includes(issue.expected))
|
|
717
|
+
shallow.push(issue.expected);
|
|
718
|
+
}
|
|
719
|
+
if (depth > bestDepth || (depth === bestDepth && best !== undefined && local.issues.length < best.length)) {
|
|
720
|
+
best = local.issues;
|
|
721
|
+
bestDepth = depth;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (bestDepth > 0 && best !== undefined) {
|
|
725
|
+
sink.issues.push(...best);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
sink.add(shallow.length > 0 ? shallow.join(" or ") : "matching a union branch", value);
|
|
729
|
+
}
|