@texaryn/schema-json 0.7.0 → 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 +32 -0
- package/README.md +33 -1
- package/THIRD-PARTY-NOTICES.md +7 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +479 -24
- package/dist/adapter.js.map +1 -1
- package/dist/bare-maps.d.ts +3 -0
- package/dist/bare-maps.d.ts.map +1 -0
- package/dist/bare-maps.js +44 -0
- package/dist/bare-maps.js.map +1 -0
- 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/instance-pointer.d.ts +9 -0
- package/dist/instance-pointer.d.ts.map +1 -0
- package/dist/instance-pointer.js +115 -0
- package/dist/instance-pointer.js.map +1 -0
- 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-shape.d.ts +25 -0
- package/dist/projection-shape.d.ts.map +1 -0
- package/dist/projection-shape.js +74 -0
- package/dist/projection-shape.js.map +1 -0
- 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 +922 -243
- 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 +9 -4
package/dist/projection.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
|
+
import { inferProjectionShape, shapeDiagnostic } from './projection-shape.js';
|
|
3
4
|
import { POSITION } from './schema-graph.js';
|
|
4
5
|
const VALID_TYPES = new Set([
|
|
5
6
|
'string',
|
|
@@ -10,6 +11,18 @@ const VALID_TYPES = new Set([
|
|
|
10
11
|
'array',
|
|
11
12
|
'null',
|
|
12
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
|
+
]);
|
|
13
26
|
function toPointer(value) {
|
|
14
27
|
return value;
|
|
15
28
|
}
|
|
@@ -27,64 +40,6 @@ function resolveExplicitType(schema) {
|
|
|
27
40
|
}
|
|
28
41
|
return undefined;
|
|
29
42
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Keywords that apply to exactly one JSON type, grouped by that type.
|
|
32
|
-
*
|
|
33
|
-
* Two different jobs are done with these, and only one of them infers
|
|
34
|
-
* anything. Every family takes part in detecting a conflict, because a schema
|
|
35
|
-
* drawing keywords from two families implies no single shape. Only `object`
|
|
36
|
-
* and `array` are shapes a form can be given, so those are the only two ever
|
|
37
|
-
* inferred: `string` and `number` are here to be noticed, not chosen.
|
|
38
|
-
*
|
|
39
|
-
* Inferring a scalar would need a rule that is right rather than symmetrical,
|
|
40
|
-
* and there is not one. `minimum` cannot tell `number` from `integer`, and a
|
|
41
|
-
* wrong scalar guess selects the wrong widget, which is harder to notice than
|
|
42
|
-
* a field that never appeared at all.
|
|
43
|
-
*
|
|
44
|
-
* `format` is deliberately absent: it annotates a string's contents rather
|
|
45
|
-
* than describing structure, and schemas apply it to non-strings in practice.
|
|
46
|
-
*/
|
|
47
|
-
const KEYWORD_FAMILIES = {
|
|
48
|
-
object: [
|
|
49
|
-
'properties',
|
|
50
|
-
'patternProperties',
|
|
51
|
-
'additionalProperties',
|
|
52
|
-
'propertyNames',
|
|
53
|
-
'required',
|
|
54
|
-
'minProperties',
|
|
55
|
-
'maxProperties',
|
|
56
|
-
'dependentSchemas',
|
|
57
|
-
'dependentRequired',
|
|
58
|
-
// json-schema-library normalises draft-07 `dependencies` into the two
|
|
59
|
-
// above at parse time. Listed anyway, so the rule does not rely on that.
|
|
60
|
-
'dependencies',
|
|
61
|
-
'unevaluatedProperties',
|
|
62
|
-
],
|
|
63
|
-
array: [
|
|
64
|
-
'items',
|
|
65
|
-
'prefixItems',
|
|
66
|
-
'additionalItems',
|
|
67
|
-
'contains',
|
|
68
|
-
'minItems',
|
|
69
|
-
'maxItems',
|
|
70
|
-
'uniqueItems',
|
|
71
|
-
'minContains',
|
|
72
|
-
'maxContains',
|
|
73
|
-
'unevaluatedItems',
|
|
74
|
-
],
|
|
75
|
-
string: ['minLength', 'maxLength', 'pattern'],
|
|
76
|
-
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
|
|
77
|
-
};
|
|
78
|
-
/** Which families a schema draws keywords from, computed over all of them at once. */
|
|
79
|
-
function projectionTypeFamilies(schema) {
|
|
80
|
-
const families = new Set();
|
|
81
|
-
for (const [family, keywords] of Object.entries(KEYWORD_FAMILIES)) {
|
|
82
|
-
if (keywords.some((keyword) => schema[keyword] !== undefined)) {
|
|
83
|
-
families.add(family);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return families;
|
|
87
|
-
}
|
|
88
43
|
/**
|
|
89
44
|
* Whether any branch of this node's `oneOf`/`anyOf` could be given a shape at
|
|
90
45
|
* all, for some value.
|
|
@@ -106,17 +61,6 @@ function compositionHasRenderableAlternative(node) {
|
|
|
106
61
|
return inferProjectionShape(schema).kind === 'resolved';
|
|
107
62
|
});
|
|
108
63
|
}
|
|
109
|
-
function inferProjectionShape(schema) {
|
|
110
|
-
const families = projectionTypeFamilies(schema);
|
|
111
|
-
if (families.size > 1)
|
|
112
|
-
return { kind: 'ambiguous', families: [...families].sort() };
|
|
113
|
-
if (families.size === 1) {
|
|
114
|
-
const [family] = families;
|
|
115
|
-
if (family === 'object' || family === 'array')
|
|
116
|
-
return { kind: 'resolved', type: family };
|
|
117
|
-
}
|
|
118
|
-
return { kind: 'none' };
|
|
119
|
-
}
|
|
120
64
|
function extractConstraints(schema) {
|
|
121
65
|
const constraints = {};
|
|
122
66
|
if (typeof schema.minLength === 'number')
|
|
@@ -152,8 +96,8 @@ function extractAnnotations(schema,
|
|
|
152
96
|
* Set where two declarations apply to this location whatever the instance is
|
|
153
97
|
* and disagree. The merged schema still carries one of them, and which one is
|
|
154
98
|
* the merge's traversal order rather than an answer.
|
|
155
|
-
|
|
156
|
-
omitDefault = false) {
|
|
99
|
+
*/
|
|
100
|
+
omitDefault = false, defaultOverride) {
|
|
157
101
|
const annotations = {};
|
|
158
102
|
if (typeof schema.title === 'string')
|
|
159
103
|
annotations.title = schema.title;
|
|
@@ -167,8 +111,15 @@ omitDefault = false) {
|
|
|
167
111
|
annotations.deprecated = schema.deprecated;
|
|
168
112
|
if (Array.isArray(schema.examples))
|
|
169
113
|
annotations.examples = schema.examples;
|
|
170
|
-
if (!omitDefault
|
|
171
|
-
|
|
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
|
+
}
|
|
172
123
|
return annotations;
|
|
173
124
|
}
|
|
174
125
|
function extractEnumValues(schema) {
|
|
@@ -177,30 +128,339 @@ function extractEnumValues(schema) {
|
|
|
177
128
|
return schema.enum.map((value) => ({ value }));
|
|
178
129
|
}
|
|
179
130
|
function dereferenceChecked(node) {
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
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) => {
|
|
183
137
|
const position = positionOf(current);
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
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];
|
|
191
216
|
}
|
|
192
|
-
current = next;
|
|
193
|
-
if (!onlyPoints(current))
|
|
194
|
-
break;
|
|
195
217
|
}
|
|
196
|
-
|
|
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 };
|
|
197
271
|
}
|
|
198
272
|
/** Follows a $ref to the node it points at; returns the node unchanged otherwise. */
|
|
199
273
|
function dereference(node) {
|
|
200
274
|
return dereferenceChecked(node).node;
|
|
201
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
|
+
}
|
|
202
462
|
// A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
|
|
203
|
-
function resolveSelectedBranch(original, reduced, data) {
|
|
463
|
+
function resolveSelectedBranch(original, reduced, data, scope = [], branchApplicability) {
|
|
204
464
|
const reducedSchema = reduced.schema;
|
|
205
465
|
if (reducedSchema && typeof reducedSchema === 'object') {
|
|
206
466
|
if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
|
|
@@ -209,26 +469,41 @@ function resolveSelectedBranch(original, reduced, data) {
|
|
|
209
469
|
}
|
|
210
470
|
const selected = [];
|
|
211
471
|
if (original.oneOf) {
|
|
212
|
-
const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
|
|
472
|
+
const matching = original.oneOf.filter((branch) => branchApplies(branch, data, scope, branchApplicability));
|
|
213
473
|
if (matching.length === 1)
|
|
214
474
|
selected.push(matching[0]);
|
|
215
475
|
}
|
|
216
476
|
for (const branch of original.anyOf ?? []) {
|
|
217
|
-
if (branchApplies(branch, data))
|
|
477
|
+
if (branchApplies(branch, data, scope, branchApplicability))
|
|
218
478
|
selected.push(branch);
|
|
219
479
|
}
|
|
220
|
-
if (!selected.some((branch) =>
|
|
480
|
+
if (!selected.some((branch) => referenceOf(branch) !== undefined))
|
|
221
481
|
return reduced;
|
|
222
482
|
let merged = reduced;
|
|
223
483
|
for (const branch of selected) {
|
|
224
484
|
const target = dereference(branch);
|
|
225
485
|
if (!isSchemaNode(target))
|
|
226
486
|
continue;
|
|
227
|
-
const targetReduced = target.reduceNode(data).node ?? target;
|
|
487
|
+
const targetReduced = target.reduceNode(data, { path: [...scope] }).node ?? target;
|
|
228
488
|
merged = mergeNode(merged, targetReduced) ?? merged;
|
|
229
489
|
}
|
|
230
490
|
return merged;
|
|
231
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
|
+
}
|
|
232
507
|
export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
|
|
233
508
|
function walkOrder(a, b) {
|
|
234
509
|
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
@@ -236,10 +511,19 @@ function walkOrder(a, b) {
|
|
|
236
511
|
return a[i] - b[i];
|
|
237
512
|
return a.length - b.length;
|
|
238
513
|
}
|
|
239
|
-
function
|
|
514
|
+
function boundaryToken(pointer, schemaKey, generation) {
|
|
515
|
+
return JSON.stringify([pointer, schemaKey, generation]);
|
|
516
|
+
}
|
|
517
|
+
function addBoundary(ctx, pointer, reason, targetPointer, schemaKey) {
|
|
240
518
|
const reasons = ctx.boundaries.get(pointer) ?? new Set();
|
|
241
519
|
reasons.add(reason);
|
|
242
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);
|
|
243
527
|
}
|
|
244
528
|
// A leaf needs no reduction, so it rides with its parent's budget unit.
|
|
245
529
|
function isLeafSchema(schema) {
|
|
@@ -252,17 +536,14 @@ function isLeafSchema(schema) {
|
|
|
252
536
|
return ('enum' in record || 'const' in record) && !('properties' in record);
|
|
253
537
|
}
|
|
254
538
|
/**
|
|
255
|
-
* The position
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* adapters report sources in this form so a diagnostic means the same thing
|
|
261
|
-
* 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.
|
|
262
544
|
*/
|
|
263
545
|
function documentPointer(node) {
|
|
264
|
-
|
|
265
|
-
return typeof location === 'string' ? location.replace(/^#/, '') : '';
|
|
546
|
+
return positionOf(node).replace(/^#/, '');
|
|
266
547
|
}
|
|
267
548
|
/**
|
|
268
549
|
* Visits every schema position that applies to one location whatever the
|
|
@@ -285,16 +566,13 @@ function documentPointer(node) {
|
|
|
285
566
|
* several places at once: `properties/x` on the node's own schema and on each
|
|
286
567
|
* of its `allOf` branches are all declarations of the same location.
|
|
287
568
|
*/
|
|
288
|
-
function eachUnconditional(roots, visitor) {
|
|
569
|
+
function eachUnconditional(roots, visitor, referenceVisitor, cache) {
|
|
289
570
|
const visited = new Set();
|
|
290
571
|
const visit = (current) => {
|
|
572
|
+
if (referenceVisitor && cache)
|
|
573
|
+
visitReferenceDefaultSites(current, cache, referenceVisitor);
|
|
291
574
|
const resolved = dereference(current);
|
|
292
|
-
|
|
293
|
-
// `collectCandidateProperties`: `resolveRef()` returns a fresh node every
|
|
294
|
-
// call, so identity alone never closes a cycle, and an inline branch has no
|
|
295
|
-
// location of its own to key on.
|
|
296
|
-
const location = resolved.schemaLocation;
|
|
297
|
-
const key = typeof location === 'string' ? location : resolved;
|
|
575
|
+
const key = traversalKey(resolved);
|
|
298
576
|
if (visited.has(key))
|
|
299
577
|
return;
|
|
300
578
|
visited.add(key);
|
|
@@ -317,9 +595,15 @@ function eachUnconditional(roots, visitor) {
|
|
|
317
595
|
* own descent already survives the shapes that throw (#119, #121), and a
|
|
318
596
|
* conflict is the wrong place to surface one.
|
|
319
597
|
*/
|
|
320
|
-
function branchApplies(branch, data) {
|
|
598
|
+
function branchApplies(branch, data, scope = [], branchApplicability) {
|
|
599
|
+
if (data === undefined && branchApplicability?.has(branch)) {
|
|
600
|
+
return branchApplicability.get(branch);
|
|
601
|
+
}
|
|
321
602
|
try {
|
|
322
|
-
|
|
603
|
+
const applies = branch.validate(data, '#', [...scope]).valid === true;
|
|
604
|
+
if (data === undefined)
|
|
605
|
+
branchApplicability?.set(branch, applies);
|
|
606
|
+
return applies;
|
|
323
607
|
}
|
|
324
608
|
catch {
|
|
325
609
|
return false;
|
|
@@ -338,18 +622,23 @@ function branchApplies(branch, data) {
|
|
|
338
622
|
* rule the projection uses to expose that branch, so the two cannot disagree
|
|
339
623
|
* about which branch it is.
|
|
340
624
|
*/
|
|
341
|
-
function eachApplicable(roots, data, visitor) {
|
|
625
|
+
function eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope = [], dynamicReferenceProjection = false) {
|
|
342
626
|
const visited = new Set();
|
|
343
|
-
const visit = (current) => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
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);
|
|
347
636
|
if (visited.has(key))
|
|
348
637
|
return;
|
|
349
638
|
visited.add(key);
|
|
350
639
|
visitor(resolved);
|
|
351
640
|
for (const branch of resolved.allOf ?? [])
|
|
352
|
-
visit(branch);
|
|
641
|
+
visit(branch, currentScope);
|
|
353
642
|
// `oneOf` contributes the one branch it selects, which is the evaluator's
|
|
354
643
|
// rule and not "every branch the data matches": an empty object matches
|
|
355
644
|
// every permissive branch, and treating all of them as applicable would
|
|
@@ -358,82 +647,123 @@ function eachApplicable(roots, data, visitor) {
|
|
|
358
647
|
// branch stands in, which is what the projection exposes.
|
|
359
648
|
const oneOf = resolved.oneOf ?? [];
|
|
360
649
|
if (oneOf.length > 0) {
|
|
361
|
-
const matching = oneOf.filter((branch) => branchApplies(branch, data));
|
|
650
|
+
const matching = oneOf.filter((branch) => branchApplies(branch, data, currentScope, branchApplicability));
|
|
362
651
|
const selected = matching.length === 1
|
|
363
652
|
? matching[0]
|
|
364
653
|
: selectProvisionalBranch(resolved, typeof data === 'object' && data !== null
|
|
365
654
|
? data
|
|
366
655
|
: undefined);
|
|
367
656
|
if (selected)
|
|
368
|
-
visit(selected);
|
|
657
|
+
visit(selected, currentScope);
|
|
369
658
|
}
|
|
370
659
|
// `anyOf` is the opposite case and needs no such rule: several of its
|
|
371
660
|
// branches applying at once is ordinary, and each that validates
|
|
372
661
|
// contributes its annotations, so each competes.
|
|
373
662
|
for (const branch of resolved.anyOf ?? []) {
|
|
374
|
-
if (branchApplies(branch, data))
|
|
375
|
-
visit(branch);
|
|
663
|
+
if (branchApplies(branch, data, currentScope, branchApplicability))
|
|
664
|
+
visit(branch, currentScope);
|
|
376
665
|
}
|
|
377
666
|
if (resolved.if) {
|
|
378
|
-
const taken = branchApplies(resolved.if, data) ? resolved.then : resolved.else;
|
|
667
|
+
const taken = branchApplies(resolved.if, data, currentScope, branchApplicability) ? resolved.then : resolved.else;
|
|
379
668
|
if (taken)
|
|
380
|
-
visit(taken);
|
|
669
|
+
visit(taken, currentScope);
|
|
381
670
|
}
|
|
382
671
|
const dependent = resolved.dependentSchemas;
|
|
383
672
|
if (dependent && typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
384
673
|
for (const [dependency, subschema] of Object.entries(dependent)) {
|
|
385
674
|
if (Object.prototype.hasOwnProperty.call(data, dependency))
|
|
386
|
-
visit(subschema);
|
|
675
|
+
visit(subschema, currentScope);
|
|
387
676
|
}
|
|
388
677
|
}
|
|
389
678
|
};
|
|
390
679
|
for (const root of roots)
|
|
391
|
-
visit(root);
|
|
680
|
+
visit(root, scope);
|
|
392
681
|
}
|
|
393
|
-
function
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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;
|
|
399
700
|
}
|
|
400
|
-
|
|
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);
|
|
401
731
|
return declarations;
|
|
402
732
|
}
|
|
403
|
-
function collectUnconditionalDefaults(roots) {
|
|
404
|
-
return declarationsFrom((visitor) => eachUnconditional(roots, visitor));
|
|
733
|
+
function collectUnconditionalDefaults(roots, cache) {
|
|
734
|
+
return declarationsFrom((visitor, referenceVisitor) => eachUnconditional(roots, visitor, referenceVisitor, cache), cache);
|
|
405
735
|
}
|
|
406
|
-
function collectApplicableDefaults(roots, data) {
|
|
407
|
-
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);
|
|
408
738
|
}
|
|
409
739
|
/** The positions that declare one property of `roots`, by both reachability rules. */
|
|
410
|
-
function childPositions(roots, key, data, declaring) {
|
|
740
|
+
function childPositions(roots, key, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
411
741
|
const unconditional = [];
|
|
412
742
|
eachUnconditional(roots.unconditional, (node) => {
|
|
413
|
-
const child = node
|
|
743
|
+
const child = memberSchema(node, key);
|
|
414
744
|
if (child)
|
|
415
745
|
unconditional.push(child);
|
|
416
746
|
});
|
|
417
747
|
const applicable = [];
|
|
418
|
-
eachApplicable(roots.applicable, data, (node) => {
|
|
419
|
-
const child = node
|
|
748
|
+
eachApplicable(roots.applicable, data, branchApplicability, (node) => {
|
|
749
|
+
const child = memberSchema(node, key);
|
|
420
750
|
if (child)
|
|
421
751
|
applicable.push(child);
|
|
422
|
-
});
|
|
752
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
423
753
|
return { unconditional, applicable, declaring };
|
|
424
754
|
}
|
|
425
755
|
/** The positions that declare the elements of `roots`, by both reachability rules. */
|
|
426
|
-
function itemPositions(roots, data, declaring) {
|
|
756
|
+
function itemPositions(roots, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
|
|
427
757
|
const unconditional = [];
|
|
428
758
|
eachUnconditional(roots.unconditional, (node) => {
|
|
429
759
|
if (node.items)
|
|
430
760
|
unconditional.push(node.items);
|
|
431
761
|
});
|
|
432
762
|
const applicable = [];
|
|
433
|
-
eachApplicable(roots.applicable, data, (node) => {
|
|
763
|
+
eachApplicable(roots.applicable, data, branchApplicability, (node) => {
|
|
434
764
|
if (node.items)
|
|
435
765
|
applicable.push(node.items);
|
|
436
|
-
});
|
|
766
|
+
}, undefined, undefined, scope, dynamicReferenceProjection);
|
|
437
767
|
return { unconditional, applicable, declaring };
|
|
438
768
|
}
|
|
439
769
|
/**
|
|
@@ -448,16 +778,75 @@ function disagreeingDefaults(declarations) {
|
|
|
448
778
|
const [first, ...rest] = declarations;
|
|
449
779
|
return rest.every((other) => deepEqual(other.value, first.value)) ? undefined : declarations;
|
|
450
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();
|
|
451
842
|
/**
|
|
452
|
-
*
|
|
453
|
-
* across every branch that might apply to it.
|
|
843
|
+
* Property keys that might need to appear at this instance location.
|
|
454
844
|
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* which
|
|
459
|
-
*
|
|
460
|
-
* 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`.
|
|
461
850
|
*
|
|
462
851
|
* **Recursion is through applicators only, and only those acting on this same
|
|
463
852
|
* instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
|
|
@@ -477,20 +866,20 @@ function disagreeingDefaults(declarations) {
|
|
|
477
866
|
* - **`items` and `prefixItems`**, for the same reason: an array's items are
|
|
478
867
|
* their own locations.
|
|
479
868
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* and a recursive `$ref` would not terminate. `schemaLocation` is stable, is
|
|
485
|
-
* distinct for each inline branch position, and repeats when a reference closes
|
|
486
|
-
* a cycle, which is precisely the three properties needed. The set is per call,
|
|
487
|
-
* because deduplicating a schema location is only sound while collecting names
|
|
488
|
-
* for one instance location; the same schema legitimately recurs at a deeper
|
|
489
|
-
* 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.
|
|
490
873
|
*/
|
|
491
|
-
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
|
+
}
|
|
492
880
|
const candidates = new Map();
|
|
493
881
|
const visited = new Set();
|
|
882
|
+
const scopes = [];
|
|
494
883
|
const record = (key, propNode, direct) => {
|
|
495
884
|
const entry = candidates.get(key) ?? { alternatives: [] };
|
|
496
885
|
if (direct)
|
|
@@ -501,26 +890,11 @@ function collectCandidateProperties(node) {
|
|
|
501
890
|
};
|
|
502
891
|
const visit = (current, own) => {
|
|
503
892
|
const resolved = dereference(current);
|
|
504
|
-
|
|
505
|
-
// where there is not, which together cover every way the traversal can
|
|
506
|
-
// come back to where it started.
|
|
507
|
-
//
|
|
508
|
-
// Neither alone is sufficient and each covers what the other cannot. A
|
|
509
|
-
// cycle requires a `$ref`, because an inline schema cannot nest into
|
|
510
|
-
// itself, and `resolveRef()` returns a fresh `SchemaNode` on every call
|
|
511
|
-
// whose raw `schema` object is fresh too, so identity never matches across
|
|
512
|
-
// one; those nodes do carry a location. An inline branch is never resolved
|
|
513
|
-
// through a ref, so its identity is stable within one traversal.
|
|
514
|
-
//
|
|
515
|
-
// This deliberately replaced a depth cap. A cap terminates, but it does so
|
|
516
|
-
// by dropping candidates a valid schema declared, which is the silent
|
|
517
|
-
// disappearance the whole projection-diagnostic design exists to prevent:
|
|
518
|
-
// a field nested under 70 applicators is still a field.
|
|
519
|
-
const location = resolved.schemaLocation;
|
|
520
|
-
const key = typeof location === 'string' ? location : resolved;
|
|
893
|
+
const key = traversalKey(resolved);
|
|
521
894
|
if (visited.has(key))
|
|
522
895
|
return;
|
|
523
896
|
visited.add(key);
|
|
897
|
+
scopes.push({ node: resolved, own });
|
|
524
898
|
for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
|
|
525
899
|
record(key, propNode, own);
|
|
526
900
|
}
|
|
@@ -528,7 +902,9 @@ function collectCandidateProperties(node) {
|
|
|
528
902
|
if (branch)
|
|
529
903
|
visit(branch, false);
|
|
530
904
|
}
|
|
531
|
-
for (const
|
|
905
|
+
for (const branch of resolved.allOf ?? [])
|
|
906
|
+
visit(branch, own);
|
|
907
|
+
for (const branches of [resolved.anyOf, resolved.oneOf]) {
|
|
532
908
|
for (const branch of branches ?? [])
|
|
533
909
|
visit(branch, false);
|
|
534
910
|
}
|
|
@@ -539,8 +915,32 @@ function collectCandidateProperties(node) {
|
|
|
539
915
|
}
|
|
540
916
|
};
|
|
541
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
|
+
}
|
|
542
935
|
return candidates;
|
|
543
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
|
+
}
|
|
544
944
|
/**
|
|
545
945
|
* The schema to project for a candidate when no branch is active.
|
|
546
946
|
*
|
|
@@ -714,6 +1114,96 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
|
|
|
714
1114
|
}
|
|
715
1115
|
return required;
|
|
716
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
|
+
}
|
|
717
1207
|
// An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
|
|
718
1208
|
const budgeted = (info) => info.cyclic || info.key === '';
|
|
719
1209
|
/**
|
|
@@ -743,9 +1233,15 @@ declaredAt, ctx,
|
|
|
743
1233
|
/** Entered as an object member, which is the only way to be past the data. */
|
|
744
1234
|
member,
|
|
745
1235
|
/** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
|
|
746
|
-
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
|
+
}
|
|
747
1243
|
const info = locationInfo(declaredAt.declaring, ctx.cache);
|
|
748
|
-
const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
|
|
1244
|
+
const { node: original, cycle: referenceCycle, unresolved: unresolvedReference } = dereferenceChecked(node);
|
|
749
1245
|
if (info.cycle || referenceCycle) {
|
|
750
1246
|
diagnostics.push({
|
|
751
1247
|
pointer: toPointer(pointer),
|
|
@@ -756,10 +1252,13 @@ lineage, path) {
|
|
|
756
1252
|
return;
|
|
757
1253
|
}
|
|
758
1254
|
const pastData = member && (data === undefined || data === null);
|
|
759
|
-
const
|
|
1255
|
+
const token = boundaryToken(pointer, info.key, ctx.boundaryGeneration);
|
|
1256
|
+
const recursive = pastData && !unresolvedReference && ((lineage?.recursive ?? false) || budgeted(info));
|
|
760
1257
|
for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
|
|
761
|
-
if (info.key !== '' &&
|
|
762
|
-
|
|
1258
|
+
if (info.key !== '' &&
|
|
1259
|
+
ancestor.key === info.key &&
|
|
1260
|
+
!ctx.expandedBoundaryTokens.has(token)) {
|
|
1261
|
+
addBoundary(ctx, ancestor.pointer, 'recursion', pointer, info.key);
|
|
763
1262
|
ctx.removed.add(pointer);
|
|
764
1263
|
return;
|
|
765
1264
|
}
|
|
@@ -770,21 +1269,29 @@ lineage, path) {
|
|
|
770
1269
|
let admit;
|
|
771
1270
|
if (isLeafSchema(originalSchema)) {
|
|
772
1271
|
const covered = ctx.reserved.has(parent);
|
|
773
|
-
admit = covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
1272
|
+
admit = ctx.expandedBoundaryTokens.has(token) || covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
774
1273
|
if (admit && !covered)
|
|
775
1274
|
ctx.nodesUsed += 1;
|
|
776
1275
|
}
|
|
777
1276
|
else {
|
|
778
|
-
const
|
|
779
|
-
|
|
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);
|
|
780
1286
|
if (admit) {
|
|
781
1287
|
ctx.objectsUsed += 1;
|
|
782
|
-
ctx.nodesUsed += 1 + leaves;
|
|
783
|
-
|
|
1288
|
+
ctx.nodesUsed += explicitlyExpanded ? 1 : 1 + leaves;
|
|
1289
|
+
if (!explicitlyExpanded)
|
|
1290
|
+
ctx.reserved.add(pointer);
|
|
784
1291
|
}
|
|
785
1292
|
}
|
|
786
1293
|
if (!admit) {
|
|
787
|
-
addBoundary(ctx, parent, 'budget');
|
|
1294
|
+
addBoundary(ctx, parent, 'budget', pointer, info.key);
|
|
788
1295
|
ctx.removed.add(pointer);
|
|
789
1296
|
return;
|
|
790
1297
|
}
|
|
@@ -795,6 +1302,36 @@ lineage, path) {
|
|
|
795
1302
|
let resolved = original;
|
|
796
1303
|
let schema = originalSchema;
|
|
797
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
|
+
}
|
|
798
1335
|
// Whether this node's own active branch could be determined. Stays true for every
|
|
799
1336
|
// node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
|
|
800
1337
|
// (see below), where the node's own presence in the projection is the thing in
|
|
@@ -818,9 +1355,9 @@ lineage, path) {
|
|
|
818
1355
|
let wrapperIdentified = false;
|
|
819
1356
|
if (!type && (original.oneOf || original.anyOf)) {
|
|
820
1357
|
composedAgainstData = true;
|
|
821
|
-
const { node: branchNode } = original.reduceNode(data);
|
|
1358
|
+
const { node: branchNode } = original.reduceNode(data, { path: [...scope] });
|
|
822
1359
|
if (branchNode) {
|
|
823
|
-
resolved = resolveSelectedBranch(original, branchNode, data);
|
|
1360
|
+
resolved = resolveSelectedBranch(original, branchNode, data, scope, ctx.branchApplicability);
|
|
824
1361
|
schema = resolved.schema;
|
|
825
1362
|
type = resolveExplicitType(schema);
|
|
826
1363
|
}
|
|
@@ -899,26 +1436,16 @@ lineage, path) {
|
|
|
899
1436
|
// without a shape, so the caller is told which pointer was skipped and
|
|
900
1437
|
// why, instead of finding out from a form that never collected the
|
|
901
1438
|
// value.
|
|
902
|
-
diagnostics.push(shape.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
code: 'unresolved-projection-shape',
|
|
913
|
-
message: `No explicit "type", and no keyword that implies one, so there is no ` +
|
|
914
|
-
`shape to render. Declare "type" on this schema.` +
|
|
915
|
-
// Said only where it applies, because inferring `string` from
|
|
916
|
-
// an enum is the tempting wrong rule and the reason deserves
|
|
917
|
-
// to travel with the case rather than every message.
|
|
918
|
-
(schema.enum !== undefined
|
|
919
|
-
? ` An "enum" alone does not imply a type, because its members may be of different types.`
|
|
920
|
-
: ''),
|
|
921
|
-
});
|
|
1439
|
+
diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
|
|
1440
|
+
}
|
|
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;
|
|
922
1449
|
}
|
|
923
1450
|
}
|
|
924
1451
|
if (!type)
|
|
@@ -930,7 +1457,7 @@ lineage, path) {
|
|
|
930
1457
|
// pointer with no node has nothing to omit an annotation from, and saying
|
|
931
1458
|
// that its `default` is undecidable on top of saying it cannot be drawn at
|
|
932
1459
|
// all would be the same schema reported twice.
|
|
933
|
-
const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional));
|
|
1460
|
+
const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional, ctx.cache));
|
|
934
1461
|
if (ambiguousDefault) {
|
|
935
1462
|
diagnostics.push({
|
|
936
1463
|
pointer: toPointer(pointer),
|
|
@@ -945,13 +1472,18 @@ lineage, path) {
|
|
|
945
1472
|
// selects. A superset of the diagnostic's: every unconditional disagreement
|
|
946
1473
|
// is also an applicable one, so the node carries both kinds and a consumer
|
|
947
1474
|
// reads one place.
|
|
948
|
-
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);
|
|
949
1477
|
const applicableConflict = disagreeingDefaults(applicableDeclarations);
|
|
950
1478
|
const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
|
|
951
1479
|
const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
|
|
952
|
-
const defaultSources = !conflictedDefault &&
|
|
1480
|
+
const defaultSources = !conflictedDefault && applicableDeclarations.length > 0 && info.cyclic
|
|
953
1481
|
? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
|
|
954
1482
|
: undefined;
|
|
1483
|
+
const defaultOverride = {
|
|
1484
|
+
present: applicableDeclarations.length > 0,
|
|
1485
|
+
value: applicableDeclarations[0]?.value,
|
|
1486
|
+
};
|
|
955
1487
|
const nodeActive = active && branchResolved;
|
|
956
1488
|
// A node the schema applies is never also provisional; the two report
|
|
957
1489
|
// different facts and only the second is a choice this projection made. The
|
|
@@ -976,19 +1508,73 @@ lineage, path) {
|
|
|
976
1508
|
// wrapper case), it has already been reduced against the real `data` at this
|
|
977
1509
|
// pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
|
|
978
1510
|
// for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
|
|
979
|
-
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
|
+
}
|
|
980
1522
|
if (reducedNode &&
|
|
981
1523
|
resolved === original &&
|
|
982
1524
|
!resolveExplicitType(originalSchema) &&
|
|
983
1525
|
(original.oneOf || original.anyOf)) {
|
|
984
|
-
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
|
|
1526
|
+
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
|
|
985
1527
|
}
|
|
986
1528
|
const reducedSchema = reducedNode?.schema;
|
|
987
1529
|
const reducedProperties = reducedSchema?.properties ??
|
|
988
1530
|
schema.properties ??
|
|
989
1531
|
{};
|
|
990
|
-
|
|
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
|
+
}
|
|
991
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
|
+
}
|
|
992
1578
|
// Two conditions, and they are not the same kind of condition.
|
|
993
1579
|
//
|
|
994
1580
|
// Exposure is load-bearing. Selection is local, but whether this node is
|
|
@@ -1008,11 +1594,32 @@ lineage, path) {
|
|
|
1008
1594
|
: undefined;
|
|
1009
1595
|
const provisionalSchema = provisionalBranch?.schema;
|
|
1010
1596
|
const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
|
|
1597
|
+
for (const key of additionalPropertyKeys(provisionalBranch, dataRecord))
|
|
1598
|
+
provisionalKeys.add(key);
|
|
1011
1599
|
const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
|
|
1012
1600
|
// Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
|
|
1013
1601
|
// branch's properties are represented (the matching branch alone, via `resolved`,
|
|
1014
1602
|
// would only expose its own properties).
|
|
1015
|
-
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
|
+
}
|
|
1016
1623
|
const propKeys = [...candidateProps.keys()];
|
|
1017
1624
|
const children = propKeys.length > 0
|
|
1018
1625
|
? propKeys.map((key) => {
|
|
@@ -1038,7 +1645,7 @@ lineage, path) {
|
|
|
1038
1645
|
active: nodeActive,
|
|
1039
1646
|
provisional: nodeProvisional ? true : undefined,
|
|
1040
1647
|
defaultConflict,
|
|
1041
|
-
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1648
|
+
annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
|
|
1042
1649
|
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
1043
1650
|
});
|
|
1044
1651
|
for (const [index, key] of propKeys.entries()) {
|
|
@@ -1048,29 +1655,89 @@ lineage, path) {
|
|
|
1048
1655
|
// selected ancestor its own locally applicable properties are
|
|
1049
1656
|
// provisional too, while a locally inactive one stays inactive.
|
|
1050
1657
|
const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
|
|
1051
|
-
const reducedChildNode = reducedNode
|
|
1658
|
+
const reducedChildNode = memberSchema(reducedNode, key);
|
|
1052
1659
|
// Precedence matters as much as the selection does. Marking the right
|
|
1053
1660
|
// branch provisional while taking its shape from `candidatePrototype`,
|
|
1054
1661
|
// which is first-wins across branches, would render another branch's
|
|
1055
1662
|
// widget and annotations under the selected branch's name, and would hand
|
|
1056
1663
|
// ADR-003's pass another branch's `default`.
|
|
1057
|
-
const provisionalChildNode = provisionalBranch
|
|
1664
|
+
const provisionalChildNode = memberSchema(provisionalBranch, key);
|
|
1058
1665
|
const candidate = candidateProps.get(key);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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 ??
|
|
1065
1716
|
composeChild(candidate.direct, provisionalChildNode) ??
|
|
1066
1717
|
candidatePrototype(candidate);
|
|
1067
|
-
|
|
1718
|
+
let positions = childPositions(declaredAt, key,
|
|
1068
1719
|
// This node's own data, not the child's: the branches being entered
|
|
1069
1720
|
// are this location's, so they are decided by the instance here. The
|
|
1070
1721
|
// child's own conditional edges are entered by the child's walk, with
|
|
1071
|
-
// the child's data.
|
|
1072
|
-
|
|
1073
|
-
|
|
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);
|
|
1074
1741
|
// Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
|
|
1075
1742
|
const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
|
|
1076
1743
|
if (counted)
|
|
@@ -1080,6 +1747,7 @@ lineage, path) {
|
|
|
1080
1747
|
}
|
|
1081
1748
|
return;
|
|
1082
1749
|
}
|
|
1750
|
+
const itemSchema = type === 'array' && resolved.items ? dereference(resolved.items).schema : undefined;
|
|
1083
1751
|
nodes.set(toPointer(pointer), {
|
|
1084
1752
|
type,
|
|
1085
1753
|
format: typeof schema.format === 'string' ? schema.format : undefined,
|
|
@@ -1089,7 +1757,7 @@ lineage, path) {
|
|
|
1089
1757
|
active: nodeActive,
|
|
1090
1758
|
provisional: nodeProvisional ? true : undefined,
|
|
1091
1759
|
defaultConflict,
|
|
1092
|
-
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1760
|
+
annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
|
|
1093
1761
|
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
1094
1762
|
// Deliberately not given the flag above. A conflict under `items` belongs
|
|
1095
1763
|
// to the element locations, and each of those reports its own; this field
|
|
@@ -1097,14 +1765,14 @@ lineage, path) {
|
|
|
1097
1765
|
// omitting its `default` would be an omission no pointer could report. It
|
|
1098
1766
|
// is also the only annotation set nothing reads a `default` from, and the
|
|
1099
1767
|
// only one the other adapter does not produce at all.
|
|
1100
|
-
itemAnnotations:
|
|
1101
|
-
? extractAnnotations(
|
|
1768
|
+
itemAnnotations: typeof itemSchema === 'object' && itemSchema !== null
|
|
1769
|
+
? extractAnnotations(itemSchema)
|
|
1102
1770
|
: undefined,
|
|
1103
1771
|
});
|
|
1104
1772
|
if (type === 'array' && resolved.items && Array.isArray(data)) {
|
|
1105
|
-
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
|
|
1773
|
+
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
|
|
1106
1774
|
data.forEach((item, index) => {
|
|
1107
|
-
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);
|
|
1108
1776
|
});
|
|
1109
1777
|
}
|
|
1110
1778
|
}
|
|
@@ -1118,13 +1786,17 @@ lineage, path) {
|
|
|
1118
1786
|
* contract), while array items are walked from `data` (an array's length is a data-time
|
|
1119
1787
|
* fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
|
|
1120
1788
|
*/
|
|
1121
|
-
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
1789
|
+
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS, options = {}) {
|
|
1122
1790
|
const nodes = new Map();
|
|
1123
1791
|
const diagnostics = [];
|
|
1124
1792
|
const ctx = {
|
|
1125
1793
|
cache,
|
|
1794
|
+
branchApplicability: cache.memoizeUndefinedBranchResults ? new WeakMap() : undefined,
|
|
1126
1795
|
limits,
|
|
1127
1796
|
boundaries: new Map(),
|
|
1797
|
+
boundaryTargets: new Map(),
|
|
1798
|
+
expandedBoundaryTokens: options.expandedBoundaryTokens ?? new Set(),
|
|
1799
|
+
boundaryGeneration: options.boundaryGeneration ?? 0,
|
|
1128
1800
|
removed: new Set(),
|
|
1129
1801
|
flagged: new Set(),
|
|
1130
1802
|
reserved: new Set(),
|
|
@@ -1132,7 +1804,7 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
|
1132
1804
|
objectsUsed: 0,
|
|
1133
1805
|
nodesUsed: 0,
|
|
1134
1806
|
};
|
|
1135
|
-
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 }]);
|
|
1136
1808
|
for (const level of ctx.queue)
|
|
1137
1809
|
for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
|
|
1138
1810
|
enter();
|
|
@@ -1152,6 +1824,13 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
|
1152
1824
|
if (node) {
|
|
1153
1825
|
;
|
|
1154
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
|
+
}
|
|
1155
1834
|
}
|
|
1156
1835
|
}
|
|
1157
1836
|
return { nodes, diagnostics };
|