@texaryn/schema-json 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -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 +952 -156
- 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,6 +1,6 @@
|
|
|
1
|
-
import { mergeNode, isSchemaNode } from 'json-schema-library';
|
|
2
|
-
import { childDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, } from './identity.js';
|
|
3
|
-
import { inferProjectionShape, shapeDiagnostic } from './projection-shape.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
|
+
import { inferProjectionShape, projectionTypeFamilies, shapeDiagnostic, shapeOfFamilies, } from './projection-shape.js';
|
|
4
4
|
import { POSITION } from './schema-graph.js';
|
|
5
5
|
const VALID_TYPES = new Set([
|
|
6
6
|
'string',
|
|
@@ -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
|
}
|
|
@@ -46,7 +58,7 @@ function compositionHasRenderableAlternative(node) {
|
|
|
46
58
|
return false;
|
|
47
59
|
if (resolveExplicitType(schema))
|
|
48
60
|
return true;
|
|
49
|
-
return
|
|
61
|
+
return inferAllOfProjectionShape(resolved, schema).kind === 'resolved';
|
|
50
62
|
});
|
|
51
63
|
}
|
|
52
64
|
function extractConstraints(schema) {
|
|
@@ -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,58 +128,413 @@ 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 inferAllOfProjectionShape(node, schema = node.schema) {
|
|
277
|
+
const families = new Set();
|
|
278
|
+
const addSchema = (candidate) => {
|
|
279
|
+
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate))
|
|
280
|
+
return;
|
|
281
|
+
for (const family of projectionTypeFamilies(candidate)) {
|
|
282
|
+
families.add(family);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
addSchema(schema);
|
|
286
|
+
const visited = new Set();
|
|
287
|
+
const visit = (current) => {
|
|
288
|
+
const position = positionOf(current);
|
|
289
|
+
if (visited.has(position))
|
|
290
|
+
return;
|
|
291
|
+
visited.add(position);
|
|
292
|
+
for (const branch of current.allOf ?? []) {
|
|
293
|
+
const reference = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
|
|
294
|
+
if (reference?.unresolved || (reference && !isSchemaNode(reference.node)))
|
|
295
|
+
continue;
|
|
296
|
+
const target = reference?.node ?? branch;
|
|
297
|
+
if (!isSchemaNode(target))
|
|
298
|
+
continue;
|
|
299
|
+
addSchema(target.schema);
|
|
300
|
+
visit(target);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
visit(node);
|
|
304
|
+
return shapeOfFamilies(families);
|
|
305
|
+
}
|
|
306
|
+
function visitReferenceDefaultSites(node, cache, visitor, dynamicReferenceProjection = false) {
|
|
307
|
+
if (cache.dialect === 'draft-07' || referenceOf(node) === undefined)
|
|
308
|
+
return;
|
|
309
|
+
const pending = [node];
|
|
310
|
+
const visited = new Set();
|
|
311
|
+
while (pending.length > 0) {
|
|
312
|
+
const current = pending.pop();
|
|
313
|
+
const position = positionOf(current);
|
|
314
|
+
if (visited.has(position))
|
|
315
|
+
continue;
|
|
316
|
+
visited.add(position);
|
|
317
|
+
const authored = authoredSchema(current, cache);
|
|
318
|
+
if (authored !== null &&
|
|
319
|
+
typeof authored === 'object' &&
|
|
320
|
+
'default' in authored) {
|
|
321
|
+
visitor(current);
|
|
322
|
+
}
|
|
323
|
+
for (const keyword of referenceKeywords(current)) {
|
|
324
|
+
if (dynamicReferenceProjection && (keyword === '$dynamicRef' || keyword === '$recursiveRef'))
|
|
325
|
+
continue;
|
|
326
|
+
const target = followRef(current, keyword);
|
|
327
|
+
if (target)
|
|
328
|
+
pending.push(target);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function traversalKey(node) {
|
|
333
|
+
const parent = node.parent;
|
|
334
|
+
const isInPlaceBranch = parent !== undefined &&
|
|
335
|
+
['if', 'then', 'else', 'contains'].some((keyword) => parent[keyword] === node || node.evaluationPath === `${parent.evaluationPath}/${keyword}`);
|
|
336
|
+
if (isInPlaceBranch) {
|
|
337
|
+
return positionOf(node);
|
|
338
|
+
}
|
|
339
|
+
const location = node.schemaLocation;
|
|
340
|
+
return typeof location === 'string' ? location : positionOf(node);
|
|
341
|
+
}
|
|
342
|
+
function allOfExplicitType(node, visited = new Set()) {
|
|
343
|
+
const position = positionOf(node);
|
|
344
|
+
if (visited.has(position))
|
|
345
|
+
return undefined;
|
|
346
|
+
visited.add(position);
|
|
347
|
+
for (const branch of node.allOf ?? []) {
|
|
348
|
+
const reference = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
|
|
349
|
+
if (reference?.unresolved || (reference && !isSchemaNode(reference.node)))
|
|
350
|
+
continue;
|
|
351
|
+
const target = reference?.node ?? branch;
|
|
352
|
+
const schema = target.schema;
|
|
353
|
+
if (schema && typeof schema === 'object') {
|
|
354
|
+
const type = resolveExplicitType(schema);
|
|
355
|
+
if (type)
|
|
356
|
+
return type;
|
|
357
|
+
}
|
|
358
|
+
const nested = allOfExplicitType(target, visited);
|
|
359
|
+
if (nested)
|
|
360
|
+
return nested;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
function declaresObject(node, visited = new Set()) {
|
|
365
|
+
const resolved = dereference(node);
|
|
366
|
+
if (!isSchemaNode(resolved))
|
|
367
|
+
return false;
|
|
368
|
+
const position = positionOf(resolved);
|
|
369
|
+
if (visited.has(position))
|
|
370
|
+
return false;
|
|
371
|
+
visited.add(position);
|
|
372
|
+
const schema = resolved.schema;
|
|
373
|
+
if (!schema || typeof schema !== 'object')
|
|
374
|
+
return false;
|
|
375
|
+
const explicitType = resolveExplicitType(schema);
|
|
376
|
+
if (explicitType)
|
|
377
|
+
return explicitType === 'object';
|
|
378
|
+
const allOfType = allOfExplicitType(resolved);
|
|
379
|
+
if (allOfType)
|
|
380
|
+
return allOfType === 'object';
|
|
381
|
+
const shape = inferAllOfProjectionShape(resolved, schema);
|
|
382
|
+
if (shape.kind === 'resolved')
|
|
383
|
+
return shape.type === 'object';
|
|
384
|
+
return (resolved.allOf ?? []).some((branch) => declaresObject(branch, visited));
|
|
385
|
+
}
|
|
386
|
+
function declaresArray(node, visited = new Set()) {
|
|
387
|
+
const resolved = dereference(node);
|
|
388
|
+
if (!isSchemaNode(resolved))
|
|
389
|
+
return false;
|
|
390
|
+
const position = positionOf(resolved);
|
|
391
|
+
if (visited.has(position))
|
|
392
|
+
return false;
|
|
393
|
+
visited.add(position);
|
|
394
|
+
const schema = resolved.schema;
|
|
395
|
+
if (!schema || typeof schema !== 'object')
|
|
396
|
+
return false;
|
|
397
|
+
const explicitType = resolveExplicitType(schema);
|
|
398
|
+
if (explicitType)
|
|
399
|
+
return explicitType === 'array';
|
|
400
|
+
const allOfType = allOfExplicitType(resolved);
|
|
401
|
+
if (allOfType)
|
|
402
|
+
return allOfType === 'array';
|
|
403
|
+
const shape = inferAllOfProjectionShape(resolved, schema);
|
|
404
|
+
if (shape.kind === 'resolved')
|
|
405
|
+
return shape.type === 'array';
|
|
406
|
+
return (resolved.allOf ?? []).some((branch) => declaresArray(branch, visited));
|
|
407
|
+
}
|
|
408
|
+
function referenceCompositionType(parts) {
|
|
409
|
+
const explicit = new Set(parts
|
|
410
|
+
.map((part) => {
|
|
411
|
+
const schema = part.schema;
|
|
412
|
+
return (schema && typeof schema === 'object' ? resolveExplicitType(schema) : undefined) ?? allOfExplicitType(part);
|
|
413
|
+
})
|
|
414
|
+
.filter((type) => type !== undefined));
|
|
415
|
+
if (explicit.size === 1)
|
|
416
|
+
return [...explicit][0];
|
|
417
|
+
if (explicit.size > 1)
|
|
418
|
+
return undefined;
|
|
419
|
+
const object = parts.some((part) => declaresObject(part));
|
|
420
|
+
const array = parts.some((part) => declaresArray(part));
|
|
421
|
+
if (object === array)
|
|
422
|
+
return undefined;
|
|
423
|
+
return object ? 'object' : 'array';
|
|
424
|
+
}
|
|
425
|
+
function hasReferencedAllOf(node, visited = new Set()) {
|
|
426
|
+
const position = positionOf(node);
|
|
427
|
+
if (visited.has(position))
|
|
428
|
+
return false;
|
|
429
|
+
visited.add(position);
|
|
430
|
+
for (const branch of node.allOf ?? []) {
|
|
431
|
+
if (referenceOf(branch) !== undefined)
|
|
432
|
+
return true;
|
|
433
|
+
if (hasReferencedAllOf(branch, visited))
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
function hasUnresolvedAllOfReference(node, visited = new Set()) {
|
|
439
|
+
const position = positionOf(node);
|
|
440
|
+
if (visited.has(position))
|
|
441
|
+
return false;
|
|
442
|
+
visited.add(position);
|
|
443
|
+
for (const branch of node.allOf ?? []) {
|
|
444
|
+
const source = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
|
|
445
|
+
if (source?.unresolved || (source && !isSchemaNode(source.node)))
|
|
446
|
+
return true;
|
|
447
|
+
const target = source?.node ?? branch;
|
|
448
|
+
if (hasUnresolvedAllOfReference(target, visited))
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
function expandAllOfReferences(node, stack = new Set()) {
|
|
454
|
+
const position = positionOf(node);
|
|
455
|
+
if (stack.has(position))
|
|
456
|
+
return node;
|
|
457
|
+
const nextStack = new Set(stack).add(position);
|
|
458
|
+
const branches = [];
|
|
459
|
+
let changed = false;
|
|
460
|
+
for (const branch of node.allOf ?? []) {
|
|
461
|
+
if (referenceOf(branch) !== undefined) {
|
|
462
|
+
const { node: target, unresolved } = dereferenceChecked(branch);
|
|
463
|
+
if (unresolved || !isSchemaNode(target))
|
|
464
|
+
return undefined;
|
|
465
|
+
if (nextStack.has(positionOf(target))) {
|
|
466
|
+
branches.push(branch);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const expanded = expandAllOfReferences(target, nextStack);
|
|
470
|
+
if (!expanded)
|
|
471
|
+
return undefined;
|
|
472
|
+
branches.push(expanded);
|
|
473
|
+
changed = true;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const expanded = expandAllOfReferences(branch, nextStack);
|
|
477
|
+
if (!expanded)
|
|
478
|
+
return undefined;
|
|
479
|
+
branches.push(expanded);
|
|
480
|
+
changed ||= expanded !== branch;
|
|
481
|
+
}
|
|
482
|
+
return changed ? { ...node, allOf: branches } : node;
|
|
483
|
+
}
|
|
484
|
+
function reduceAllOfForProjection(node, data, scope = []) {
|
|
485
|
+
if (hasUnresolvedAllOfReference(node))
|
|
486
|
+
return undefined;
|
|
487
|
+
if (node.getDraftVersion() !== 'draft-07')
|
|
488
|
+
return node.reduceNode(data, { path: [...scope] }).node;
|
|
489
|
+
const expanded = expandAllOfReferences(node);
|
|
490
|
+
return expanded?.reduceNode(data, { path: [...scope] }).node;
|
|
491
|
+
}
|
|
134
492
|
// A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
|
|
135
|
-
function resolveSelectedBranch(original, reduced, data) {
|
|
493
|
+
function resolveSelectedBranch(original, reduced, data, scope = [], branchApplicability) {
|
|
136
494
|
const reducedSchema = reduced.schema;
|
|
137
495
|
if (reducedSchema && typeof reducedSchema === 'object') {
|
|
138
|
-
if (resolveExplicitType(reducedSchema) ||
|
|
496
|
+
if (resolveExplicitType(reducedSchema) ||
|
|
497
|
+
inferAllOfProjectionShape(reduced, reducedSchema).kind === 'resolved') {
|
|
139
498
|
return reduced;
|
|
140
499
|
}
|
|
141
500
|
}
|
|
142
501
|
const selected = [];
|
|
143
502
|
if (original.oneOf) {
|
|
144
|
-
const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
|
|
503
|
+
const matching = original.oneOf.filter((branch) => branchApplies(branch, data, scope, branchApplicability));
|
|
145
504
|
if (matching.length === 1)
|
|
146
505
|
selected.push(matching[0]);
|
|
147
506
|
}
|
|
148
507
|
for (const branch of original.anyOf ?? []) {
|
|
149
|
-
if (branchApplies(branch, data))
|
|
508
|
+
if (branchApplies(branch, data, scope, branchApplicability))
|
|
150
509
|
selected.push(branch);
|
|
151
510
|
}
|
|
152
|
-
if (!selected.some((branch) =>
|
|
511
|
+
if (!selected.some((branch) => referenceOf(branch) !== undefined))
|
|
153
512
|
return reduced;
|
|
154
513
|
let merged = reduced;
|
|
155
514
|
for (const branch of selected) {
|
|
156
515
|
const target = dereference(branch);
|
|
157
516
|
if (!isSchemaNode(target))
|
|
158
517
|
continue;
|
|
159
|
-
const targetReduced = target.reduceNode(data).node ?? target;
|
|
518
|
+
const targetReduced = target.reduceNode(data, { path: [...scope] }).node ?? target;
|
|
160
519
|
merged = mergeNode(merged, targetReduced) ?? merged;
|
|
161
520
|
}
|
|
162
521
|
return merged;
|
|
163
522
|
}
|
|
523
|
+
/** Fills properties a selected Draft 7 conditional `$ref` loses during reduction. */
|
|
524
|
+
function resolveSelectedConditionalReference(original, reduced, data, scope = [], branchApplicability) {
|
|
525
|
+
if (original.getDraftVersion() !== 'draft-07' || !original.if)
|
|
526
|
+
return reduced;
|
|
527
|
+
const selected = branchApplies(original.if, data, scope, branchApplicability) ? original.then : original.else;
|
|
528
|
+
if (!selected || typeof selected.$ref !== 'string')
|
|
529
|
+
return reduced;
|
|
530
|
+
const target = dereference(selected);
|
|
531
|
+
if (!isSchemaNode(target))
|
|
532
|
+
return reduced;
|
|
533
|
+
const targetReduced = reduceAllOfForProjection(target, data, scope) ?? target;
|
|
534
|
+
const reducedProperties = reduced.properties ?? {};
|
|
535
|
+
const hasUnprojectedProperty = Object.keys(targetReduced.properties ?? {}).some((key) => !Object.hasOwn(reducedProperties, key));
|
|
536
|
+
return hasUnprojectedProperty ? mergeNode(reduced, targetReduced) ?? reduced : reduced;
|
|
537
|
+
}
|
|
164
538
|
export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
|
|
165
539
|
function walkOrder(a, b) {
|
|
166
540
|
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
@@ -168,10 +542,19 @@ function walkOrder(a, b) {
|
|
|
168
542
|
return a[i] - b[i];
|
|
169
543
|
return a.length - b.length;
|
|
170
544
|
}
|
|
171
|
-
function
|
|
545
|
+
function boundaryToken(pointer, schemaKey, generation) {
|
|
546
|
+
return JSON.stringify([pointer, schemaKey, generation]);
|
|
547
|
+
}
|
|
548
|
+
function addBoundary(ctx, pointer, reason, targetPointer, schemaKey) {
|
|
172
549
|
const reasons = ctx.boundaries.get(pointer) ?? new Set();
|
|
173
550
|
reasons.add(reason);
|
|
174
551
|
ctx.boundaries.set(pointer, reasons);
|
|
552
|
+
const targets = ctx.boundaryTargets.get(pointer) ?? new Map();
|
|
553
|
+
if (![...targets.values()].some((target) => target.reason === reason)) {
|
|
554
|
+
const token = boundaryToken(targetPointer, schemaKey, ctx.boundaryGeneration);
|
|
555
|
+
targets.set(token, { pointer: toPointer(targetPointer), reason, token });
|
|
556
|
+
}
|
|
557
|
+
ctx.boundaryTargets.set(pointer, targets);
|
|
175
558
|
}
|
|
176
559
|
// A leaf needs no reduction, so it rides with its parent's budget unit.
|
|
177
560
|
function isLeafSchema(schema) {
|
|
@@ -184,17 +567,14 @@ function isLeafSchema(schema) {
|
|
|
184
567
|
return ('enum' in record || 'const' in record) && !('properties' in record);
|
|
185
568
|
}
|
|
186
569
|
/**
|
|
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.
|
|
570
|
+
* The authored schema position as a JSON Pointer with the root as the empty
|
|
571
|
+
* string. Reference sites and targets keep separate positions, and encoded
|
|
572
|
+
* reference segments use their document spelling. Both adapters report
|
|
573
|
+
* sources in this form so diagnostics mean the same thing whichever produced
|
|
574
|
+
* them.
|
|
194
575
|
*/
|
|
195
576
|
function documentPointer(node) {
|
|
196
|
-
|
|
197
|
-
return typeof location === 'string' ? location.replace(/^#/, '') : '';
|
|
577
|
+
return positionOf(node).replace(/^#/, '');
|
|
198
578
|
}
|
|
199
579
|
/**
|
|
200
580
|
* Visits every schema position that applies to one location whatever the
|
|
@@ -217,16 +597,13 @@ function documentPointer(node) {
|
|
|
217
597
|
* several places at once: `properties/x` on the node's own schema and on each
|
|
218
598
|
* of its `allOf` branches are all declarations of the same location.
|
|
219
599
|
*/
|
|
220
|
-
function eachUnconditional(roots, visitor) {
|
|
600
|
+
function eachUnconditional(roots, visitor, referenceVisitor, cache) {
|
|
221
601
|
const visited = new Set();
|
|
222
602
|
const visit = (current) => {
|
|
603
|
+
if (referenceVisitor && cache)
|
|
604
|
+
visitReferenceDefaultSites(current, cache, referenceVisitor);
|
|
223
605
|
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;
|
|
606
|
+
const key = traversalKey(resolved);
|
|
230
607
|
if (visited.has(key))
|
|
231
608
|
return;
|
|
232
609
|
visited.add(key);
|
|
@@ -249,9 +626,15 @@ function eachUnconditional(roots, visitor) {
|
|
|
249
626
|
* own descent already survives the shapes that throw (#119, #121), and a
|
|
250
627
|
* conflict is the wrong place to surface one.
|
|
251
628
|
*/
|
|
252
|
-
function branchApplies(branch, data) {
|
|
629
|
+
function branchApplies(branch, data, scope = [], branchApplicability) {
|
|
630
|
+
if (data === undefined && branchApplicability?.has(branch)) {
|
|
631
|
+
return branchApplicability.get(branch);
|
|
632
|
+
}
|
|
253
633
|
try {
|
|
254
|
-
|
|
634
|
+
const applies = branch.validate(data, '#', [...scope]).valid === true;
|
|
635
|
+
if (data === undefined)
|
|
636
|
+
branchApplicability?.set(branch, applies);
|
|
637
|
+
return applies;
|
|
255
638
|
}
|
|
256
639
|
catch {
|
|
257
640
|
return false;
|
|
@@ -270,18 +653,23 @@ function branchApplies(branch, data) {
|
|
|
270
653
|
* rule the projection uses to expose that branch, so the two cannot disagree
|
|
271
654
|
* about which branch it is.
|
|
272
655
|
*/
|
|
273
|
-
function eachApplicable(roots, data, visitor) {
|
|
656
|
+
function eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope = [], dynamicReferenceProjection = false) {
|
|
274
657
|
const visited = new Set();
|
|
275
|
-
const visit = (current) => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
658
|
+
const visit = (current, parentScope) => {
|
|
659
|
+
if (referenceVisitor && cache)
|
|
660
|
+
visitReferenceDefaultSites(current, cache, referenceVisitor, dynamicReferenceProjection);
|
|
661
|
+
const entered = dynamicReferenceProjection
|
|
662
|
+
? enterDynamicScope(current, parentScope)
|
|
663
|
+
: { node: dereference(current), path: parentScope, dynamicReference: false };
|
|
664
|
+
const resolved = entered.node;
|
|
665
|
+
const currentScope = entered.path;
|
|
666
|
+
const key = traversalKey(resolved);
|
|
279
667
|
if (visited.has(key))
|
|
280
668
|
return;
|
|
281
669
|
visited.add(key);
|
|
282
670
|
visitor(resolved);
|
|
283
671
|
for (const branch of resolved.allOf ?? [])
|
|
284
|
-
visit(branch);
|
|
672
|
+
visit(branch, currentScope);
|
|
285
673
|
// `oneOf` contributes the one branch it selects, which is the evaluator's
|
|
286
674
|
// rule and not "every branch the data matches": an empty object matches
|
|
287
675
|
// every permissive branch, and treating all of them as applicable would
|
|
@@ -290,82 +678,123 @@ function eachApplicable(roots, data, visitor) {
|
|
|
290
678
|
// branch stands in, which is what the projection exposes.
|
|
291
679
|
const oneOf = resolved.oneOf ?? [];
|
|
292
680
|
if (oneOf.length > 0) {
|
|
293
|
-
const matching = oneOf.filter((branch) => branchApplies(branch, data));
|
|
681
|
+
const matching = oneOf.filter((branch) => branchApplies(branch, data, currentScope, branchApplicability));
|
|
294
682
|
const selected = matching.length === 1
|
|
295
683
|
? matching[0]
|
|
296
684
|
: selectProvisionalBranch(resolved, typeof data === 'object' && data !== null
|
|
297
685
|
? data
|
|
298
686
|
: undefined);
|
|
299
687
|
if (selected)
|
|
300
|
-
visit(selected);
|
|
688
|
+
visit(selected, currentScope);
|
|
301
689
|
}
|
|
302
690
|
// `anyOf` is the opposite case and needs no such rule: several of its
|
|
303
691
|
// branches applying at once is ordinary, and each that validates
|
|
304
692
|
// contributes its annotations, so each competes.
|
|
305
693
|
for (const branch of resolved.anyOf ?? []) {
|
|
306
|
-
if (branchApplies(branch, data))
|
|
307
|
-
visit(branch);
|
|
694
|
+
if (branchApplies(branch, data, currentScope, branchApplicability))
|
|
695
|
+
visit(branch, currentScope);
|
|
308
696
|
}
|
|
309
697
|
if (resolved.if) {
|
|
310
|
-
const taken = branchApplies(resolved.if, data) ? resolved.then : resolved.else;
|
|
698
|
+
const taken = branchApplies(resolved.if, data, currentScope, branchApplicability) ? resolved.then : resolved.else;
|
|
311
699
|
if (taken)
|
|
312
|
-
visit(taken);
|
|
700
|
+
visit(taken, currentScope);
|
|
313
701
|
}
|
|
314
702
|
const dependent = resolved.dependentSchemas;
|
|
315
703
|
if (dependent && typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
316
704
|
for (const [dependency, subschema] of Object.entries(dependent)) {
|
|
317
705
|
if (Object.prototype.hasOwnProperty.call(data, dependency))
|
|
318
|
-
visit(subschema);
|
|
706
|
+
visit(subschema, currentScope);
|
|
319
707
|
}
|
|
320
708
|
}
|
|
321
709
|
};
|
|
322
710
|
for (const root of roots)
|
|
323
|
-
visit(root);
|
|
711
|
+
visit(root, scope);
|
|
324
712
|
}
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
713
|
+
function enterDynamicScope(node, scope) {
|
|
714
|
+
const path = [...scope];
|
|
715
|
+
let current = node;
|
|
716
|
+
let dynamicReference = false;
|
|
717
|
+
const seen = new Set();
|
|
718
|
+
while (typeof current.$ref === 'string' ||
|
|
719
|
+
typeof current.schema.$dynamicRef === 'string' ||
|
|
720
|
+
typeof current.schema.$recursiveRef === 'string') {
|
|
721
|
+
const position = positionOf(current);
|
|
722
|
+
if (seen.has(position))
|
|
723
|
+
break;
|
|
724
|
+
seen.add(position);
|
|
725
|
+
const resolved = current.resolveRef({ pointer: '#', path });
|
|
726
|
+
if (!isSchemaNode(resolved))
|
|
727
|
+
break;
|
|
728
|
+
const schema = current.schema;
|
|
729
|
+
if (typeof schema.$dynamicRef === 'string' || typeof schema.$recursiveRef === 'string') {
|
|
730
|
+
dynamicReference = true;
|
|
331
731
|
}
|
|
332
|
-
|
|
732
|
+
if (resolved === current)
|
|
733
|
+
break;
|
|
734
|
+
if (typeof current.$ref === 'string' &&
|
|
735
|
+
current.getDraftVersion() !== 'draft-07' &&
|
|
736
|
+
!onlyPoints(current)) {
|
|
737
|
+
return { node: dereference(current), path, dynamicReference };
|
|
738
|
+
}
|
|
739
|
+
current = resolved;
|
|
740
|
+
}
|
|
741
|
+
if (path[path.length - 1]?.node !== current)
|
|
742
|
+
path.push({ pointer: '#', node: current });
|
|
743
|
+
return { node: current, path, dynamicReference };
|
|
744
|
+
}
|
|
745
|
+
function declarationsFrom(walkRoots, cache) {
|
|
746
|
+
const declarations = [];
|
|
747
|
+
const seen = new Set();
|
|
748
|
+
const add = (node) => {
|
|
749
|
+
const schema = authoredSchema(node, cache);
|
|
750
|
+
if (schema === null || typeof schema !== 'object' || !('default' in schema))
|
|
751
|
+
return;
|
|
752
|
+
const record = schema;
|
|
753
|
+
if (typeof record.$ref === 'string' && cache.dialect === 'draft-07')
|
|
754
|
+
return;
|
|
755
|
+
const source = documentPointer(node);
|
|
756
|
+
if (seen.has(source))
|
|
757
|
+
return;
|
|
758
|
+
seen.add(source);
|
|
759
|
+
declarations.push({ value: record.default, source });
|
|
760
|
+
};
|
|
761
|
+
walkRoots(add, add);
|
|
333
762
|
return declarations;
|
|
334
763
|
}
|
|
335
|
-
function collectUnconditionalDefaults(roots) {
|
|
336
|
-
return declarationsFrom((visitor) => eachUnconditional(roots, visitor));
|
|
764
|
+
function collectUnconditionalDefaults(roots, cache) {
|
|
765
|
+
return declarationsFrom((visitor, referenceVisitor) => eachUnconditional(roots, visitor, referenceVisitor, cache), cache);
|
|
337
766
|
}
|
|
338
|
-
function collectApplicableDefaults(roots, data) {
|
|
339
|
-
return declarationsFrom((visitor) => eachApplicable(roots, data, visitor));
|
|
767
|
+
function collectApplicableDefaults(roots, data, cache, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
768
|
+
return declarationsFrom((visitor, referenceVisitor) => eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope, dynamicReferenceProjection), cache);
|
|
340
769
|
}
|
|
341
770
|
/** The positions that declare one property of `roots`, by both reachability rules. */
|
|
342
|
-
function childPositions(roots, key, data, declaring) {
|
|
771
|
+
function childPositions(roots, key, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
343
772
|
const unconditional = [];
|
|
344
773
|
eachUnconditional(roots.unconditional, (node) => {
|
|
345
|
-
const child = node
|
|
774
|
+
const child = memberSchema(node, key);
|
|
346
775
|
if (child)
|
|
347
776
|
unconditional.push(child);
|
|
348
777
|
});
|
|
349
778
|
const applicable = [];
|
|
350
|
-
eachApplicable(roots.applicable, data, (node) => {
|
|
351
|
-
const child = node
|
|
779
|
+
eachApplicable(roots.applicable, data, branchApplicability, (node) => {
|
|
780
|
+
const child = memberSchema(node, key);
|
|
352
781
|
if (child)
|
|
353
782
|
applicable.push(child);
|
|
354
|
-
});
|
|
783
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
355
784
|
return { unconditional, applicable, declaring };
|
|
356
785
|
}
|
|
357
786
|
/** The positions that declare the elements of `roots`, by both reachability rules. */
|
|
358
|
-
function itemPositions(roots, data, declaring) {
|
|
787
|
+
function itemPositions(roots, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
359
788
|
const unconditional = [];
|
|
360
789
|
eachUnconditional(roots.unconditional, (node) => {
|
|
361
790
|
if (node.items)
|
|
362
791
|
unconditional.push(node.items);
|
|
363
792
|
});
|
|
364
793
|
const applicable = [];
|
|
365
|
-
eachApplicable(roots.applicable, data, (node) => {
|
|
794
|
+
eachApplicable(roots.applicable, data, branchApplicability, (node) => {
|
|
366
795
|
if (node.items)
|
|
367
796
|
applicable.push(node.items);
|
|
368
|
-
});
|
|
797
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
369
798
|
return { unconditional, applicable, declaring };
|
|
370
799
|
}
|
|
371
800
|
/**
|
|
@@ -380,16 +809,75 @@ function disagreeingDefaults(declarations) {
|
|
|
380
809
|
const [first, ...rest] = declarations;
|
|
381
810
|
return rest.every((other) => deepEqual(other.value, first.value)) ? undefined : declarations;
|
|
382
811
|
}
|
|
812
|
+
function additionalPropertyKeys(node, data) {
|
|
813
|
+
if (!node?.additionalProperties)
|
|
814
|
+
return [];
|
|
815
|
+
const keys = new Set([...Object.keys(data ?? {}), ...(node.required ?? [])]);
|
|
816
|
+
return [...keys].filter((key) => !Object.hasOwn(node.properties ?? {}, key) &&
|
|
817
|
+
!node.patternProperties?.some(({ pattern }) => pattern.test(key)));
|
|
818
|
+
}
|
|
819
|
+
function additionalPropertySchema(node, key) {
|
|
820
|
+
if (!node || Object.hasOwn(node.properties ?? {}, key))
|
|
821
|
+
return undefined;
|
|
822
|
+
if (node.patternProperties?.some(({ pattern }) => pattern.test(key)))
|
|
823
|
+
return undefined;
|
|
824
|
+
return node.additionalProperties;
|
|
825
|
+
}
|
|
826
|
+
function memberSchema(node, key) {
|
|
827
|
+
if (!node)
|
|
828
|
+
return undefined;
|
|
829
|
+
if (Object.hasOwn(node.properties ?? {}, key))
|
|
830
|
+
return node.properties?.[key];
|
|
831
|
+
return additionalPropertySchema(node, key);
|
|
832
|
+
}
|
|
833
|
+
function applicableAdditionalPropertySchemas(roots, key, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
834
|
+
const schemas = [];
|
|
835
|
+
const seen = new Set();
|
|
836
|
+
eachApplicable(roots, data, branchApplicability, (node) => {
|
|
837
|
+
const schema = additionalPropertySchema(node, key);
|
|
838
|
+
if (!schema)
|
|
839
|
+
return;
|
|
840
|
+
const position = positionOf(schema);
|
|
841
|
+
if (seen.has(position))
|
|
842
|
+
return;
|
|
843
|
+
seen.add(position);
|
|
844
|
+
schemas.push(schema);
|
|
845
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
846
|
+
return schemas;
|
|
847
|
+
}
|
|
848
|
+
function applicableRequiredKeys(roots, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
849
|
+
const keys = new Set();
|
|
850
|
+
eachApplicable(roots, data, branchApplicability, (node) => {
|
|
851
|
+
for (const key of node.required ?? [])
|
|
852
|
+
keys.add(key);
|
|
853
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
854
|
+
return keys;
|
|
855
|
+
}
|
|
856
|
+
function unconditionalRequiredKeys(roots) {
|
|
857
|
+
const keys = new Set();
|
|
858
|
+
eachUnconditional(roots, (node) => {
|
|
859
|
+
for (const key of node.required ?? [])
|
|
860
|
+
keys.add(key);
|
|
861
|
+
});
|
|
862
|
+
return keys;
|
|
863
|
+
}
|
|
864
|
+
function reduceAdditionalPropertySchema(source, data, scope = []) {
|
|
865
|
+
if (source.getDraftVersion() !== 'draft-07')
|
|
866
|
+
return source.reduceNode(data, { path: [...scope] }).node ?? source;
|
|
867
|
+
const { node, unresolved } = dereferenceChecked(source);
|
|
868
|
+
if (unresolved || !isSchemaNode(node))
|
|
869
|
+
return source;
|
|
870
|
+
return reduceAllOfForProjection(node, data) ?? node;
|
|
871
|
+
}
|
|
872
|
+
const candidatePropertyCache = new WeakMap();
|
|
383
873
|
/**
|
|
384
|
-
*
|
|
385
|
-
* across every branch that might apply to it.
|
|
874
|
+
* Property keys that might need to appear at this instance location.
|
|
386
875
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* which
|
|
391
|
-
*
|
|
392
|
-
* matching, which is exactly the flicker the contract exists to prevent.
|
|
876
|
+
* Declared keys remain static candidates across all branches. Keys governed
|
|
877
|
+
* only by `additionalProperties` come from the current data, since the schema
|
|
878
|
+
* has no finite set of names to enumerate; required names are candidates too.
|
|
879
|
+
* `reduceNode` decides which candidates apply now, so an inactive branch keeps
|
|
880
|
+
* its declared pointers with `active: false`.
|
|
393
881
|
*
|
|
394
882
|
* **Recursion is through applicators only, and only those acting on this same
|
|
395
883
|
* instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
|
|
@@ -409,20 +897,20 @@ function disagreeingDefaults(declarations) {
|
|
|
409
897
|
* - **`items` and `prefixItems`**, for the same reason: an array's items are
|
|
410
898
|
* their own locations.
|
|
411
899
|
*
|
|
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.
|
|
900
|
+
* Inline conditional branches use `positionOf` because the library can assign
|
|
901
|
+
* `if`, `then` and `else` the same `schemaLocation` after resolving a
|
|
902
|
+
* reference. Other nodes keep their location key, so references still close
|
|
903
|
+
* at the same point in the traversal as before.
|
|
422
904
|
*/
|
|
423
|
-
function collectCandidateProperties(node) {
|
|
905
|
+
function collectCandidateProperties(node, data, declarations = []) {
|
|
906
|
+
if (declarations.length === 0 && candidatePropertyCache.has(node)) {
|
|
907
|
+
const cached = candidatePropertyCache.get(node);
|
|
908
|
+
if (cached)
|
|
909
|
+
return cached;
|
|
910
|
+
}
|
|
424
911
|
const candidates = new Map();
|
|
425
912
|
const visited = new Set();
|
|
913
|
+
const scopes = [];
|
|
426
914
|
const record = (key, propNode, direct) => {
|
|
427
915
|
const entry = candidates.get(key) ?? { alternatives: [] };
|
|
428
916
|
if (direct)
|
|
@@ -433,26 +921,11 @@ function collectCandidateProperties(node) {
|
|
|
433
921
|
};
|
|
434
922
|
const visit = (current, own) => {
|
|
435
923
|
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;
|
|
924
|
+
const key = traversalKey(resolved);
|
|
453
925
|
if (visited.has(key))
|
|
454
926
|
return;
|
|
455
927
|
visited.add(key);
|
|
928
|
+
scopes.push({ node: resolved, own });
|
|
456
929
|
for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
|
|
457
930
|
record(key, propNode, own);
|
|
458
931
|
}
|
|
@@ -460,7 +933,9 @@ function collectCandidateProperties(node) {
|
|
|
460
933
|
if (branch)
|
|
461
934
|
visit(branch, false);
|
|
462
935
|
}
|
|
463
|
-
for (const
|
|
936
|
+
for (const branch of resolved.allOf ?? [])
|
|
937
|
+
visit(branch, own);
|
|
938
|
+
for (const branches of [resolved.anyOf, resolved.oneOf]) {
|
|
464
939
|
for (const branch of branches ?? [])
|
|
465
940
|
visit(branch, false);
|
|
466
941
|
}
|
|
@@ -471,8 +946,32 @@ function collectCandidateProperties(node) {
|
|
|
471
946
|
}
|
|
472
947
|
};
|
|
473
948
|
visit(node, true);
|
|
949
|
+
for (const declaration of declarations)
|
|
950
|
+
visit(declaration, false);
|
|
951
|
+
const memberKeys = new Set(Object.keys(data ?? {}));
|
|
952
|
+
for (const scope of scopes)
|
|
953
|
+
for (const key of scope.node.required ?? [])
|
|
954
|
+
memberKeys.add(key);
|
|
955
|
+
for (const { node: scope, own } of scopes) {
|
|
956
|
+
for (const key of memberKeys) {
|
|
957
|
+
const propNode = additionalPropertySchema(scope, key);
|
|
958
|
+
if (propNode)
|
|
959
|
+
record(key, propNode, own);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const hasDataDependentAdditionalProperties = scopes.some(({ node: scope }) => Boolean(scope.additionalProperties));
|
|
963
|
+
if (declarations.length === 0) {
|
|
964
|
+
candidatePropertyCache.set(node, hasDataDependentAdditionalProperties ? null : candidates);
|
|
965
|
+
}
|
|
474
966
|
return candidates;
|
|
475
967
|
}
|
|
968
|
+
function hasRetainedDescendant(position, cache) {
|
|
969
|
+
for (const retained of cache.retainedPositions) {
|
|
970
|
+
if (retained === position || retained.startsWith(`${position}/`))
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
476
975
|
/**
|
|
477
976
|
* The schema to project for a candidate when no branch is active.
|
|
478
977
|
*
|
|
@@ -646,6 +1145,96 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
|
|
|
646
1145
|
}
|
|
647
1146
|
return required;
|
|
648
1147
|
}
|
|
1148
|
+
// reduceNode recompiles a subtree when any descendant has reduction work.
|
|
1149
|
+
const REDUCE_NODE_SCHEMA_FIELDS = [
|
|
1150
|
+
'$ref',
|
|
1151
|
+
'$dynamicRef',
|
|
1152
|
+
'$recursiveRef',
|
|
1153
|
+
'$defs',
|
|
1154
|
+
'if',
|
|
1155
|
+
'then',
|
|
1156
|
+
'else',
|
|
1157
|
+
'allOf',
|
|
1158
|
+
'anyOf',
|
|
1159
|
+
'oneOf',
|
|
1160
|
+
'contains',
|
|
1161
|
+
'dependentSchemas',
|
|
1162
|
+
'dependentRequired',
|
|
1163
|
+
'definitions',
|
|
1164
|
+
'dependencies',
|
|
1165
|
+
'patternProperties',
|
|
1166
|
+
'propertyDependencies',
|
|
1167
|
+
];
|
|
1168
|
+
const SCHEMA_MAP_KEYWORDS = [
|
|
1169
|
+
'$defs',
|
|
1170
|
+
'definitions',
|
|
1171
|
+
'dependentSchemas',
|
|
1172
|
+
'patternProperties',
|
|
1173
|
+
'properties',
|
|
1174
|
+
];
|
|
1175
|
+
const SCHEMA_SINGLE_KEYWORDS = [
|
|
1176
|
+
'additionalItems',
|
|
1177
|
+
'additionalProperties',
|
|
1178
|
+
'contains',
|
|
1179
|
+
'if',
|
|
1180
|
+
'items',
|
|
1181
|
+
'not',
|
|
1182
|
+
'propertyNames',
|
|
1183
|
+
'then',
|
|
1184
|
+
'else',
|
|
1185
|
+
'unevaluatedItems',
|
|
1186
|
+
'unevaluatedProperties',
|
|
1187
|
+
];
|
|
1188
|
+
const SCHEMA_ARRAY_KEYWORDS = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
|
|
1189
|
+
const reductionRequirementCache = new WeakMap();
|
|
1190
|
+
function schemaRequiresReduction(schema, visiting = new WeakSet()) {
|
|
1191
|
+
if (schema === true)
|
|
1192
|
+
return true;
|
|
1193
|
+
if (typeof schema !== 'object' || schema === null)
|
|
1194
|
+
return false;
|
|
1195
|
+
const cached = reductionRequirementCache.get(schema);
|
|
1196
|
+
if (cached !== undefined)
|
|
1197
|
+
return cached;
|
|
1198
|
+
if (visiting.has(schema))
|
|
1199
|
+
return false;
|
|
1200
|
+
visiting.add(schema);
|
|
1201
|
+
const record = schema;
|
|
1202
|
+
const direct = REDUCE_NODE_SCHEMA_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(record, field)) ||
|
|
1203
|
+
Object.prototype.hasOwnProperty.call(record, 'oneOfProperty') ||
|
|
1204
|
+
Array.isArray(record.type);
|
|
1205
|
+
const children = [];
|
|
1206
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
1207
|
+
const value = record[keyword];
|
|
1208
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1209
|
+
children.push(...Object.values(value));
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
|
|
1213
|
+
const value = record[keyword];
|
|
1214
|
+
if (keyword === 'items' && Array.isArray(value))
|
|
1215
|
+
children.push(...value);
|
|
1216
|
+
else if (value !== undefined)
|
|
1217
|
+
children.push(value);
|
|
1218
|
+
}
|
|
1219
|
+
for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
|
|
1220
|
+
const value = record[keyword];
|
|
1221
|
+
if (Array.isArray(value))
|
|
1222
|
+
children.push(...value);
|
|
1223
|
+
}
|
|
1224
|
+
for (const value of Object.values(record.dependencies ?? {}))
|
|
1225
|
+
children.push(value);
|
|
1226
|
+
for (const values of Object.values(record.propertyDependencies ?? {})) {
|
|
1227
|
+
if (typeof values === 'object' && values !== null)
|
|
1228
|
+
children.push(...Object.values(values));
|
|
1229
|
+
}
|
|
1230
|
+
const required = direct || children.some((child) => schemaRequiresReduction(child, visiting));
|
|
1231
|
+
visiting.delete(schema);
|
|
1232
|
+
reductionRequirementCache.set(schema, required);
|
|
1233
|
+
return required;
|
|
1234
|
+
}
|
|
1235
|
+
function requiresReduction(node) {
|
|
1236
|
+
return node.reducers.length > 0 || schemaRequiresReduction(node.schema);
|
|
1237
|
+
}
|
|
649
1238
|
// An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
|
|
650
1239
|
const budgeted = (info) => info.cyclic || info.key === '';
|
|
651
1240
|
/**
|
|
@@ -675,9 +1264,15 @@ declaredAt, ctx,
|
|
|
675
1264
|
/** Entered as an object member, which is the only way to be past the data. */
|
|
676
1265
|
member,
|
|
677
1266
|
/** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
|
|
678
|
-
lineage, path) {
|
|
1267
|
+
lineage, path, scope, dynamicReferenceAtLocation = false) {
|
|
1268
|
+
if (ctx.cache.dynamicReferenceProjection) {
|
|
1269
|
+
const entered = enterDynamicScope(node, scope);
|
|
1270
|
+
node = entered.node;
|
|
1271
|
+
scope = entered.path;
|
|
1272
|
+
dynamicReferenceAtLocation ||= entered.dynamicReference;
|
|
1273
|
+
}
|
|
679
1274
|
const info = locationInfo(declaredAt.declaring, ctx.cache);
|
|
680
|
-
const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
|
|
1275
|
+
const { node: original, cycle: referenceCycle, unresolved: unresolvedReference } = dereferenceChecked(node);
|
|
681
1276
|
if (info.cycle || referenceCycle) {
|
|
682
1277
|
diagnostics.push({
|
|
683
1278
|
pointer: toPointer(pointer),
|
|
@@ -688,10 +1283,13 @@ lineage, path) {
|
|
|
688
1283
|
return;
|
|
689
1284
|
}
|
|
690
1285
|
const pastData = member && (data === undefined || data === null);
|
|
691
|
-
const
|
|
1286
|
+
const token = boundaryToken(pointer, info.key, ctx.boundaryGeneration);
|
|
1287
|
+
const recursive = pastData && !unresolvedReference && ((lineage?.recursive ?? false) || budgeted(info));
|
|
692
1288
|
for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
|
|
693
|
-
if (info.key !== '' &&
|
|
694
|
-
|
|
1289
|
+
if (info.key !== '' &&
|
|
1290
|
+
ancestor.key === info.key &&
|
|
1291
|
+
!ctx.expandedBoundaryTokens.has(token)) {
|
|
1292
|
+
addBoundary(ctx, ancestor.pointer, 'recursion', pointer, info.key);
|
|
695
1293
|
ctx.removed.add(pointer);
|
|
696
1294
|
return;
|
|
697
1295
|
}
|
|
@@ -702,21 +1300,29 @@ lineage, path) {
|
|
|
702
1300
|
let admit;
|
|
703
1301
|
if (isLeafSchema(originalSchema)) {
|
|
704
1302
|
const covered = ctx.reserved.has(parent);
|
|
705
|
-
admit = covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
1303
|
+
admit = ctx.expandedBoundaryTokens.has(token) || covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
706
1304
|
if (admit && !covered)
|
|
707
1305
|
ctx.nodesUsed += 1;
|
|
708
1306
|
}
|
|
709
1307
|
else {
|
|
710
|
-
const
|
|
711
|
-
|
|
1308
|
+
const explicitlyExpanded = ctx.expandedBoundaryTokens.has(token);
|
|
1309
|
+
const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
|
|
1310
|
+
const candidateDeclarations = dynamicReferenceAtLocation
|
|
1311
|
+
? []
|
|
1312
|
+
: [...declaredAt.applicable, ...retainedDeclarations];
|
|
1313
|
+
const leaves = [...collectCandidateProperties(original, undefined, candidateDeclarations).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
|
|
1314
|
+
admit =
|
|
1315
|
+
explicitlyExpanded ||
|
|
1316
|
+
(ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes);
|
|
712
1317
|
if (admit) {
|
|
713
1318
|
ctx.objectsUsed += 1;
|
|
714
|
-
ctx.nodesUsed += 1 + leaves;
|
|
715
|
-
|
|
1319
|
+
ctx.nodesUsed += explicitlyExpanded ? 1 : 1 + leaves;
|
|
1320
|
+
if (!explicitlyExpanded)
|
|
1321
|
+
ctx.reserved.add(pointer);
|
|
716
1322
|
}
|
|
717
1323
|
}
|
|
718
1324
|
if (!admit) {
|
|
719
|
-
addBoundary(ctx, parent, 'budget');
|
|
1325
|
+
addBoundary(ctx, parent, 'budget', pointer, info.key);
|
|
720
1326
|
ctx.removed.add(pointer);
|
|
721
1327
|
return;
|
|
722
1328
|
}
|
|
@@ -727,6 +1333,36 @@ lineage, path) {
|
|
|
727
1333
|
let resolved = original;
|
|
728
1334
|
let schema = originalSchema;
|
|
729
1335
|
let type = resolveExplicitType(schema);
|
|
1336
|
+
const localShape = inferAllOfProjectionShape(original, schema);
|
|
1337
|
+
const allOfType = original.allOf ? allOfExplicitType(original) : undefined;
|
|
1338
|
+
const hasReferencedAllOfBranch = original.allOf ? hasReferencedAllOf(original) : false;
|
|
1339
|
+
if (original.allOf &&
|
|
1340
|
+
!original.oneOf &&
|
|
1341
|
+
!original.anyOf &&
|
|
1342
|
+
!type &&
|
|
1343
|
+
(localShape.kind !== 'resolved' || allOfType !== undefined) &&
|
|
1344
|
+
(hasReferencedAllOfBranch || allOfType !== undefined)) {
|
|
1345
|
+
const reductionData = (allOfType === 'object' || declaresObject(original)) &&
|
|
1346
|
+
(data === undefined || data === null)
|
|
1347
|
+
? {}
|
|
1348
|
+
: data;
|
|
1349
|
+
const reduced = reduceAllOfForProjection(original, reductionData, scope);
|
|
1350
|
+
const reducedSchema = reduced?.schema;
|
|
1351
|
+
if (reduced && reducedSchema && typeof reducedSchema === 'object') {
|
|
1352
|
+
resolved = reduced;
|
|
1353
|
+
schema = reducedSchema;
|
|
1354
|
+
if (!type)
|
|
1355
|
+
type = resolveExplicitType(schema);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (!type && ctx.cache.dynamicReferenceProjection && original.allOf?.length) {
|
|
1359
|
+
const reduced = original.reduceNode(data, { path: [...scope] }).node;
|
|
1360
|
+
if (reduced) {
|
|
1361
|
+
resolved = reduced;
|
|
1362
|
+
schema = reduced.schema;
|
|
1363
|
+
type = resolveExplicitType(schema);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
730
1366
|
// Whether this node's own active branch could be determined. Stays true for every
|
|
731
1367
|
// node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
|
|
732
1368
|
// (see below), where the node's own presence in the projection is the thing in
|
|
@@ -750,9 +1386,9 @@ lineage, path) {
|
|
|
750
1386
|
let wrapperIdentified = false;
|
|
751
1387
|
if (!type && (original.oneOf || original.anyOf)) {
|
|
752
1388
|
composedAgainstData = true;
|
|
753
|
-
const { node: branchNode } = original.reduceNode(data);
|
|
1389
|
+
const { node: branchNode } = original.reduceNode(data, { path: [...scope] });
|
|
754
1390
|
if (branchNode) {
|
|
755
|
-
resolved = resolveSelectedBranch(original, branchNode, data);
|
|
1391
|
+
resolved = resolveSelectedBranch(original, branchNode, data, scope, ctx.branchApplicability);
|
|
756
1392
|
schema = resolved.schema;
|
|
757
1393
|
type = resolveExplicitType(schema);
|
|
758
1394
|
}
|
|
@@ -812,7 +1448,7 @@ lineage, path) {
|
|
|
812
1448
|
// chosen, and inferring first would take a schema carrying both `properties`
|
|
813
1449
|
// and `oneOf` down the object path without ever resolving its branch.
|
|
814
1450
|
if (!type) {
|
|
815
|
-
const shape =
|
|
1451
|
+
const shape = inferAllOfProjectionShape(resolved, schema);
|
|
816
1452
|
if (shape.kind === 'resolved') {
|
|
817
1453
|
type = shape.type;
|
|
818
1454
|
}
|
|
@@ -834,6 +1470,15 @@ lineage, path) {
|
|
|
834
1470
|
diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
|
|
835
1471
|
}
|
|
836
1472
|
}
|
|
1473
|
+
// Arrays do not pass through the object path that reduces the active schema.
|
|
1474
|
+
if (type === 'array' && resolved === original && original.allOf) {
|
|
1475
|
+
const reduced = reduceAllOfForProjection(original, data, scope);
|
|
1476
|
+
const reducedSchema = reduced?.schema;
|
|
1477
|
+
if (reduced && reducedSchema && typeof reducedSchema === 'object') {
|
|
1478
|
+
resolved = reduced;
|
|
1479
|
+
schema = reducedSchema;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
837
1482
|
if (!type)
|
|
838
1483
|
return;
|
|
839
1484
|
const selfLineage = pastData
|
|
@@ -843,7 +1488,7 @@ lineage, path) {
|
|
|
843
1488
|
// pointer with no node has nothing to omit an annotation from, and saying
|
|
844
1489
|
// that its `default` is undecidable on top of saying it cannot be drawn at
|
|
845
1490
|
// all would be the same schema reported twice.
|
|
846
|
-
const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional));
|
|
1491
|
+
const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional, ctx.cache));
|
|
847
1492
|
if (ambiguousDefault) {
|
|
848
1493
|
diagnostics.push({
|
|
849
1494
|
pointer: toPointer(pointer),
|
|
@@ -858,13 +1503,18 @@ lineage, path) {
|
|
|
858
1503
|
// selects. A superset of the diagnostic's: every unconditional disagreement
|
|
859
1504
|
// is also an applicable one, so the node carries both kinds and a consumer
|
|
860
1505
|
// reads one place.
|
|
861
|
-
const
|
|
1506
|
+
const applicableData = type === 'object' && !(typeof data === 'object' && data !== null) ? {} : data;
|
|
1507
|
+
const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, applicableData, ctx.cache, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
862
1508
|
const applicableConflict = disagreeingDefaults(applicableDeclarations);
|
|
863
1509
|
const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
|
|
864
1510
|
const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
|
|
865
|
-
const defaultSources = !conflictedDefault &&
|
|
1511
|
+
const defaultSources = !conflictedDefault && applicableDeclarations.length > 0 && info.cyclic
|
|
866
1512
|
? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
|
|
867
1513
|
: undefined;
|
|
1514
|
+
const defaultOverride = {
|
|
1515
|
+
present: applicableDeclarations.length > 0,
|
|
1516
|
+
value: applicableDeclarations[0]?.value,
|
|
1517
|
+
};
|
|
868
1518
|
const nodeActive = active && branchResolved;
|
|
869
1519
|
// A node the schema applies is never also provisional; the two report
|
|
870
1520
|
// different facts and only the second is a choice this projection made. The
|
|
@@ -889,19 +1539,73 @@ lineage, path) {
|
|
|
889
1539
|
// wrapper case), it has already been reduced against the real `data` at this
|
|
890
1540
|
// pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
|
|
891
1541
|
// for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
|
|
892
|
-
let reducedNode = resolved === original
|
|
1542
|
+
let reducedNode = resolved === original
|
|
1543
|
+
? requiresReduction(resolved)
|
|
1544
|
+
? original.allOf && hasReferencedAllOf(original)
|
|
1545
|
+
? reduceAllOfForProjection(resolved, dataRecord ?? {}, scope)
|
|
1546
|
+
: resolved.reduceNode(dataRecord ?? {}, { path: [...scope] }).node
|
|
1547
|
+
: resolved
|
|
1548
|
+
: resolved;
|
|
1549
|
+
if (reducedNode && resolved === original && (!member || data !== undefined)) {
|
|
1550
|
+
// Missing child data has no evaluated conditional branch to recover.
|
|
1551
|
+
reducedNode = resolveSelectedConditionalReference(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
|
|
1552
|
+
}
|
|
893
1553
|
if (reducedNode &&
|
|
894
1554
|
resolved === original &&
|
|
895
1555
|
!resolveExplicitType(originalSchema) &&
|
|
896
1556
|
(original.oneOf || original.anyOf)) {
|
|
897
|
-
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
|
|
1557
|
+
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
|
|
898
1558
|
}
|
|
899
1559
|
const reducedSchema = reducedNode?.schema;
|
|
900
1560
|
const reducedProperties = reducedSchema?.properties ??
|
|
901
1561
|
schema.properties ??
|
|
902
1562
|
{};
|
|
903
|
-
|
|
1563
|
+
// Dynamic applicability is rebuilt below; only retained declarations belong beside it here.
|
|
1564
|
+
const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
|
|
1565
|
+
const candidateDeclarations = dynamicReferenceAtLocation
|
|
1566
|
+
? []
|
|
1567
|
+
: [...declaredAt.applicable, ...retainedDeclarations];
|
|
1568
|
+
let candidateProps = collectCandidateProperties(original, dataRecord, candidateDeclarations);
|
|
1569
|
+
const activeKeys = new Set([
|
|
1570
|
+
...Object.keys(reducedProperties),
|
|
1571
|
+
...additionalPropertyKeys(reducedNode, dataRecord),
|
|
1572
|
+
]);
|
|
1573
|
+
if (!reducedNode) {
|
|
1574
|
+
for (const [key, candidate] of candidateProps) {
|
|
1575
|
+
if (candidate.direct)
|
|
1576
|
+
activeKeys.add(key);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
904
1579
|
const requiredSet = computeRequiredSet(resolved, reducedSchema, dataRecord);
|
|
1580
|
+
const dynamicApplicableNodes = [];
|
|
1581
|
+
if (ctx.cache.dynamicReferenceProjection) {
|
|
1582
|
+
eachApplicable([original], data, ctx.branchApplicability, (applicableNode) => {
|
|
1583
|
+
dynamicApplicableNodes.push(applicableNode);
|
|
1584
|
+
for (const key of Object.keys(applicableNode.properties ?? {}))
|
|
1585
|
+
activeKeys.add(key);
|
|
1586
|
+
for (const key of applicableNode.required ?? [])
|
|
1587
|
+
requiredSet.add(key);
|
|
1588
|
+
}, undefined, undefined, scope, true);
|
|
1589
|
+
const expandedCandidates = new Map(candidateProps);
|
|
1590
|
+
const activeCandidateNode = reducedNode ?? resolved;
|
|
1591
|
+
if (activeCandidateNode !== original) {
|
|
1592
|
+
for (const [key, candidate] of collectCandidateProperties(activeCandidateNode, dataRecord)) {
|
|
1593
|
+
if (!expandedCandidates.has(key))
|
|
1594
|
+
expandedCandidates.set(key, candidate);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
for (const applicableNode of dynamicApplicableNodes) {
|
|
1598
|
+
for (const [key, propertyNode] of Object.entries(applicableNode.properties ?? {})) {
|
|
1599
|
+
if (!expandedCandidates.has(key))
|
|
1600
|
+
expandedCandidates.set(key, { direct: propertyNode, alternatives: [] });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
candidateProps = expandedCandidates;
|
|
1604
|
+
}
|
|
1605
|
+
if (!reducedNode) {
|
|
1606
|
+
for (const key of unconditionalRequiredKeys(declaredAt.unconditional))
|
|
1607
|
+
requiredSet.add(key);
|
|
1608
|
+
}
|
|
905
1609
|
// Two conditions, and they are not the same kind of condition.
|
|
906
1610
|
//
|
|
907
1611
|
// Exposure is load-bearing. Selection is local, but whether this node is
|
|
@@ -921,11 +1625,32 @@ lineage, path) {
|
|
|
921
1625
|
: undefined;
|
|
922
1626
|
const provisionalSchema = provisionalBranch?.schema;
|
|
923
1627
|
const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
|
|
1628
|
+
for (const key of additionalPropertyKeys(provisionalBranch, dataRecord))
|
|
1629
|
+
provisionalKeys.add(key);
|
|
924
1630
|
const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
|
|
925
1631
|
// Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
|
|
926
1632
|
// branch's properties are represented (the matching branch alone, via `resolved`,
|
|
927
1633
|
// would only expose its own properties).
|
|
928
|
-
const
|
|
1634
|
+
const applicableRequired = applicableRequiredKeys(declaredAt.applicable, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1635
|
+
const provisionalRequiredKeys = provisionalBranch
|
|
1636
|
+
? applicableRequiredKeys([provisionalBranch], applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)
|
|
1637
|
+
: new Set();
|
|
1638
|
+
for (const key of candidateProps.keys()) {
|
|
1639
|
+
const present = dataRecord !== undefined && Object.hasOwn(dataRecord, key);
|
|
1640
|
+
if ((present || applicableRequired.has(key)) &&
|
|
1641
|
+
applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0) {
|
|
1642
|
+
activeKeys.add(key);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (provisionalBranch) {
|
|
1646
|
+
for (const key of candidateProps.keys()) {
|
|
1647
|
+
if (provisionalRequiredKeys.has(key) &&
|
|
1648
|
+
(applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0 ||
|
|
1649
|
+
applicableAdditionalPropertySchemas([provisionalBranch], key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0)) {
|
|
1650
|
+
provisionalKeys.add(key);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
929
1654
|
const propKeys = [...candidateProps.keys()];
|
|
930
1655
|
const children = propKeys.length > 0
|
|
931
1656
|
? propKeys.map((key) => {
|
|
@@ -951,7 +1676,7 @@ lineage, path) {
|
|
|
951
1676
|
active: nodeActive,
|
|
952
1677
|
provisional: nodeProvisional ? true : undefined,
|
|
953
1678
|
defaultConflict,
|
|
954
|
-
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1679
|
+
annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
|
|
955
1680
|
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
956
1681
|
});
|
|
957
1682
|
for (const [index, key] of propKeys.entries()) {
|
|
@@ -961,29 +1686,89 @@ lineage, path) {
|
|
|
961
1686
|
// selected ancestor its own locally applicable properties are
|
|
962
1687
|
// provisional too, while a locally inactive one stays inactive.
|
|
963
1688
|
const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
|
|
964
|
-
const reducedChildNode = reducedNode
|
|
1689
|
+
const reducedChildNode = memberSchema(reducedNode, key);
|
|
965
1690
|
// Precedence matters as much as the selection does. Marking the right
|
|
966
1691
|
// branch provisional while taking its shape from `candidatePrototype`,
|
|
967
1692
|
// which is first-wins across branches, would render another branch's
|
|
968
1693
|
// widget and annotations under the selected branch's name, and would hand
|
|
969
1694
|
// ADR-003's pass another branch's `default`.
|
|
970
|
-
const provisionalChildNode = provisionalBranch
|
|
1695
|
+
const provisionalChildNode = memberSchema(provisionalBranch, key);
|
|
971
1696
|
const candidate = candidateProps.get(key);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1697
|
+
const childData = dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined;
|
|
1698
|
+
const sourceChildNode = reducedChildNode ?? candidate.direct ?? candidatePrototype(candidate);
|
|
1699
|
+
const sourceSchema = sourceChildNode.schema;
|
|
1700
|
+
let scopedChildNode;
|
|
1701
|
+
let childScope = scope;
|
|
1702
|
+
if (ctx.cache.dynamicReferenceProjection && childActive) {
|
|
1703
|
+
const nextScope = [...scope];
|
|
1704
|
+
if (typeof sourceSchema.$dynamicAnchor === 'string' ||
|
|
1705
|
+
typeof sourceSchema.$id === 'string' ||
|
|
1706
|
+
sourceSchema.$recursiveAnchor === true) {
|
|
1707
|
+
nextScope.push({ pointer: `#${childPointer}`, node: sourceChildNode });
|
|
1708
|
+
}
|
|
1709
|
+
const result = resolved.getNodeChild(key, dataRecord ?? {}, {
|
|
1710
|
+
pointer: `#${childPointer}`,
|
|
1711
|
+
path: nextScope,
|
|
1712
|
+
});
|
|
1713
|
+
if (!result.error && isSchemaNode(result.node)) {
|
|
1714
|
+
scopedChildNode = result.node;
|
|
1715
|
+
if (nextScope[nextScope.length - 1]?.node !== scopedChildNode) {
|
|
1716
|
+
nextScope.push({ pointer: `#${childPointer}`, node: scopedChildNode });
|
|
1717
|
+
}
|
|
1718
|
+
childScope = nextScope;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
let applicableChildNode = reducedChildNode;
|
|
1722
|
+
let scopedApplicableChildNode = scopedChildNode;
|
|
1723
|
+
const hasScopedReference = typeof sourceSchema.$dynamicRef === 'string' || typeof sourceSchema.$recursiveRef === 'string';
|
|
1724
|
+
for (const source of applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)) {
|
|
1725
|
+
const alreadyApplicable = applicableChildNode !== undefined && positionOf(applicableChildNode) === positionOf(source);
|
|
1726
|
+
const alreadyScoped = scopedApplicableChildNode !== undefined && positionOf(scopedApplicableChildNode) === positionOf(source);
|
|
1727
|
+
if (alreadyApplicable && (!scopedApplicableChildNode || alreadyScoped))
|
|
1728
|
+
continue;
|
|
1729
|
+
const reduced = reduceAdditionalPropertySchema(source, childData, scope);
|
|
1730
|
+
if (!alreadyApplicable) {
|
|
1731
|
+
applicableChildNode =
|
|
1732
|
+
applicableChildNode === undefined
|
|
1733
|
+
? reduced
|
|
1734
|
+
: mergeNode(applicableChildNode, reduced) ?? applicableChildNode;
|
|
1735
|
+
}
|
|
1736
|
+
if (scopedApplicableChildNode && !alreadyScoped) {
|
|
1737
|
+
scopedApplicableChildNode = mergeNode(scopedApplicableChildNode, reduced) ?? scopedApplicableChildNode;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
// Applicable declarations retain their authored scope here. The parent
|
|
1741
|
+
// reducer merges `properties` across `allOf`, which can make an
|
|
1742
|
+
// `additionalProperties` schema in a separate branch appear inapplicable.
|
|
1743
|
+
// Failing that, use the reducer's child, then the selected branch, then a
|
|
1744
|
+
// stable placeholder from the candidate set.
|
|
1745
|
+
const childNode = (hasScopedReference ? scopedApplicableChildNode : undefined) ??
|
|
1746
|
+
applicableChildNode ??
|
|
978
1747
|
composeChild(candidate.direct, provisionalChildNode) ??
|
|
979
1748
|
candidatePrototype(candidate);
|
|
980
|
-
|
|
1749
|
+
let positions = childPositions(declaredAt, key,
|
|
981
1750
|
// This node's own data, not the child's: the branches being entered
|
|
982
1751
|
// are this location's, so they are decided by the instance here. The
|
|
983
1752
|
// child's own conditional edges are entered by the child's walk, with
|
|
984
|
-
// the child's data.
|
|
985
|
-
|
|
986
|
-
|
|
1753
|
+
// the child's data. `applicableData` is the reducer's object stand-in
|
|
1754
|
+
// when this location has no object value yet.
|
|
1755
|
+
applicableData, childDeclaring(info, key), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1756
|
+
const dynamicChildDeclarations = dynamicApplicableNodes.flatMap((applicableNode) => {
|
|
1757
|
+
const dynamicChild = applicableNode.properties?.[key];
|
|
1758
|
+
return dynamicChild ? [dynamicChild] : [];
|
|
1759
|
+
});
|
|
1760
|
+
positions = {
|
|
1761
|
+
...positions,
|
|
1762
|
+
declaring: extendDeclaring(positions.declaring, dynamicChildDeclarations),
|
|
1763
|
+
};
|
|
1764
|
+
if (scopedChildNode && hasScopedReference && !positions.declaring.includes(scopedChildNode)) {
|
|
1765
|
+
positions = {
|
|
1766
|
+
...positions,
|
|
1767
|
+
applicable: [...positions.applicable, scopedChildNode],
|
|
1768
|
+
declaring: [...positions.declaring, scopedChildNode],
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const enter = () => walk(childNode, childPointer, childData, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index], childScope, hasScopedReference);
|
|
987
1772
|
// Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
|
|
988
1773
|
const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
|
|
989
1774
|
if (counted)
|
|
@@ -1003,7 +1788,7 @@ lineage, path) {
|
|
|
1003
1788
|
active: nodeActive,
|
|
1004
1789
|
provisional: nodeProvisional ? true : undefined,
|
|
1005
1790
|
defaultConflict,
|
|
1006
|
-
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1791
|
+
annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
|
|
1007
1792
|
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
1008
1793
|
// Deliberately not given the flag above. A conflict under `items` belongs
|
|
1009
1794
|
// to the element locations, and each of those reports its own; this field
|
|
@@ -1016,9 +1801,9 @@ lineage, path) {
|
|
|
1016
1801
|
: undefined,
|
|
1017
1802
|
});
|
|
1018
1803
|
if (type === 'array' && resolved.items && Array.isArray(data)) {
|
|
1019
|
-
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
|
|
1804
|
+
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1020
1805
|
data.forEach((item, index) => {
|
|
1021
|
-
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index]);
|
|
1806
|
+
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index], scope);
|
|
1022
1807
|
});
|
|
1023
1808
|
}
|
|
1024
1809
|
}
|
|
@@ -1032,13 +1817,17 @@ lineage, path) {
|
|
|
1032
1817
|
* contract), while array items are walked from `data` (an array's length is a data-time
|
|
1033
1818
|
* fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
|
|
1034
1819
|
*/
|
|
1035
|
-
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
1820
|
+
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS, options = {}) {
|
|
1036
1821
|
const nodes = new Map();
|
|
1037
1822
|
const diagnostics = [];
|
|
1038
1823
|
const ctx = {
|
|
1039
1824
|
cache,
|
|
1825
|
+
branchApplicability: cache.memoizeUndefinedBranchResults ? new WeakMap() : undefined,
|
|
1040
1826
|
limits,
|
|
1041
1827
|
boundaries: new Map(),
|
|
1828
|
+
boundaryTargets: new Map(),
|
|
1829
|
+
expandedBoundaryTokens: options.expandedBoundaryTokens ?? new Set(),
|
|
1830
|
+
boundaryGeneration: options.boundaryGeneration ?? 0,
|
|
1042
1831
|
removed: new Set(),
|
|
1043
1832
|
flagged: new Set(),
|
|
1044
1833
|
reserved: new Set(),
|
|
@@ -1046,7 +1835,7 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
|
1046
1835
|
objectsUsed: 0,
|
|
1047
1836
|
nodesUsed: 0,
|
|
1048
1837
|
};
|
|
1049
|
-
walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, []);
|
|
1838
|
+
walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, [], [{ pointer: '#', node: root }]);
|
|
1050
1839
|
for (const level of ctx.queue)
|
|
1051
1840
|
for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
|
|
1052
1841
|
enter();
|
|
@@ -1066,6 +1855,13 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
|
1066
1855
|
if (node) {
|
|
1067
1856
|
;
|
|
1068
1857
|
node.boundaries = ['recursion', 'budget'].filter((reason) => reasons.has(reason));
|
|
1858
|
+
const targets = ctx.boundaryTargets.get(pointer);
|
|
1859
|
+
if (targets) {
|
|
1860
|
+
;
|
|
1861
|
+
node.boundaryTargets = [
|
|
1862
|
+
...targets.values(),
|
|
1863
|
+
];
|
|
1864
|
+
}
|
|
1069
1865
|
}
|
|
1070
1866
|
}
|
|
1071
1867
|
return { nodes, diagnostics };
|