@texaryn/schema-json 0.7.1 → 0.8.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 +24 -0
- package/README.md +28 -0
- package/THIRD-PARTY-NOTICES.md +7 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +464 -13
- package/dist/adapter.js.map +1 -1
- package/dist/bare-maps.js +1 -1
- package/dist/identity.d.ts +11 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +103 -22
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/local-pointer-aliases.d.ts +4 -0
- package/dist/local-pointer-aliases.d.ts.map +1 -0
- package/dist/local-pointer-aliases.js +345 -0
- package/dist/local-pointer-aliases.js.map +1 -0
- package/dist/normalize.d.ts +1 -1
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +3 -1
- package/dist/normalize.js.map +1 -1
- package/dist/projection-validation-coherence.d.ts +11 -0
- package/dist/projection-validation-coherence.d.ts.map +1 -0
- package/dist/projection-validation-coherence.js +213 -0
- package/dist/projection-validation-coherence.js.map +1 -0
- package/dist/projection.d.ts +2 -2
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +917 -152
- package/dist/projection.js.map +1 -1
- package/dist/resources.d.ts +19 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +730 -0
- package/dist/resources.js.map +1 -0
- package/dist/root-reference.js +1 -1
- package/dist/schema-graph.d.ts +7 -0
- package/dist/schema-graph.d.ts.map +1 -1
- package/dist/schema-graph.js +79 -20
- package/dist/schema-graph.js.map +1 -1
- package/dist/submission-projection.d.ts +5 -0
- package/dist/submission-projection.d.ts.map +1 -0
- package/dist/submission-projection.js +233 -0
- package/dist/submission-projection.js.map +1 -0
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/json-schema-library/LICENSE.md +9 -0
- package/dist/vendor/json-schema-library/index.mjs +2 -0
- package/dist/vendor/json-schema-library/index.mjs.map +1 -0
- package/package.json +8 -3
package/dist/projection.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mergeNode, isSchemaNode } from 'json-schema-library';
|
|
2
|
-
import { childDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, } from './identity.js';
|
|
1
|
+
import { mergeNode, isSchemaNode } from './vendor/json-schema-library/index.mjs';
|
|
2
|
+
import { authoredSchema, childDeclaring, extendDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, referenceKeywords, referenceOf, } from './identity.js';
|
|
3
3
|
import { inferProjectionShape, shapeDiagnostic } from './projection-shape.js';
|
|
4
4
|
import { POSITION } from './schema-graph.js';
|
|
5
5
|
const VALID_TYPES = new Set([
|
|
@@ -11,6 +11,18 @@ const VALID_TYPES = new Set([
|
|
|
11
11
|
'array',
|
|
12
12
|
'null',
|
|
13
13
|
]);
|
|
14
|
+
const NON_ASSERTION_REFERENCE_SIBLINGS = new Set([
|
|
15
|
+
'$id',
|
|
16
|
+
'$anchor',
|
|
17
|
+
'$dynamicAnchor',
|
|
18
|
+
'$recursiveAnchor',
|
|
19
|
+
'$schema',
|
|
20
|
+
'$vocabulary',
|
|
21
|
+
'$comment',
|
|
22
|
+
'$defs',
|
|
23
|
+
'definitions',
|
|
24
|
+
POSITION,
|
|
25
|
+
]);
|
|
14
26
|
function toPointer(value) {
|
|
15
27
|
return value;
|
|
16
28
|
}
|
|
@@ -84,8 +96,8 @@ function extractAnnotations(schema,
|
|
|
84
96
|
* Set where two declarations apply to this location whatever the instance is
|
|
85
97
|
* and disagree. The merged schema still carries one of them, and which one is
|
|
86
98
|
* the merge's traversal order rather than an answer.
|
|
87
|
-
|
|
88
|
-
omitDefault = false) {
|
|
99
|
+
*/
|
|
100
|
+
omitDefault = false, defaultOverride) {
|
|
89
101
|
const annotations = {};
|
|
90
102
|
if (typeof schema.title === 'string')
|
|
91
103
|
annotations.title = schema.title;
|
|
@@ -99,8 +111,15 @@ omitDefault = false) {
|
|
|
99
111
|
annotations.deprecated = schema.deprecated;
|
|
100
112
|
if (Array.isArray(schema.examples))
|
|
101
113
|
annotations.examples = schema.examples;
|
|
102
|
-
if (!omitDefault
|
|
103
|
-
|
|
114
|
+
if (!omitDefault) {
|
|
115
|
+
if (defaultOverride) {
|
|
116
|
+
if (defaultOverride.present)
|
|
117
|
+
annotations.default = defaultOverride.value;
|
|
118
|
+
}
|
|
119
|
+
else if ('default' in schema) {
|
|
120
|
+
annotations.default = schema.default;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
104
123
|
return annotations;
|
|
105
124
|
}
|
|
106
125
|
function extractEnumValues(schema) {
|
|
@@ -109,30 +128,339 @@ function extractEnumValues(schema) {
|
|
|
109
128
|
return schema.enum.map((value) => ({ value }));
|
|
110
129
|
}
|
|
111
130
|
function dereferenceChecked(node) {
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
131
|
+
const completed = new Set();
|
|
132
|
+
const active = new Set();
|
|
133
|
+
const parts = [];
|
|
134
|
+
let cycle = false;
|
|
135
|
+
let unresolved = false;
|
|
136
|
+
const visit = (current, referringSite = false) => {
|
|
115
137
|
const position = positionOf(current);
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
if (active.has(position)) {
|
|
139
|
+
cycle = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (completed.has(position))
|
|
143
|
+
return;
|
|
144
|
+
const references = referenceKeywords(current);
|
|
145
|
+
if (references.length === 0) {
|
|
146
|
+
parts.push(current);
|
|
147
|
+
completed.add(position);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!referringSite && current.getDraftVersion() !== 'draft-07' && !onlyPoints(current)) {
|
|
151
|
+
parts.push(current);
|
|
152
|
+
completed.add(position);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
active.add(position);
|
|
156
|
+
for (const keyword of references) {
|
|
157
|
+
const target = followRef(current, keyword);
|
|
158
|
+
if (target === undefined || !isSchemaNode(target)) {
|
|
159
|
+
unresolved = true;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
visit(target);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (current.getDraftVersion() !== 'draft-07') {
|
|
166
|
+
const sibling = mergeNode(current, current, ...references);
|
|
167
|
+
const siblingSchema = sibling?.schema;
|
|
168
|
+
if (isSchemaNode(sibling) &&
|
|
169
|
+
Object.keys(siblingSchema ?? {}).some((keyword) => !NON_ASSERTION_REFERENCE_SIBLINGS.has(keyword))) {
|
|
170
|
+
parts.push(sibling);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
active.delete(position);
|
|
174
|
+
completed.add(position);
|
|
175
|
+
};
|
|
176
|
+
visit(node, true);
|
|
177
|
+
if (cycle)
|
|
178
|
+
return { node, cycle: true, unresolved: false };
|
|
179
|
+
if (unresolved || parts.length === 0)
|
|
180
|
+
return { node, cycle: false, unresolved: true };
|
|
181
|
+
if (parts.length === 1)
|
|
182
|
+
return { node: parts[0], cycle: false, unresolved: false };
|
|
183
|
+
const position = JSON.stringify(parts.map(positionOf));
|
|
184
|
+
const type = referenceCompositionType(parts);
|
|
185
|
+
const oneOfSource = parts.find((part) => part.oneOf !== undefined);
|
|
186
|
+
const anyOfSource = parts.find((part) => part.anyOf !== undefined);
|
|
187
|
+
const projectionFields = {};
|
|
188
|
+
const projectionKeys = [
|
|
189
|
+
'format',
|
|
190
|
+
'minLength',
|
|
191
|
+
'maxLength',
|
|
192
|
+
'pattern',
|
|
193
|
+
'minimum',
|
|
194
|
+
'maximum',
|
|
195
|
+
'exclusiveMinimum',
|
|
196
|
+
'exclusiveMaximum',
|
|
197
|
+
'multipleOf',
|
|
198
|
+
'minItems',
|
|
199
|
+
'maxItems',
|
|
200
|
+
'uniqueItems',
|
|
201
|
+
'enum',
|
|
202
|
+
'title',
|
|
203
|
+
'description',
|
|
204
|
+
'readOnly',
|
|
205
|
+
'writeOnly',
|
|
206
|
+
'deprecated',
|
|
207
|
+
'examples',
|
|
208
|
+
];
|
|
209
|
+
for (const part of parts) {
|
|
210
|
+
const record = part.schema;
|
|
211
|
+
if (!record || typeof record !== 'object')
|
|
212
|
+
continue;
|
|
213
|
+
for (const keyword of projectionKeys) {
|
|
214
|
+
if (keyword in record)
|
|
215
|
+
projectionFields[keyword] = record[keyword];
|
|
123
216
|
}
|
|
124
|
-
current = next;
|
|
125
|
-
if (!onlyPoints(current))
|
|
126
|
-
break;
|
|
127
217
|
}
|
|
128
|
-
|
|
218
|
+
const schema = {
|
|
219
|
+
...projectionFields,
|
|
220
|
+
[POSITION]: `reference-composition:${position}`,
|
|
221
|
+
...(type === undefined ? {} : { type }),
|
|
222
|
+
// Compile a harmless branch so json-schema-library installs the allOf
|
|
223
|
+
// reducer, then attach the already compiled components below. Recompiling
|
|
224
|
+
// their raw schemas here would resolve relative references in the first
|
|
225
|
+
// component's resource scope instead of each component's original scope.
|
|
226
|
+
allOf: [{}],
|
|
227
|
+
};
|
|
228
|
+
const first = parts[0];
|
|
229
|
+
const composed = first.compileSchema(schema, `${first.evaluationPath}/$ref`, `${first.schemaLocation}/reference-composition`);
|
|
230
|
+
composed.schema = {
|
|
231
|
+
...composed.schema,
|
|
232
|
+
allOf: parts.map((part) => part.schema),
|
|
233
|
+
...(oneOfSource?.oneOf ? { oneOf: oneOfSource.oneOf.map((branch) => branch.schema) } : {}),
|
|
234
|
+
...(anyOfSource?.anyOf ? { anyOf: anyOfSource.anyOf.map((branch) => branch.schema) } : {}),
|
|
235
|
+
};
|
|
236
|
+
composed.allOf = [...parts];
|
|
237
|
+
if (oneOfSource?.oneOf)
|
|
238
|
+
composed.oneOf = [...oneOfSource.oneOf];
|
|
239
|
+
if (anyOfSource?.anyOf)
|
|
240
|
+
composed.anyOf = [...anyOfSource.anyOf];
|
|
241
|
+
composed.reduceNode = (data, options = {}) => {
|
|
242
|
+
let reduced;
|
|
243
|
+
let reductionFailed = false;
|
|
244
|
+
let reductionError;
|
|
245
|
+
for (const part of parts) {
|
|
246
|
+
const outcome = part.reduceNode(data, options);
|
|
247
|
+
if (!isSchemaNode(outcome.node)) {
|
|
248
|
+
reductionFailed = true;
|
|
249
|
+
reductionError ??= outcome.error;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const candidate = outcome.node;
|
|
253
|
+
reduced = reduced ? mergeNode(reduced, candidate) : candidate;
|
|
254
|
+
}
|
|
255
|
+
if (reductionFailed)
|
|
256
|
+
return { node: undefined, error: reductionError };
|
|
257
|
+
if (!reduced)
|
|
258
|
+
return { node: composed, error: undefined };
|
|
259
|
+
const reducedSchema = {
|
|
260
|
+
...reduced.schema,
|
|
261
|
+
[POSITION]: `reference-composition:${position}`,
|
|
262
|
+
};
|
|
263
|
+
if (type !== undefined)
|
|
264
|
+
reducedSchema.type = type;
|
|
265
|
+
const reducedNode = { ...reduced, schema: reducedSchema, allOf: undefined };
|
|
266
|
+
if (type !== undefined)
|
|
267
|
+
reducedNode.type = type;
|
|
268
|
+
return { node: reducedNode, error: undefined };
|
|
269
|
+
};
|
|
270
|
+
return { node: composed, cycle: false, unresolved: false };
|
|
129
271
|
}
|
|
130
272
|
/** Follows a $ref to the node it points at; returns the node unchanged otherwise. */
|
|
131
273
|
function dereference(node) {
|
|
132
274
|
return dereferenceChecked(node).node;
|
|
133
275
|
}
|
|
276
|
+
function visitReferenceDefaultSites(node, cache, visitor, dynamicReferenceProjection = false) {
|
|
277
|
+
if (cache.dialect === 'draft-07' || referenceOf(node) === undefined)
|
|
278
|
+
return;
|
|
279
|
+
const pending = [node];
|
|
280
|
+
const visited = new Set();
|
|
281
|
+
while (pending.length > 0) {
|
|
282
|
+
const current = pending.pop();
|
|
283
|
+
const position = positionOf(current);
|
|
284
|
+
if (visited.has(position))
|
|
285
|
+
continue;
|
|
286
|
+
visited.add(position);
|
|
287
|
+
const authored = authoredSchema(current, cache);
|
|
288
|
+
if (authored !== null &&
|
|
289
|
+
typeof authored === 'object' &&
|
|
290
|
+
'default' in authored) {
|
|
291
|
+
visitor(current);
|
|
292
|
+
}
|
|
293
|
+
for (const keyword of referenceKeywords(current)) {
|
|
294
|
+
if (dynamicReferenceProjection && (keyword === '$dynamicRef' || keyword === '$recursiveRef'))
|
|
295
|
+
continue;
|
|
296
|
+
const target = followRef(current, keyword);
|
|
297
|
+
if (target)
|
|
298
|
+
pending.push(target);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function traversalKey(node) {
|
|
303
|
+
const parent = node.parent;
|
|
304
|
+
const isInPlaceBranch = parent !== undefined &&
|
|
305
|
+
['if', 'then', 'else', 'contains'].some((keyword) => parent[keyword] === node || node.evaluationPath === `${parent.evaluationPath}/${keyword}`);
|
|
306
|
+
if (isInPlaceBranch) {
|
|
307
|
+
return positionOf(node);
|
|
308
|
+
}
|
|
309
|
+
const location = node.schemaLocation;
|
|
310
|
+
return typeof location === 'string' ? location : positionOf(node);
|
|
311
|
+
}
|
|
312
|
+
function allOfExplicitType(node, visited = new Set()) {
|
|
313
|
+
const position = positionOf(node);
|
|
314
|
+
if (visited.has(position))
|
|
315
|
+
return undefined;
|
|
316
|
+
visited.add(position);
|
|
317
|
+
for (const branch of node.allOf ?? []) {
|
|
318
|
+
const reference = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
|
|
319
|
+
if (reference?.unresolved || (reference && !isSchemaNode(reference.node)))
|
|
320
|
+
continue;
|
|
321
|
+
const target = reference?.node ?? branch;
|
|
322
|
+
const schema = target.schema;
|
|
323
|
+
if (schema && typeof schema === 'object') {
|
|
324
|
+
const type = resolveExplicitType(schema);
|
|
325
|
+
if (type)
|
|
326
|
+
return type;
|
|
327
|
+
}
|
|
328
|
+
const nested = allOfExplicitType(target, visited);
|
|
329
|
+
if (nested)
|
|
330
|
+
return nested;
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
function declaresObject(node, visited = new Set()) {
|
|
335
|
+
const resolved = dereference(node);
|
|
336
|
+
if (!isSchemaNode(resolved))
|
|
337
|
+
return false;
|
|
338
|
+
const position = positionOf(resolved);
|
|
339
|
+
if (visited.has(position))
|
|
340
|
+
return false;
|
|
341
|
+
visited.add(position);
|
|
342
|
+
const schema = resolved.schema;
|
|
343
|
+
if (!schema || typeof schema !== 'object')
|
|
344
|
+
return false;
|
|
345
|
+
const explicitType = resolveExplicitType(schema);
|
|
346
|
+
if (explicitType)
|
|
347
|
+
return explicitType === 'object';
|
|
348
|
+
const allOfType = allOfExplicitType(resolved);
|
|
349
|
+
if (allOfType)
|
|
350
|
+
return allOfType === 'object';
|
|
351
|
+
const shape = inferProjectionShape(schema);
|
|
352
|
+
if (shape.kind === 'resolved')
|
|
353
|
+
return shape.type === 'object';
|
|
354
|
+
return (resolved.allOf ?? []).some((branch) => declaresObject(branch, visited));
|
|
355
|
+
}
|
|
356
|
+
function declaresArray(node, visited = new Set()) {
|
|
357
|
+
const resolved = dereference(node);
|
|
358
|
+
if (!isSchemaNode(resolved))
|
|
359
|
+
return false;
|
|
360
|
+
const position = positionOf(resolved);
|
|
361
|
+
if (visited.has(position))
|
|
362
|
+
return false;
|
|
363
|
+
visited.add(position);
|
|
364
|
+
const schema = resolved.schema;
|
|
365
|
+
if (!schema || typeof schema !== 'object')
|
|
366
|
+
return false;
|
|
367
|
+
const explicitType = resolveExplicitType(schema);
|
|
368
|
+
if (explicitType)
|
|
369
|
+
return explicitType === 'array';
|
|
370
|
+
const allOfType = allOfExplicitType(resolved);
|
|
371
|
+
if (allOfType)
|
|
372
|
+
return allOfType === 'array';
|
|
373
|
+
const shape = inferProjectionShape(schema);
|
|
374
|
+
if (shape.kind === 'resolved')
|
|
375
|
+
return shape.type === 'array';
|
|
376
|
+
return (resolved.allOf ?? []).some((branch) => declaresArray(branch, visited));
|
|
377
|
+
}
|
|
378
|
+
function referenceCompositionType(parts) {
|
|
379
|
+
const explicit = new Set(parts
|
|
380
|
+
.map((part) => {
|
|
381
|
+
const schema = part.schema;
|
|
382
|
+
return (schema && typeof schema === 'object' ? resolveExplicitType(schema) : undefined) ?? allOfExplicitType(part);
|
|
383
|
+
})
|
|
384
|
+
.filter((type) => type !== undefined));
|
|
385
|
+
if (explicit.size === 1)
|
|
386
|
+
return [...explicit][0];
|
|
387
|
+
if (explicit.size > 1)
|
|
388
|
+
return undefined;
|
|
389
|
+
const object = parts.some((part) => declaresObject(part));
|
|
390
|
+
const array = parts.some((part) => declaresArray(part));
|
|
391
|
+
if (object === array)
|
|
392
|
+
return undefined;
|
|
393
|
+
return object ? 'object' : 'array';
|
|
394
|
+
}
|
|
395
|
+
function hasReferencedAllOf(node, visited = new Set()) {
|
|
396
|
+
const position = positionOf(node);
|
|
397
|
+
if (visited.has(position))
|
|
398
|
+
return false;
|
|
399
|
+
visited.add(position);
|
|
400
|
+
for (const branch of node.allOf ?? []) {
|
|
401
|
+
if (referenceOf(branch) !== undefined)
|
|
402
|
+
return true;
|
|
403
|
+
if (hasReferencedAllOf(branch, visited))
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
function hasUnresolvedAllOfReference(node, visited = new Set()) {
|
|
409
|
+
const position = positionOf(node);
|
|
410
|
+
if (visited.has(position))
|
|
411
|
+
return false;
|
|
412
|
+
visited.add(position);
|
|
413
|
+
for (const branch of node.allOf ?? []) {
|
|
414
|
+
const source = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
|
|
415
|
+
if (source?.unresolved || (source && !isSchemaNode(source.node)))
|
|
416
|
+
return true;
|
|
417
|
+
const target = source?.node ?? branch;
|
|
418
|
+
if (hasUnresolvedAllOfReference(target, visited))
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
function expandAllOfReferences(node, stack = new Set()) {
|
|
424
|
+
const position = positionOf(node);
|
|
425
|
+
if (stack.has(position))
|
|
426
|
+
return node;
|
|
427
|
+
const nextStack = new Set(stack).add(position);
|
|
428
|
+
const branches = [];
|
|
429
|
+
let changed = false;
|
|
430
|
+
for (const branch of node.allOf ?? []) {
|
|
431
|
+
if (referenceOf(branch) !== undefined) {
|
|
432
|
+
const { node: target, unresolved } = dereferenceChecked(branch);
|
|
433
|
+
if (unresolved || !isSchemaNode(target))
|
|
434
|
+
return undefined;
|
|
435
|
+
if (nextStack.has(positionOf(target))) {
|
|
436
|
+
branches.push(branch);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const expanded = expandAllOfReferences(target, nextStack);
|
|
440
|
+
if (!expanded)
|
|
441
|
+
return undefined;
|
|
442
|
+
branches.push(expanded);
|
|
443
|
+
changed = true;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const expanded = expandAllOfReferences(branch, nextStack);
|
|
447
|
+
if (!expanded)
|
|
448
|
+
return undefined;
|
|
449
|
+
branches.push(expanded);
|
|
450
|
+
changed ||= expanded !== branch;
|
|
451
|
+
}
|
|
452
|
+
return changed ? { ...node, allOf: branches } : node;
|
|
453
|
+
}
|
|
454
|
+
function reduceAllOfForProjection(node, data, scope = []) {
|
|
455
|
+
if (hasUnresolvedAllOfReference(node))
|
|
456
|
+
return undefined;
|
|
457
|
+
if (node.getDraftVersion() !== 'draft-07')
|
|
458
|
+
return node.reduceNode(data, { path: [...scope] }).node;
|
|
459
|
+
const expanded = expandAllOfReferences(node);
|
|
460
|
+
return expanded?.reduceNode(data, { path: [...scope] }).node;
|
|
461
|
+
}
|
|
134
462
|
// A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
|
|
135
|
-
function resolveSelectedBranch(original, reduced, data) {
|
|
463
|
+
function resolveSelectedBranch(original, reduced, data, scope = [], branchApplicability) {
|
|
136
464
|
const reducedSchema = reduced.schema;
|
|
137
465
|
if (reducedSchema && typeof reducedSchema === 'object') {
|
|
138
466
|
if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
|
|
@@ -141,26 +469,41 @@ function resolveSelectedBranch(original, reduced, data) {
|
|
|
141
469
|
}
|
|
142
470
|
const selected = [];
|
|
143
471
|
if (original.oneOf) {
|
|
144
|
-
const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
|
|
472
|
+
const matching = original.oneOf.filter((branch) => branchApplies(branch, data, scope, branchApplicability));
|
|
145
473
|
if (matching.length === 1)
|
|
146
474
|
selected.push(matching[0]);
|
|
147
475
|
}
|
|
148
476
|
for (const branch of original.anyOf ?? []) {
|
|
149
|
-
if (branchApplies(branch, data))
|
|
477
|
+
if (branchApplies(branch, data, scope, branchApplicability))
|
|
150
478
|
selected.push(branch);
|
|
151
479
|
}
|
|
152
|
-
if (!selected.some((branch) =>
|
|
480
|
+
if (!selected.some((branch) => referenceOf(branch) !== undefined))
|
|
153
481
|
return reduced;
|
|
154
482
|
let merged = reduced;
|
|
155
483
|
for (const branch of selected) {
|
|
156
484
|
const target = dereference(branch);
|
|
157
485
|
if (!isSchemaNode(target))
|
|
158
486
|
continue;
|
|
159
|
-
const targetReduced = target.reduceNode(data).node ?? target;
|
|
487
|
+
const targetReduced = target.reduceNode(data, { path: [...scope] }).node ?? target;
|
|
160
488
|
merged = mergeNode(merged, targetReduced) ?? merged;
|
|
161
489
|
}
|
|
162
490
|
return merged;
|
|
163
491
|
}
|
|
492
|
+
/** Fills properties a selected Draft 7 conditional `$ref` loses during reduction. */
|
|
493
|
+
function resolveSelectedConditionalReference(original, reduced, data, scope = [], branchApplicability) {
|
|
494
|
+
if (original.getDraftVersion() !== 'draft-07' || !original.if)
|
|
495
|
+
return reduced;
|
|
496
|
+
const selected = branchApplies(original.if, data, scope, branchApplicability) ? original.then : original.else;
|
|
497
|
+
if (!selected || typeof selected.$ref !== 'string')
|
|
498
|
+
return reduced;
|
|
499
|
+
const target = dereference(selected);
|
|
500
|
+
if (!isSchemaNode(target))
|
|
501
|
+
return reduced;
|
|
502
|
+
const targetReduced = reduceAllOfForProjection(target, data, scope) ?? target;
|
|
503
|
+
const reducedProperties = reduced.properties ?? {};
|
|
504
|
+
const hasUnprojectedProperty = Object.keys(targetReduced.properties ?? {}).some((key) => !Object.hasOwn(reducedProperties, key));
|
|
505
|
+
return hasUnprojectedProperty ? mergeNode(reduced, targetReduced) ?? reduced : reduced;
|
|
506
|
+
}
|
|
164
507
|
export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
|
|
165
508
|
function walkOrder(a, b) {
|
|
166
509
|
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
@@ -168,10 +511,19 @@ function walkOrder(a, b) {
|
|
|
168
511
|
return a[i] - b[i];
|
|
169
512
|
return a.length - b.length;
|
|
170
513
|
}
|
|
171
|
-
function
|
|
514
|
+
function boundaryToken(pointer, schemaKey, generation) {
|
|
515
|
+
return JSON.stringify([pointer, schemaKey, generation]);
|
|
516
|
+
}
|
|
517
|
+
function addBoundary(ctx, pointer, reason, targetPointer, schemaKey) {
|
|
172
518
|
const reasons = ctx.boundaries.get(pointer) ?? new Set();
|
|
173
519
|
reasons.add(reason);
|
|
174
520
|
ctx.boundaries.set(pointer, reasons);
|
|
521
|
+
const targets = ctx.boundaryTargets.get(pointer) ?? new Map();
|
|
522
|
+
if (![...targets.values()].some((target) => target.reason === reason)) {
|
|
523
|
+
const token = boundaryToken(targetPointer, schemaKey, ctx.boundaryGeneration);
|
|
524
|
+
targets.set(token, { pointer: toPointer(targetPointer), reason, token });
|
|
525
|
+
}
|
|
526
|
+
ctx.boundaryTargets.set(pointer, targets);
|
|
175
527
|
}
|
|
176
528
|
// A leaf needs no reduction, so it rides with its parent's budget unit.
|
|
177
529
|
function isLeafSchema(schema) {
|
|
@@ -184,17 +536,14 @@ function isLeafSchema(schema) {
|
|
|
184
536
|
return ('enum' in record || 'const' in record) && !('properties' in record);
|
|
185
537
|
}
|
|
186
538
|
/**
|
|
187
|
-
* The position
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* adapters report sources in this form so a diagnostic means the same thing
|
|
193
|
-
* whichever one produced it.
|
|
539
|
+
* The authored schema position as a JSON Pointer with the root as the empty
|
|
540
|
+
* string. Reference sites and targets keep separate positions, and encoded
|
|
541
|
+
* reference segments use their document spelling. Both adapters report
|
|
542
|
+
* sources in this form so diagnostics mean the same thing whichever produced
|
|
543
|
+
* them.
|
|
194
544
|
*/
|
|
195
545
|
function documentPointer(node) {
|
|
196
|
-
|
|
197
|
-
return typeof location === 'string' ? location.replace(/^#/, '') : '';
|
|
546
|
+
return positionOf(node).replace(/^#/, '');
|
|
198
547
|
}
|
|
199
548
|
/**
|
|
200
549
|
* Visits every schema position that applies to one location whatever the
|
|
@@ -217,16 +566,13 @@ function documentPointer(node) {
|
|
|
217
566
|
* several places at once: `properties/x` on the node's own schema and on each
|
|
218
567
|
* of its `allOf` branches are all declarations of the same location.
|
|
219
568
|
*/
|
|
220
|
-
function eachUnconditional(roots, visitor) {
|
|
569
|
+
function eachUnconditional(roots, visitor, referenceVisitor, cache) {
|
|
221
570
|
const visited = new Set();
|
|
222
571
|
const visit = (current) => {
|
|
572
|
+
if (referenceVisitor && cache)
|
|
573
|
+
visitReferenceDefaultSites(current, cache, referenceVisitor);
|
|
223
574
|
const resolved = dereference(current);
|
|
224
|
-
|
|
225
|
-
// `collectCandidateProperties`: `resolveRef()` returns a fresh node every
|
|
226
|
-
// call, so identity alone never closes a cycle, and an inline branch has no
|
|
227
|
-
// location of its own to key on.
|
|
228
|
-
const location = resolved.schemaLocation;
|
|
229
|
-
const key = typeof location === 'string' ? location : resolved;
|
|
575
|
+
const key = traversalKey(resolved);
|
|
230
576
|
if (visited.has(key))
|
|
231
577
|
return;
|
|
232
578
|
visited.add(key);
|
|
@@ -249,9 +595,15 @@ function eachUnconditional(roots, visitor) {
|
|
|
249
595
|
* own descent already survives the shapes that throw (#119, #121), and a
|
|
250
596
|
* conflict is the wrong place to surface one.
|
|
251
597
|
*/
|
|
252
|
-
function branchApplies(branch, data) {
|
|
598
|
+
function branchApplies(branch, data, scope = [], branchApplicability) {
|
|
599
|
+
if (data === undefined && branchApplicability?.has(branch)) {
|
|
600
|
+
return branchApplicability.get(branch);
|
|
601
|
+
}
|
|
253
602
|
try {
|
|
254
|
-
|
|
603
|
+
const applies = branch.validate(data, '#', [...scope]).valid === true;
|
|
604
|
+
if (data === undefined)
|
|
605
|
+
branchApplicability?.set(branch, applies);
|
|
606
|
+
return applies;
|
|
255
607
|
}
|
|
256
608
|
catch {
|
|
257
609
|
return false;
|
|
@@ -270,18 +622,23 @@ function branchApplies(branch, data) {
|
|
|
270
622
|
* rule the projection uses to expose that branch, so the two cannot disagree
|
|
271
623
|
* about which branch it is.
|
|
272
624
|
*/
|
|
273
|
-
function eachApplicable(roots, data, visitor) {
|
|
625
|
+
function eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope = [], dynamicReferenceProjection = false) {
|
|
274
626
|
const visited = new Set();
|
|
275
|
-
const visit = (current) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
627
|
+
const visit = (current, parentScope) => {
|
|
628
|
+
if (referenceVisitor && cache)
|
|
629
|
+
visitReferenceDefaultSites(current, cache, referenceVisitor, dynamicReferenceProjection);
|
|
630
|
+
const entered = dynamicReferenceProjection
|
|
631
|
+
? enterDynamicScope(current, parentScope)
|
|
632
|
+
: { node: dereference(current), path: parentScope, dynamicReference: false };
|
|
633
|
+
const resolved = entered.node;
|
|
634
|
+
const currentScope = entered.path;
|
|
635
|
+
const key = traversalKey(resolved);
|
|
279
636
|
if (visited.has(key))
|
|
280
637
|
return;
|
|
281
638
|
visited.add(key);
|
|
282
639
|
visitor(resolved);
|
|
283
640
|
for (const branch of resolved.allOf ?? [])
|
|
284
|
-
visit(branch);
|
|
641
|
+
visit(branch, currentScope);
|
|
285
642
|
// `oneOf` contributes the one branch it selects, which is the evaluator's
|
|
286
643
|
// rule and not "every branch the data matches": an empty object matches
|
|
287
644
|
// every permissive branch, and treating all of them as applicable would
|
|
@@ -290,82 +647,123 @@ function eachApplicable(roots, data, visitor) {
|
|
|
290
647
|
// branch stands in, which is what the projection exposes.
|
|
291
648
|
const oneOf = resolved.oneOf ?? [];
|
|
292
649
|
if (oneOf.length > 0) {
|
|
293
|
-
const matching = oneOf.filter((branch) => branchApplies(branch, data));
|
|
650
|
+
const matching = oneOf.filter((branch) => branchApplies(branch, data, currentScope, branchApplicability));
|
|
294
651
|
const selected = matching.length === 1
|
|
295
652
|
? matching[0]
|
|
296
653
|
: selectProvisionalBranch(resolved, typeof data === 'object' && data !== null
|
|
297
654
|
? data
|
|
298
655
|
: undefined);
|
|
299
656
|
if (selected)
|
|
300
|
-
visit(selected);
|
|
657
|
+
visit(selected, currentScope);
|
|
301
658
|
}
|
|
302
659
|
// `anyOf` is the opposite case and needs no such rule: several of its
|
|
303
660
|
// branches applying at once is ordinary, and each that validates
|
|
304
661
|
// contributes its annotations, so each competes.
|
|
305
662
|
for (const branch of resolved.anyOf ?? []) {
|
|
306
|
-
if (branchApplies(branch, data))
|
|
307
|
-
visit(branch);
|
|
663
|
+
if (branchApplies(branch, data, currentScope, branchApplicability))
|
|
664
|
+
visit(branch, currentScope);
|
|
308
665
|
}
|
|
309
666
|
if (resolved.if) {
|
|
310
|
-
const taken = branchApplies(resolved.if, data) ? resolved.then : resolved.else;
|
|
667
|
+
const taken = branchApplies(resolved.if, data, currentScope, branchApplicability) ? resolved.then : resolved.else;
|
|
311
668
|
if (taken)
|
|
312
|
-
visit(taken);
|
|
669
|
+
visit(taken, currentScope);
|
|
313
670
|
}
|
|
314
671
|
const dependent = resolved.dependentSchemas;
|
|
315
672
|
if (dependent && typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
316
673
|
for (const [dependency, subschema] of Object.entries(dependent)) {
|
|
317
674
|
if (Object.prototype.hasOwnProperty.call(data, dependency))
|
|
318
|
-
visit(subschema);
|
|
675
|
+
visit(subschema, currentScope);
|
|
319
676
|
}
|
|
320
677
|
}
|
|
321
678
|
};
|
|
322
679
|
for (const root of roots)
|
|
323
|
-
visit(root);
|
|
680
|
+
visit(root, scope);
|
|
324
681
|
}
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
682
|
+
function enterDynamicScope(node, scope) {
|
|
683
|
+
const path = [...scope];
|
|
684
|
+
let current = node;
|
|
685
|
+
let dynamicReference = false;
|
|
686
|
+
const seen = new Set();
|
|
687
|
+
while (typeof current.$ref === 'string' ||
|
|
688
|
+
typeof current.schema.$dynamicRef === 'string' ||
|
|
689
|
+
typeof current.schema.$recursiveRef === 'string') {
|
|
690
|
+
const position = positionOf(current);
|
|
691
|
+
if (seen.has(position))
|
|
692
|
+
break;
|
|
693
|
+
seen.add(position);
|
|
694
|
+
const resolved = current.resolveRef({ pointer: '#', path });
|
|
695
|
+
if (!isSchemaNode(resolved))
|
|
696
|
+
break;
|
|
697
|
+
const schema = current.schema;
|
|
698
|
+
if (typeof schema.$dynamicRef === 'string' || typeof schema.$recursiveRef === 'string') {
|
|
699
|
+
dynamicReference = true;
|
|
331
700
|
}
|
|
332
|
-
|
|
701
|
+
if (resolved === current)
|
|
702
|
+
break;
|
|
703
|
+
if (typeof current.$ref === 'string' &&
|
|
704
|
+
current.getDraftVersion() !== 'draft-07' &&
|
|
705
|
+
!onlyPoints(current)) {
|
|
706
|
+
return { node: dereference(current), path, dynamicReference };
|
|
707
|
+
}
|
|
708
|
+
current = resolved;
|
|
709
|
+
}
|
|
710
|
+
if (path[path.length - 1]?.node !== current)
|
|
711
|
+
path.push({ pointer: '#', node: current });
|
|
712
|
+
return { node: current, path, dynamicReference };
|
|
713
|
+
}
|
|
714
|
+
function declarationsFrom(walkRoots, cache) {
|
|
715
|
+
const declarations = [];
|
|
716
|
+
const seen = new Set();
|
|
717
|
+
const add = (node) => {
|
|
718
|
+
const schema = authoredSchema(node, cache);
|
|
719
|
+
if (schema === null || typeof schema !== 'object' || !('default' in schema))
|
|
720
|
+
return;
|
|
721
|
+
const record = schema;
|
|
722
|
+
if (typeof record.$ref === 'string' && cache.dialect === 'draft-07')
|
|
723
|
+
return;
|
|
724
|
+
const source = documentPointer(node);
|
|
725
|
+
if (seen.has(source))
|
|
726
|
+
return;
|
|
727
|
+
seen.add(source);
|
|
728
|
+
declarations.push({ value: record.default, source });
|
|
729
|
+
};
|
|
730
|
+
walkRoots(add, add);
|
|
333
731
|
return declarations;
|
|
334
732
|
}
|
|
335
|
-
function collectUnconditionalDefaults(roots) {
|
|
336
|
-
return declarationsFrom((visitor) => eachUnconditional(roots, visitor));
|
|
733
|
+
function collectUnconditionalDefaults(roots, cache) {
|
|
734
|
+
return declarationsFrom((visitor, referenceVisitor) => eachUnconditional(roots, visitor, referenceVisitor, cache), cache);
|
|
337
735
|
}
|
|
338
|
-
function collectApplicableDefaults(roots, data) {
|
|
339
|
-
return declarationsFrom((visitor) => eachApplicable(roots, data, visitor));
|
|
736
|
+
function collectApplicableDefaults(roots, data, cache, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
737
|
+
return declarationsFrom((visitor, referenceVisitor) => eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope, dynamicReferenceProjection), cache);
|
|
340
738
|
}
|
|
341
739
|
/** The positions that declare one property of `roots`, by both reachability rules. */
|
|
342
|
-
function childPositions(roots, key, data, declaring) {
|
|
740
|
+
function childPositions(roots, key, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
343
741
|
const unconditional = [];
|
|
344
742
|
eachUnconditional(roots.unconditional, (node) => {
|
|
345
|
-
const child = node
|
|
743
|
+
const child = memberSchema(node, key);
|
|
346
744
|
if (child)
|
|
347
745
|
unconditional.push(child);
|
|
348
746
|
});
|
|
349
747
|
const applicable = [];
|
|
350
|
-
eachApplicable(roots.applicable, data, (node) => {
|
|
351
|
-
const child = node
|
|
748
|
+
eachApplicable(roots.applicable, data, branchApplicability, (node) => {
|
|
749
|
+
const child = memberSchema(node, key);
|
|
352
750
|
if (child)
|
|
353
751
|
applicable.push(child);
|
|
354
|
-
});
|
|
752
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
355
753
|
return { unconditional, applicable, declaring };
|
|
356
754
|
}
|
|
357
755
|
/** The positions that declare the elements of `roots`, by both reachability rules. */
|
|
358
|
-
function itemPositions(roots, data, declaring) {
|
|
756
|
+
function itemPositions(roots, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
359
757
|
const unconditional = [];
|
|
360
758
|
eachUnconditional(roots.unconditional, (node) => {
|
|
361
759
|
if (node.items)
|
|
362
760
|
unconditional.push(node.items);
|
|
363
761
|
});
|
|
364
762
|
const applicable = [];
|
|
365
|
-
eachApplicable(roots.applicable, data, (node) => {
|
|
763
|
+
eachApplicable(roots.applicable, data, branchApplicability, (node) => {
|
|
366
764
|
if (node.items)
|
|
367
765
|
applicable.push(node.items);
|
|
368
|
-
});
|
|
766
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
369
767
|
return { unconditional, applicable, declaring };
|
|
370
768
|
}
|
|
371
769
|
/**
|
|
@@ -380,16 +778,75 @@ function disagreeingDefaults(declarations) {
|
|
|
380
778
|
const [first, ...rest] = declarations;
|
|
381
779
|
return rest.every((other) => deepEqual(other.value, first.value)) ? undefined : declarations;
|
|
382
780
|
}
|
|
781
|
+
function additionalPropertyKeys(node, data) {
|
|
782
|
+
if (!node?.additionalProperties)
|
|
783
|
+
return [];
|
|
784
|
+
const keys = new Set([...Object.keys(data ?? {}), ...(node.required ?? [])]);
|
|
785
|
+
return [...keys].filter((key) => !Object.hasOwn(node.properties ?? {}, key) &&
|
|
786
|
+
!node.patternProperties?.some(({ pattern }) => pattern.test(key)));
|
|
787
|
+
}
|
|
788
|
+
function additionalPropertySchema(node, key) {
|
|
789
|
+
if (!node || Object.hasOwn(node.properties ?? {}, key))
|
|
790
|
+
return undefined;
|
|
791
|
+
if (node.patternProperties?.some(({ pattern }) => pattern.test(key)))
|
|
792
|
+
return undefined;
|
|
793
|
+
return node.additionalProperties;
|
|
794
|
+
}
|
|
795
|
+
function memberSchema(node, key) {
|
|
796
|
+
if (!node)
|
|
797
|
+
return undefined;
|
|
798
|
+
if (Object.hasOwn(node.properties ?? {}, key))
|
|
799
|
+
return node.properties?.[key];
|
|
800
|
+
return additionalPropertySchema(node, key);
|
|
801
|
+
}
|
|
802
|
+
function applicableAdditionalPropertySchemas(roots, key, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
803
|
+
const schemas = [];
|
|
804
|
+
const seen = new Set();
|
|
805
|
+
eachApplicable(roots, data, branchApplicability, (node) => {
|
|
806
|
+
const schema = additionalPropertySchema(node, key);
|
|
807
|
+
if (!schema)
|
|
808
|
+
return;
|
|
809
|
+
const position = positionOf(schema);
|
|
810
|
+
if (seen.has(position))
|
|
811
|
+
return;
|
|
812
|
+
seen.add(position);
|
|
813
|
+
schemas.push(schema);
|
|
814
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
815
|
+
return schemas;
|
|
816
|
+
}
|
|
817
|
+
function applicableRequiredKeys(roots, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
818
|
+
const keys = new Set();
|
|
819
|
+
eachApplicable(roots, data, branchApplicability, (node) => {
|
|
820
|
+
for (const key of node.required ?? [])
|
|
821
|
+
keys.add(key);
|
|
822
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
823
|
+
return keys;
|
|
824
|
+
}
|
|
825
|
+
function unconditionalRequiredKeys(roots) {
|
|
826
|
+
const keys = new Set();
|
|
827
|
+
eachUnconditional(roots, (node) => {
|
|
828
|
+
for (const key of node.required ?? [])
|
|
829
|
+
keys.add(key);
|
|
830
|
+
});
|
|
831
|
+
return keys;
|
|
832
|
+
}
|
|
833
|
+
function reduceAdditionalPropertySchema(source, data, scope = []) {
|
|
834
|
+
if (source.getDraftVersion() !== 'draft-07')
|
|
835
|
+
return source.reduceNode(data, { path: [...scope] }).node ?? source;
|
|
836
|
+
const { node, unresolved } = dereferenceChecked(source);
|
|
837
|
+
if (unresolved || !isSchemaNode(node))
|
|
838
|
+
return source;
|
|
839
|
+
return reduceAllOfForProjection(node, data) ?? node;
|
|
840
|
+
}
|
|
841
|
+
const candidatePropertyCache = new WeakMap();
|
|
383
842
|
/**
|
|
384
|
-
*
|
|
385
|
-
* across every branch that might apply to it.
|
|
843
|
+
* Property keys that might need to appear at this instance location.
|
|
386
844
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* which
|
|
391
|
-
*
|
|
392
|
-
* matching, which is exactly the flicker the contract exists to prevent.
|
|
845
|
+
* Declared keys remain static candidates across all branches. Keys governed
|
|
846
|
+
* only by `additionalProperties` come from the current data, since the schema
|
|
847
|
+
* has no finite set of names to enumerate; required names are candidates too.
|
|
848
|
+
* `reduceNode` decides which candidates apply now, so an inactive branch keeps
|
|
849
|
+
* its declared pointers with `active: false`.
|
|
393
850
|
*
|
|
394
851
|
* **Recursion is through applicators only, and only those acting on this same
|
|
395
852
|
* instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
|
|
@@ -409,20 +866,20 @@ function disagreeingDefaults(declarations) {
|
|
|
409
866
|
* - **`items` and `prefixItems`**, for the same reason: an array's items are
|
|
410
867
|
* their own locations.
|
|
411
868
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* and a recursive `$ref` would not terminate. `schemaLocation` is stable, is
|
|
417
|
-
* distinct for each inline branch position, and repeats when a reference closes
|
|
418
|
-
* a cycle, which is precisely the three properties needed. The set is per call,
|
|
419
|
-
* because deduplicating a schema location is only sound while collecting names
|
|
420
|
-
* for one instance location; the same schema legitimately recurs at a deeper
|
|
421
|
-
* pointer, and `walk` visits it again there.
|
|
869
|
+
* Inline conditional branches use `positionOf` because the library can assign
|
|
870
|
+
* `if`, `then` and `else` the same `schemaLocation` after resolving a
|
|
871
|
+
* reference. Other nodes keep their location key, so references still close
|
|
872
|
+
* at the same point in the traversal as before.
|
|
422
873
|
*/
|
|
423
|
-
function collectCandidateProperties(node) {
|
|
874
|
+
function collectCandidateProperties(node, data, declarations = []) {
|
|
875
|
+
if (declarations.length === 0 && candidatePropertyCache.has(node)) {
|
|
876
|
+
const cached = candidatePropertyCache.get(node);
|
|
877
|
+
if (cached)
|
|
878
|
+
return cached;
|
|
879
|
+
}
|
|
424
880
|
const candidates = new Map();
|
|
425
881
|
const visited = new Set();
|
|
882
|
+
const scopes = [];
|
|
426
883
|
const record = (key, propNode, direct) => {
|
|
427
884
|
const entry = candidates.get(key) ?? { alternatives: [] };
|
|
428
885
|
if (direct)
|
|
@@ -433,26 +890,11 @@ function collectCandidateProperties(node) {
|
|
|
433
890
|
};
|
|
434
891
|
const visit = (current, own) => {
|
|
435
892
|
const resolved = dereference(current);
|
|
436
|
-
|
|
437
|
-
// where there is not, which together cover every way the traversal can
|
|
438
|
-
// come back to where it started.
|
|
439
|
-
//
|
|
440
|
-
// Neither alone is sufficient and each covers what the other cannot. A
|
|
441
|
-
// cycle requires a `$ref`, because an inline schema cannot nest into
|
|
442
|
-
// itself, and `resolveRef()` returns a fresh `SchemaNode` on every call
|
|
443
|
-
// whose raw `schema` object is fresh too, so identity never matches across
|
|
444
|
-
// one; those nodes do carry a location. An inline branch is never resolved
|
|
445
|
-
// through a ref, so its identity is stable within one traversal.
|
|
446
|
-
//
|
|
447
|
-
// This deliberately replaced a depth cap. A cap terminates, but it does so
|
|
448
|
-
// by dropping candidates a valid schema declared, which is the silent
|
|
449
|
-
// disappearance the whole projection-diagnostic design exists to prevent:
|
|
450
|
-
// a field nested under 70 applicators is still a field.
|
|
451
|
-
const location = resolved.schemaLocation;
|
|
452
|
-
const key = typeof location === 'string' ? location : resolved;
|
|
893
|
+
const key = traversalKey(resolved);
|
|
453
894
|
if (visited.has(key))
|
|
454
895
|
return;
|
|
455
896
|
visited.add(key);
|
|
897
|
+
scopes.push({ node: resolved, own });
|
|
456
898
|
for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
|
|
457
899
|
record(key, propNode, own);
|
|
458
900
|
}
|
|
@@ -460,7 +902,9 @@ function collectCandidateProperties(node) {
|
|
|
460
902
|
if (branch)
|
|
461
903
|
visit(branch, false);
|
|
462
904
|
}
|
|
463
|
-
for (const
|
|
905
|
+
for (const branch of resolved.allOf ?? [])
|
|
906
|
+
visit(branch, own);
|
|
907
|
+
for (const branches of [resolved.anyOf, resolved.oneOf]) {
|
|
464
908
|
for (const branch of branches ?? [])
|
|
465
909
|
visit(branch, false);
|
|
466
910
|
}
|
|
@@ -471,8 +915,32 @@ function collectCandidateProperties(node) {
|
|
|
471
915
|
}
|
|
472
916
|
};
|
|
473
917
|
visit(node, true);
|
|
918
|
+
for (const declaration of declarations)
|
|
919
|
+
visit(declaration, false);
|
|
920
|
+
const memberKeys = new Set(Object.keys(data ?? {}));
|
|
921
|
+
for (const scope of scopes)
|
|
922
|
+
for (const key of scope.node.required ?? [])
|
|
923
|
+
memberKeys.add(key);
|
|
924
|
+
for (const { node: scope, own } of scopes) {
|
|
925
|
+
for (const key of memberKeys) {
|
|
926
|
+
const propNode = additionalPropertySchema(scope, key);
|
|
927
|
+
if (propNode)
|
|
928
|
+
record(key, propNode, own);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const hasDataDependentAdditionalProperties = scopes.some(({ node: scope }) => Boolean(scope.additionalProperties));
|
|
932
|
+
if (declarations.length === 0) {
|
|
933
|
+
candidatePropertyCache.set(node, hasDataDependentAdditionalProperties ? null : candidates);
|
|
934
|
+
}
|
|
474
935
|
return candidates;
|
|
475
936
|
}
|
|
937
|
+
function hasRetainedDescendant(position, cache) {
|
|
938
|
+
for (const retained of cache.retainedPositions) {
|
|
939
|
+
if (retained === position || retained.startsWith(`${position}/`))
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
476
944
|
/**
|
|
477
945
|
* The schema to project for a candidate when no branch is active.
|
|
478
946
|
*
|
|
@@ -646,6 +1114,96 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
|
|
|
646
1114
|
}
|
|
647
1115
|
return required;
|
|
648
1116
|
}
|
|
1117
|
+
// reduceNode recompiles a subtree when any descendant has reduction work.
|
|
1118
|
+
const REDUCE_NODE_SCHEMA_FIELDS = [
|
|
1119
|
+
'$ref',
|
|
1120
|
+
'$dynamicRef',
|
|
1121
|
+
'$recursiveRef',
|
|
1122
|
+
'$defs',
|
|
1123
|
+
'if',
|
|
1124
|
+
'then',
|
|
1125
|
+
'else',
|
|
1126
|
+
'allOf',
|
|
1127
|
+
'anyOf',
|
|
1128
|
+
'oneOf',
|
|
1129
|
+
'contains',
|
|
1130
|
+
'dependentSchemas',
|
|
1131
|
+
'dependentRequired',
|
|
1132
|
+
'definitions',
|
|
1133
|
+
'dependencies',
|
|
1134
|
+
'patternProperties',
|
|
1135
|
+
'propertyDependencies',
|
|
1136
|
+
];
|
|
1137
|
+
const SCHEMA_MAP_KEYWORDS = [
|
|
1138
|
+
'$defs',
|
|
1139
|
+
'definitions',
|
|
1140
|
+
'dependentSchemas',
|
|
1141
|
+
'patternProperties',
|
|
1142
|
+
'properties',
|
|
1143
|
+
];
|
|
1144
|
+
const SCHEMA_SINGLE_KEYWORDS = [
|
|
1145
|
+
'additionalItems',
|
|
1146
|
+
'additionalProperties',
|
|
1147
|
+
'contains',
|
|
1148
|
+
'if',
|
|
1149
|
+
'items',
|
|
1150
|
+
'not',
|
|
1151
|
+
'propertyNames',
|
|
1152
|
+
'then',
|
|
1153
|
+
'else',
|
|
1154
|
+
'unevaluatedItems',
|
|
1155
|
+
'unevaluatedProperties',
|
|
1156
|
+
];
|
|
1157
|
+
const SCHEMA_ARRAY_KEYWORDS = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
|
|
1158
|
+
const reductionRequirementCache = new WeakMap();
|
|
1159
|
+
function schemaRequiresReduction(schema, visiting = new WeakSet()) {
|
|
1160
|
+
if (schema === true)
|
|
1161
|
+
return true;
|
|
1162
|
+
if (typeof schema !== 'object' || schema === null)
|
|
1163
|
+
return false;
|
|
1164
|
+
const cached = reductionRequirementCache.get(schema);
|
|
1165
|
+
if (cached !== undefined)
|
|
1166
|
+
return cached;
|
|
1167
|
+
if (visiting.has(schema))
|
|
1168
|
+
return false;
|
|
1169
|
+
visiting.add(schema);
|
|
1170
|
+
const record = schema;
|
|
1171
|
+
const direct = REDUCE_NODE_SCHEMA_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(record, field)) ||
|
|
1172
|
+
Object.prototype.hasOwnProperty.call(record, 'oneOfProperty') ||
|
|
1173
|
+
Array.isArray(record.type);
|
|
1174
|
+
const children = [];
|
|
1175
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
1176
|
+
const value = record[keyword];
|
|
1177
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1178
|
+
children.push(...Object.values(value));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
|
|
1182
|
+
const value = record[keyword];
|
|
1183
|
+
if (keyword === 'items' && Array.isArray(value))
|
|
1184
|
+
children.push(...value);
|
|
1185
|
+
else if (value !== undefined)
|
|
1186
|
+
children.push(value);
|
|
1187
|
+
}
|
|
1188
|
+
for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
|
|
1189
|
+
const value = record[keyword];
|
|
1190
|
+
if (Array.isArray(value))
|
|
1191
|
+
children.push(...value);
|
|
1192
|
+
}
|
|
1193
|
+
for (const value of Object.values(record.dependencies ?? {}))
|
|
1194
|
+
children.push(value);
|
|
1195
|
+
for (const values of Object.values(record.propertyDependencies ?? {})) {
|
|
1196
|
+
if (typeof values === 'object' && values !== null)
|
|
1197
|
+
children.push(...Object.values(values));
|
|
1198
|
+
}
|
|
1199
|
+
const required = direct || children.some((child) => schemaRequiresReduction(child, visiting));
|
|
1200
|
+
visiting.delete(schema);
|
|
1201
|
+
reductionRequirementCache.set(schema, required);
|
|
1202
|
+
return required;
|
|
1203
|
+
}
|
|
1204
|
+
function requiresReduction(node) {
|
|
1205
|
+
return node.reducers.length > 0 || schemaRequiresReduction(node.schema);
|
|
1206
|
+
}
|
|
649
1207
|
// An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
|
|
650
1208
|
const budgeted = (info) => info.cyclic || info.key === '';
|
|
651
1209
|
/**
|
|
@@ -675,9 +1233,15 @@ declaredAt, ctx,
|
|
|
675
1233
|
/** Entered as an object member, which is the only way to be past the data. */
|
|
676
1234
|
member,
|
|
677
1235
|
/** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
|
|
678
|
-
lineage, path) {
|
|
1236
|
+
lineage, path, scope, dynamicReferenceAtLocation = false) {
|
|
1237
|
+
if (ctx.cache.dynamicReferenceProjection) {
|
|
1238
|
+
const entered = enterDynamicScope(node, scope);
|
|
1239
|
+
node = entered.node;
|
|
1240
|
+
scope = entered.path;
|
|
1241
|
+
dynamicReferenceAtLocation ||= entered.dynamicReference;
|
|
1242
|
+
}
|
|
679
1243
|
const info = locationInfo(declaredAt.declaring, ctx.cache);
|
|
680
|
-
const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
|
|
1244
|
+
const { node: original, cycle: referenceCycle, unresolved: unresolvedReference } = dereferenceChecked(node);
|
|
681
1245
|
if (info.cycle || referenceCycle) {
|
|
682
1246
|
diagnostics.push({
|
|
683
1247
|
pointer: toPointer(pointer),
|
|
@@ -688,10 +1252,13 @@ lineage, path) {
|
|
|
688
1252
|
return;
|
|
689
1253
|
}
|
|
690
1254
|
const pastData = member && (data === undefined || data === null);
|
|
691
|
-
const
|
|
1255
|
+
const token = boundaryToken(pointer, info.key, ctx.boundaryGeneration);
|
|
1256
|
+
const recursive = pastData && !unresolvedReference && ((lineage?.recursive ?? false) || budgeted(info));
|
|
692
1257
|
for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
|
|
693
|
-
if (info.key !== '' &&
|
|
694
|
-
|
|
1258
|
+
if (info.key !== '' &&
|
|
1259
|
+
ancestor.key === info.key &&
|
|
1260
|
+
!ctx.expandedBoundaryTokens.has(token)) {
|
|
1261
|
+
addBoundary(ctx, ancestor.pointer, 'recursion', pointer, info.key);
|
|
695
1262
|
ctx.removed.add(pointer);
|
|
696
1263
|
return;
|
|
697
1264
|
}
|
|
@@ -702,21 +1269,29 @@ lineage, path) {
|
|
|
702
1269
|
let admit;
|
|
703
1270
|
if (isLeafSchema(originalSchema)) {
|
|
704
1271
|
const covered = ctx.reserved.has(parent);
|
|
705
|
-
admit = covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
1272
|
+
admit = ctx.expandedBoundaryTokens.has(token) || covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
706
1273
|
if (admit && !covered)
|
|
707
1274
|
ctx.nodesUsed += 1;
|
|
708
1275
|
}
|
|
709
1276
|
else {
|
|
710
|
-
const
|
|
711
|
-
|
|
1277
|
+
const explicitlyExpanded = ctx.expandedBoundaryTokens.has(token);
|
|
1278
|
+
const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
|
|
1279
|
+
const candidateDeclarations = dynamicReferenceAtLocation
|
|
1280
|
+
? []
|
|
1281
|
+
: [...declaredAt.applicable, ...retainedDeclarations];
|
|
1282
|
+
const leaves = [...collectCandidateProperties(original, undefined, candidateDeclarations).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
|
|
1283
|
+
admit =
|
|
1284
|
+
explicitlyExpanded ||
|
|
1285
|
+
(ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes);
|
|
712
1286
|
if (admit) {
|
|
713
1287
|
ctx.objectsUsed += 1;
|
|
714
|
-
ctx.nodesUsed += 1 + leaves;
|
|
715
|
-
|
|
1288
|
+
ctx.nodesUsed += explicitlyExpanded ? 1 : 1 + leaves;
|
|
1289
|
+
if (!explicitlyExpanded)
|
|
1290
|
+
ctx.reserved.add(pointer);
|
|
716
1291
|
}
|
|
717
1292
|
}
|
|
718
1293
|
if (!admit) {
|
|
719
|
-
addBoundary(ctx, parent, 'budget');
|
|
1294
|
+
addBoundary(ctx, parent, 'budget', pointer, info.key);
|
|
720
1295
|
ctx.removed.add(pointer);
|
|
721
1296
|
return;
|
|
722
1297
|
}
|
|
@@ -727,6 +1302,36 @@ lineage, path) {
|
|
|
727
1302
|
let resolved = original;
|
|
728
1303
|
let schema = originalSchema;
|
|
729
1304
|
let type = resolveExplicitType(schema);
|
|
1305
|
+
const localShape = inferProjectionShape(schema);
|
|
1306
|
+
const allOfType = original.allOf ? allOfExplicitType(original) : undefined;
|
|
1307
|
+
const hasReferencedAllOfBranch = original.allOf ? hasReferencedAllOf(original) : false;
|
|
1308
|
+
if (original.allOf &&
|
|
1309
|
+
!original.oneOf &&
|
|
1310
|
+
!original.anyOf &&
|
|
1311
|
+
!type &&
|
|
1312
|
+
(localShape.kind !== 'resolved' || allOfType !== undefined) &&
|
|
1313
|
+
(hasReferencedAllOfBranch || allOfType !== undefined)) {
|
|
1314
|
+
const reductionData = (allOfType === 'object' || declaresObject(original)) &&
|
|
1315
|
+
(data === undefined || data === null)
|
|
1316
|
+
? {}
|
|
1317
|
+
: data;
|
|
1318
|
+
const reduced = reduceAllOfForProjection(original, reductionData, scope);
|
|
1319
|
+
const reducedSchema = reduced?.schema;
|
|
1320
|
+
if (reduced && reducedSchema && typeof reducedSchema === 'object') {
|
|
1321
|
+
resolved = reduced;
|
|
1322
|
+
schema = reducedSchema;
|
|
1323
|
+
if (!type)
|
|
1324
|
+
type = resolveExplicitType(schema);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (!type && ctx.cache.dynamicReferenceProjection && original.allOf?.length) {
|
|
1328
|
+
const reduced = original.reduceNode(data, { path: [...scope] }).node;
|
|
1329
|
+
if (reduced) {
|
|
1330
|
+
resolved = reduced;
|
|
1331
|
+
schema = reduced.schema;
|
|
1332
|
+
type = resolveExplicitType(schema);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
730
1335
|
// Whether this node's own active branch could be determined. Stays true for every
|
|
731
1336
|
// node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
|
|
732
1337
|
// (see below), where the node's own presence in the projection is the thing in
|
|
@@ -750,9 +1355,9 @@ lineage, path) {
|
|
|
750
1355
|
let wrapperIdentified = false;
|
|
751
1356
|
if (!type && (original.oneOf || original.anyOf)) {
|
|
752
1357
|
composedAgainstData = true;
|
|
753
|
-
const { node: branchNode } = original.reduceNode(data);
|
|
1358
|
+
const { node: branchNode } = original.reduceNode(data, { path: [...scope] });
|
|
754
1359
|
if (branchNode) {
|
|
755
|
-
resolved = resolveSelectedBranch(original, branchNode, data);
|
|
1360
|
+
resolved = resolveSelectedBranch(original, branchNode, data, scope, ctx.branchApplicability);
|
|
756
1361
|
schema = resolved.schema;
|
|
757
1362
|
type = resolveExplicitType(schema);
|
|
758
1363
|
}
|
|
@@ -834,6 +1439,15 @@ lineage, path) {
|
|
|
834
1439
|
diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
|
|
835
1440
|
}
|
|
836
1441
|
}
|
|
1442
|
+
// Arrays do not pass through the object path that reduces the active schema.
|
|
1443
|
+
if (type === 'array' && resolved === original && original.allOf) {
|
|
1444
|
+
const reduced = reduceAllOfForProjection(original, data, scope);
|
|
1445
|
+
const reducedSchema = reduced?.schema;
|
|
1446
|
+
if (reduced && reducedSchema && typeof reducedSchema === 'object') {
|
|
1447
|
+
resolved = reduced;
|
|
1448
|
+
schema = reducedSchema;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
837
1451
|
if (!type)
|
|
838
1452
|
return;
|
|
839
1453
|
const selfLineage = pastData
|
|
@@ -843,7 +1457,7 @@ lineage, path) {
|
|
|
843
1457
|
// pointer with no node has nothing to omit an annotation from, and saying
|
|
844
1458
|
// that its `default` is undecidable on top of saying it cannot be drawn at
|
|
845
1459
|
// all would be the same schema reported twice.
|
|
846
|
-
const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional));
|
|
1460
|
+
const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional, ctx.cache));
|
|
847
1461
|
if (ambiguousDefault) {
|
|
848
1462
|
diagnostics.push({
|
|
849
1463
|
pointer: toPointer(pointer),
|
|
@@ -858,13 +1472,18 @@ lineage, path) {
|
|
|
858
1472
|
// selects. A superset of the diagnostic's: every unconditional disagreement
|
|
859
1473
|
// is also an applicable one, so the node carries both kinds and a consumer
|
|
860
1474
|
// reads one place.
|
|
861
|
-
const
|
|
1475
|
+
const applicableData = type === 'object' && !(typeof data === 'object' && data !== null) ? {} : data;
|
|
1476
|
+
const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, applicableData, ctx.cache, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
862
1477
|
const applicableConflict = disagreeingDefaults(applicableDeclarations);
|
|
863
1478
|
const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
|
|
864
1479
|
const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
|
|
865
|
-
const defaultSources = !conflictedDefault &&
|
|
1480
|
+
const defaultSources = !conflictedDefault && applicableDeclarations.length > 0 && info.cyclic
|
|
866
1481
|
? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
|
|
867
1482
|
: undefined;
|
|
1483
|
+
const defaultOverride = {
|
|
1484
|
+
present: applicableDeclarations.length > 0,
|
|
1485
|
+
value: applicableDeclarations[0]?.value,
|
|
1486
|
+
};
|
|
868
1487
|
const nodeActive = active && branchResolved;
|
|
869
1488
|
// A node the schema applies is never also provisional; the two report
|
|
870
1489
|
// different facts and only the second is a choice this projection made. The
|
|
@@ -889,19 +1508,73 @@ lineage, path) {
|
|
|
889
1508
|
// wrapper case), it has already been reduced against the real `data` at this
|
|
890
1509
|
// pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
|
|
891
1510
|
// for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
|
|
892
|
-
let reducedNode = resolved === original
|
|
1511
|
+
let reducedNode = resolved === original
|
|
1512
|
+
? requiresReduction(resolved)
|
|
1513
|
+
? original.allOf && hasReferencedAllOf(original)
|
|
1514
|
+
? reduceAllOfForProjection(resolved, dataRecord ?? {}, scope)
|
|
1515
|
+
: resolved.reduceNode(dataRecord ?? {}, { path: [...scope] }).node
|
|
1516
|
+
: resolved
|
|
1517
|
+
: resolved;
|
|
1518
|
+
if (reducedNode && resolved === original && (!member || data !== undefined)) {
|
|
1519
|
+
// Missing child data has no evaluated conditional branch to recover.
|
|
1520
|
+
reducedNode = resolveSelectedConditionalReference(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
|
|
1521
|
+
}
|
|
893
1522
|
if (reducedNode &&
|
|
894
1523
|
resolved === original &&
|
|
895
1524
|
!resolveExplicitType(originalSchema) &&
|
|
896
1525
|
(original.oneOf || original.anyOf)) {
|
|
897
|
-
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
|
|
1526
|
+
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
|
|
898
1527
|
}
|
|
899
1528
|
const reducedSchema = reducedNode?.schema;
|
|
900
1529
|
const reducedProperties = reducedSchema?.properties ??
|
|
901
1530
|
schema.properties ??
|
|
902
1531
|
{};
|
|
903
|
-
|
|
1532
|
+
// Dynamic applicability is rebuilt below; only retained declarations belong beside it here.
|
|
1533
|
+
const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
|
|
1534
|
+
const candidateDeclarations = dynamicReferenceAtLocation
|
|
1535
|
+
? []
|
|
1536
|
+
: [...declaredAt.applicable, ...retainedDeclarations];
|
|
1537
|
+
let candidateProps = collectCandidateProperties(original, dataRecord, candidateDeclarations);
|
|
1538
|
+
const activeKeys = new Set([
|
|
1539
|
+
...Object.keys(reducedProperties),
|
|
1540
|
+
...additionalPropertyKeys(reducedNode, dataRecord),
|
|
1541
|
+
]);
|
|
1542
|
+
if (!reducedNode) {
|
|
1543
|
+
for (const [key, candidate] of candidateProps) {
|
|
1544
|
+
if (candidate.direct)
|
|
1545
|
+
activeKeys.add(key);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
904
1548
|
const requiredSet = computeRequiredSet(resolved, reducedSchema, dataRecord);
|
|
1549
|
+
const dynamicApplicableNodes = [];
|
|
1550
|
+
if (ctx.cache.dynamicReferenceProjection) {
|
|
1551
|
+
eachApplicable([original], data, ctx.branchApplicability, (applicableNode) => {
|
|
1552
|
+
dynamicApplicableNodes.push(applicableNode);
|
|
1553
|
+
for (const key of Object.keys(applicableNode.properties ?? {}))
|
|
1554
|
+
activeKeys.add(key);
|
|
1555
|
+
for (const key of applicableNode.required ?? [])
|
|
1556
|
+
requiredSet.add(key);
|
|
1557
|
+
}, undefined, undefined, scope, true);
|
|
1558
|
+
const expandedCandidates = new Map(candidateProps);
|
|
1559
|
+
const activeCandidateNode = reducedNode ?? resolved;
|
|
1560
|
+
if (activeCandidateNode !== original) {
|
|
1561
|
+
for (const [key, candidate] of collectCandidateProperties(activeCandidateNode, dataRecord)) {
|
|
1562
|
+
if (!expandedCandidates.has(key))
|
|
1563
|
+
expandedCandidates.set(key, candidate);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
for (const applicableNode of dynamicApplicableNodes) {
|
|
1567
|
+
for (const [key, propertyNode] of Object.entries(applicableNode.properties ?? {})) {
|
|
1568
|
+
if (!expandedCandidates.has(key))
|
|
1569
|
+
expandedCandidates.set(key, { direct: propertyNode, alternatives: [] });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
candidateProps = expandedCandidates;
|
|
1573
|
+
}
|
|
1574
|
+
if (!reducedNode) {
|
|
1575
|
+
for (const key of unconditionalRequiredKeys(declaredAt.unconditional))
|
|
1576
|
+
requiredSet.add(key);
|
|
1577
|
+
}
|
|
905
1578
|
// Two conditions, and they are not the same kind of condition.
|
|
906
1579
|
//
|
|
907
1580
|
// Exposure is load-bearing. Selection is local, but whether this node is
|
|
@@ -921,11 +1594,32 @@ lineage, path) {
|
|
|
921
1594
|
: undefined;
|
|
922
1595
|
const provisionalSchema = provisionalBranch?.schema;
|
|
923
1596
|
const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
|
|
1597
|
+
for (const key of additionalPropertyKeys(provisionalBranch, dataRecord))
|
|
1598
|
+
provisionalKeys.add(key);
|
|
924
1599
|
const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
|
|
925
1600
|
// Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
|
|
926
1601
|
// branch's properties are represented (the matching branch alone, via `resolved`,
|
|
927
1602
|
// would only expose its own properties).
|
|
928
|
-
const
|
|
1603
|
+
const applicableRequired = applicableRequiredKeys(declaredAt.applicable, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1604
|
+
const provisionalRequiredKeys = provisionalBranch
|
|
1605
|
+
? applicableRequiredKeys([provisionalBranch], applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)
|
|
1606
|
+
: new Set();
|
|
1607
|
+
for (const key of candidateProps.keys()) {
|
|
1608
|
+
const present = dataRecord !== undefined && Object.hasOwn(dataRecord, key);
|
|
1609
|
+
if ((present || applicableRequired.has(key)) &&
|
|
1610
|
+
applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0) {
|
|
1611
|
+
activeKeys.add(key);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (provisionalBranch) {
|
|
1615
|
+
for (const key of candidateProps.keys()) {
|
|
1616
|
+
if (provisionalRequiredKeys.has(key) &&
|
|
1617
|
+
(applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0 ||
|
|
1618
|
+
applicableAdditionalPropertySchemas([provisionalBranch], key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0)) {
|
|
1619
|
+
provisionalKeys.add(key);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
929
1623
|
const propKeys = [...candidateProps.keys()];
|
|
930
1624
|
const children = propKeys.length > 0
|
|
931
1625
|
? propKeys.map((key) => {
|
|
@@ -951,7 +1645,7 @@ lineage, path) {
|
|
|
951
1645
|
active: nodeActive,
|
|
952
1646
|
provisional: nodeProvisional ? true : undefined,
|
|
953
1647
|
defaultConflict,
|
|
954
|
-
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1648
|
+
annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
|
|
955
1649
|
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
956
1650
|
});
|
|
957
1651
|
for (const [index, key] of propKeys.entries()) {
|
|
@@ -961,29 +1655,89 @@ lineage, path) {
|
|
|
961
1655
|
// selected ancestor its own locally applicable properties are
|
|
962
1656
|
// provisional too, while a locally inactive one stays inactive.
|
|
963
1657
|
const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
|
|
964
|
-
const reducedChildNode = reducedNode
|
|
1658
|
+
const reducedChildNode = memberSchema(reducedNode, key);
|
|
965
1659
|
// Precedence matters as much as the selection does. Marking the right
|
|
966
1660
|
// branch provisional while taking its shape from `candidatePrototype`,
|
|
967
1661
|
// which is first-wins across branches, would render another branch's
|
|
968
1662
|
// widget and annotations under the selected branch's name, and would hand
|
|
969
1663
|
// ADR-003's pass another branch's `default`.
|
|
970
|
-
const provisionalChildNode = provisionalBranch
|
|
1664
|
+
const provisionalChildNode = memberSchema(provisionalBranch, key);
|
|
971
1665
|
const candidate = candidateProps.get(key);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1666
|
+
const childData = dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined;
|
|
1667
|
+
const sourceChildNode = reducedChildNode ?? candidate.direct ?? candidatePrototype(candidate);
|
|
1668
|
+
const sourceSchema = sourceChildNode.schema;
|
|
1669
|
+
let scopedChildNode;
|
|
1670
|
+
let childScope = scope;
|
|
1671
|
+
if (ctx.cache.dynamicReferenceProjection && childActive) {
|
|
1672
|
+
const nextScope = [...scope];
|
|
1673
|
+
if (typeof sourceSchema.$dynamicAnchor === 'string' ||
|
|
1674
|
+
typeof sourceSchema.$id === 'string' ||
|
|
1675
|
+
sourceSchema.$recursiveAnchor === true) {
|
|
1676
|
+
nextScope.push({ pointer: `#${childPointer}`, node: sourceChildNode });
|
|
1677
|
+
}
|
|
1678
|
+
const result = resolved.getNodeChild(key, dataRecord ?? {}, {
|
|
1679
|
+
pointer: `#${childPointer}`,
|
|
1680
|
+
path: nextScope,
|
|
1681
|
+
});
|
|
1682
|
+
if (!result.error && isSchemaNode(result.node)) {
|
|
1683
|
+
scopedChildNode = result.node;
|
|
1684
|
+
if (nextScope[nextScope.length - 1]?.node !== scopedChildNode) {
|
|
1685
|
+
nextScope.push({ pointer: `#${childPointer}`, node: scopedChildNode });
|
|
1686
|
+
}
|
|
1687
|
+
childScope = nextScope;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
let applicableChildNode = reducedChildNode;
|
|
1691
|
+
let scopedApplicableChildNode = scopedChildNode;
|
|
1692
|
+
const hasScopedReference = typeof sourceSchema.$dynamicRef === 'string' || typeof sourceSchema.$recursiveRef === 'string';
|
|
1693
|
+
for (const source of applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)) {
|
|
1694
|
+
const alreadyApplicable = applicableChildNode !== undefined && positionOf(applicableChildNode) === positionOf(source);
|
|
1695
|
+
const alreadyScoped = scopedApplicableChildNode !== undefined && positionOf(scopedApplicableChildNode) === positionOf(source);
|
|
1696
|
+
if (alreadyApplicable && (!scopedApplicableChildNode || alreadyScoped))
|
|
1697
|
+
continue;
|
|
1698
|
+
const reduced = reduceAdditionalPropertySchema(source, childData, scope);
|
|
1699
|
+
if (!alreadyApplicable) {
|
|
1700
|
+
applicableChildNode =
|
|
1701
|
+
applicableChildNode === undefined
|
|
1702
|
+
? reduced
|
|
1703
|
+
: mergeNode(applicableChildNode, reduced) ?? applicableChildNode;
|
|
1704
|
+
}
|
|
1705
|
+
if (scopedApplicableChildNode && !alreadyScoped) {
|
|
1706
|
+
scopedApplicableChildNode = mergeNode(scopedApplicableChildNode, reduced) ?? scopedApplicableChildNode;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
// Applicable declarations retain their authored scope here. The parent
|
|
1710
|
+
// reducer merges `properties` across `allOf`, which can make an
|
|
1711
|
+
// `additionalProperties` schema in a separate branch appear inapplicable.
|
|
1712
|
+
// Failing that, use the reducer's child, then the selected branch, then a
|
|
1713
|
+
// stable placeholder from the candidate set.
|
|
1714
|
+
const childNode = (hasScopedReference ? scopedApplicableChildNode : undefined) ??
|
|
1715
|
+
applicableChildNode ??
|
|
978
1716
|
composeChild(candidate.direct, provisionalChildNode) ??
|
|
979
1717
|
candidatePrototype(candidate);
|
|
980
|
-
|
|
1718
|
+
let positions = childPositions(declaredAt, key,
|
|
981
1719
|
// This node's own data, not the child's: the branches being entered
|
|
982
1720
|
// are this location's, so they are decided by the instance here. The
|
|
983
1721
|
// child's own conditional edges are entered by the child's walk, with
|
|
984
|
-
// the child's data.
|
|
985
|
-
|
|
986
|
-
|
|
1722
|
+
// the child's data. `applicableData` is the reducer's object stand-in
|
|
1723
|
+
// when this location has no object value yet.
|
|
1724
|
+
applicableData, childDeclaring(info, key), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1725
|
+
const dynamicChildDeclarations = dynamicApplicableNodes.flatMap((applicableNode) => {
|
|
1726
|
+
const dynamicChild = applicableNode.properties?.[key];
|
|
1727
|
+
return dynamicChild ? [dynamicChild] : [];
|
|
1728
|
+
});
|
|
1729
|
+
positions = {
|
|
1730
|
+
...positions,
|
|
1731
|
+
declaring: extendDeclaring(positions.declaring, dynamicChildDeclarations),
|
|
1732
|
+
};
|
|
1733
|
+
if (scopedChildNode && hasScopedReference && !positions.declaring.includes(scopedChildNode)) {
|
|
1734
|
+
positions = {
|
|
1735
|
+
...positions,
|
|
1736
|
+
applicable: [...positions.applicable, scopedChildNode],
|
|
1737
|
+
declaring: [...positions.declaring, scopedChildNode],
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
const enter = () => walk(childNode, childPointer, childData, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index], childScope, hasScopedReference);
|
|
987
1741
|
// Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
|
|
988
1742
|
const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
|
|
989
1743
|
if (counted)
|
|
@@ -1003,7 +1757,7 @@ lineage, path) {
|
|
|
1003
1757
|
active: nodeActive,
|
|
1004
1758
|
provisional: nodeProvisional ? true : undefined,
|
|
1005
1759
|
defaultConflict,
|
|
1006
|
-
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1760
|
+
annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
|
|
1007
1761
|
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
1008
1762
|
// Deliberately not given the flag above. A conflict under `items` belongs
|
|
1009
1763
|
// to the element locations, and each of those reports its own; this field
|
|
@@ -1016,9 +1770,9 @@ lineage, path) {
|
|
|
1016
1770
|
: undefined,
|
|
1017
1771
|
});
|
|
1018
1772
|
if (type === 'array' && resolved.items && Array.isArray(data)) {
|
|
1019
|
-
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
|
|
1773
|
+
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1020
1774
|
data.forEach((item, index) => {
|
|
1021
|
-
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index]);
|
|
1775
|
+
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index], scope);
|
|
1022
1776
|
});
|
|
1023
1777
|
}
|
|
1024
1778
|
}
|
|
@@ -1032,13 +1786,17 @@ lineage, path) {
|
|
|
1032
1786
|
* contract), while array items are walked from `data` (an array's length is a data-time
|
|
1033
1787
|
* fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
|
|
1034
1788
|
*/
|
|
1035
|
-
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
1789
|
+
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS, options = {}) {
|
|
1036
1790
|
const nodes = new Map();
|
|
1037
1791
|
const diagnostics = [];
|
|
1038
1792
|
const ctx = {
|
|
1039
1793
|
cache,
|
|
1794
|
+
branchApplicability: cache.memoizeUndefinedBranchResults ? new WeakMap() : undefined,
|
|
1040
1795
|
limits,
|
|
1041
1796
|
boundaries: new Map(),
|
|
1797
|
+
boundaryTargets: new Map(),
|
|
1798
|
+
expandedBoundaryTokens: options.expandedBoundaryTokens ?? new Set(),
|
|
1799
|
+
boundaryGeneration: options.boundaryGeneration ?? 0,
|
|
1042
1800
|
removed: new Set(),
|
|
1043
1801
|
flagged: new Set(),
|
|
1044
1802
|
reserved: new Set(),
|
|
@@ -1046,7 +1804,7 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
|
1046
1804
|
objectsUsed: 0,
|
|
1047
1805
|
nodesUsed: 0,
|
|
1048
1806
|
};
|
|
1049
|
-
walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, []);
|
|
1807
|
+
walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, [], [{ pointer: '#', node: root }]);
|
|
1050
1808
|
for (const level of ctx.queue)
|
|
1051
1809
|
for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
|
|
1052
1810
|
enter();
|
|
@@ -1066,6 +1824,13 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
|
1066
1824
|
if (node) {
|
|
1067
1825
|
;
|
|
1068
1826
|
node.boundaries = ['recursion', 'budget'].filter((reason) => reasons.has(reason));
|
|
1827
|
+
const targets = ctx.boundaryTargets.get(pointer);
|
|
1828
|
+
if (targets) {
|
|
1829
|
+
;
|
|
1830
|
+
node.boundaryTargets = [
|
|
1831
|
+
...targets.values(),
|
|
1832
|
+
];
|
|
1833
|
+
}
|
|
1069
1834
|
}
|
|
1070
1835
|
}
|
|
1071
1836
|
return { nodes, diagnostics };
|