@texaryn/schema-json 0.3.0 → 0.4.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 +106 -0
- package/THIRD-PARTY-NOTICES.md +43 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +8 -0
- package/dist/adapter.js.map +1 -1
- package/dist/metaschemas/2019-09.d.ts +4 -0
- package/dist/metaschemas/2019-09.d.ts.map +1 -0
- package/dist/metaschemas/2019-09.js +18 -0
- package/dist/metaschemas/2019-09.js.map +1 -0
- package/dist/metaschemas/2020-12.d.ts +4 -0
- package/dist/metaschemas/2020-12.d.ts.map +1 -0
- package/dist/metaschemas/2020-12.js +20 -0
- package/dist/metaschemas/2020-12.js.map +1 -0
- package/dist/metaschemas/draft-07.d.ts +4 -0
- package/dist/metaschemas/draft-07.d.ts.map +1 -0
- package/dist/metaschemas/draft-07.js +6 -0
- package/dist/metaschemas/draft-07.js.map +1 -0
- package/dist/metaschemas/index.d.ts +22 -0
- package/dist/metaschemas/index.d.ts.map +1 -0
- package/dist/metaschemas/index.js +102 -0
- package/dist/metaschemas/index.js.map +1 -0
- package/dist/projection.d.ts +1 -1
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +496 -47
- package/dist/projection.js.map +1 -1
- package/package.json +4 -3
package/dist/projection.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeNode } from 'json-schema-library';
|
|
1
2
|
const VALID_TYPES = new Set([
|
|
2
3
|
'string',
|
|
3
4
|
'number',
|
|
@@ -13,7 +14,8 @@ function toPointer(value) {
|
|
|
13
14
|
function escapeSegment(segment) {
|
|
14
15
|
return segment.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
/** The `type` the schema itself declares, and nothing else. */
|
|
18
|
+
function resolveExplicitType(schema) {
|
|
17
19
|
const type = schema.type;
|
|
18
20
|
if (Array.isArray(type)) {
|
|
19
21
|
return type.find((t) => VALID_TYPES.has(t));
|
|
@@ -23,6 +25,96 @@ function resolveType(schema) {
|
|
|
23
25
|
}
|
|
24
26
|
return undefined;
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Keywords that apply to exactly one JSON type, grouped by that type.
|
|
30
|
+
*
|
|
31
|
+
* Two different jobs are done with these, and only one of them infers
|
|
32
|
+
* anything. Every family takes part in detecting a conflict, because a schema
|
|
33
|
+
* drawing keywords from two families implies no single shape. Only `object`
|
|
34
|
+
* and `array` are shapes a form can be given, so those are the only two ever
|
|
35
|
+
* inferred: `string` and `number` are here to be noticed, not chosen.
|
|
36
|
+
*
|
|
37
|
+
* Inferring a scalar would need a rule that is right rather than symmetrical,
|
|
38
|
+
* and there is not one. `minimum` cannot tell `number` from `integer`, and a
|
|
39
|
+
* wrong scalar guess selects the wrong widget, which is harder to notice than
|
|
40
|
+
* a field that never appeared at all.
|
|
41
|
+
*
|
|
42
|
+
* `format` is deliberately absent: it annotates a string's contents rather
|
|
43
|
+
* than describing structure, and schemas apply it to non-strings in practice.
|
|
44
|
+
*/
|
|
45
|
+
const KEYWORD_FAMILIES = {
|
|
46
|
+
object: [
|
|
47
|
+
'properties',
|
|
48
|
+
'patternProperties',
|
|
49
|
+
'additionalProperties',
|
|
50
|
+
'propertyNames',
|
|
51
|
+
'required',
|
|
52
|
+
'minProperties',
|
|
53
|
+
'maxProperties',
|
|
54
|
+
'dependentSchemas',
|
|
55
|
+
'dependentRequired',
|
|
56
|
+
// json-schema-library normalises draft-07 `dependencies` into the two
|
|
57
|
+
// above at parse time. Listed anyway, so the rule does not rely on that.
|
|
58
|
+
'dependencies',
|
|
59
|
+
'unevaluatedProperties',
|
|
60
|
+
],
|
|
61
|
+
array: [
|
|
62
|
+
'items',
|
|
63
|
+
'prefixItems',
|
|
64
|
+
'additionalItems',
|
|
65
|
+
'contains',
|
|
66
|
+
'minItems',
|
|
67
|
+
'maxItems',
|
|
68
|
+
'uniqueItems',
|
|
69
|
+
'minContains',
|
|
70
|
+
'maxContains',
|
|
71
|
+
'unevaluatedItems',
|
|
72
|
+
],
|
|
73
|
+
string: ['minLength', 'maxLength', 'pattern'],
|
|
74
|
+
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
|
|
75
|
+
};
|
|
76
|
+
/** Which families a schema draws keywords from, computed over all of them at once. */
|
|
77
|
+
function projectionTypeFamilies(schema) {
|
|
78
|
+
const families = new Set();
|
|
79
|
+
for (const [family, keywords] of Object.entries(KEYWORD_FAMILIES)) {
|
|
80
|
+
if (keywords.some((keyword) => schema[keyword] !== undefined)) {
|
|
81
|
+
families.add(family);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return families;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether any branch of this node's `oneOf`/`anyOf` could be given a shape at
|
|
88
|
+
* all, for some value.
|
|
89
|
+
*
|
|
90
|
+
* This is what separates "the current value matches no branch" from "this
|
|
91
|
+
* composition is unrenderable whatever the value is". The first is validation's
|
|
92
|
+
* subject and transient; the second is a limitation of this adapter and worth
|
|
93
|
+
* reporting.
|
|
94
|
+
*/
|
|
95
|
+
function compositionHasRenderableAlternative(node) {
|
|
96
|
+
const branches = [...(node.oneOf ?? []), ...(node.anyOf ?? [])];
|
|
97
|
+
return branches.some((branch) => {
|
|
98
|
+
const resolved = dereference(branch);
|
|
99
|
+
const schema = resolved.schema;
|
|
100
|
+
if (!schema || typeof schema !== 'object')
|
|
101
|
+
return false;
|
|
102
|
+
if (resolveExplicitType(schema))
|
|
103
|
+
return true;
|
|
104
|
+
return inferProjectionShape(schema).kind === 'resolved';
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function inferProjectionShape(schema) {
|
|
108
|
+
const families = projectionTypeFamilies(schema);
|
|
109
|
+
if (families.size > 1)
|
|
110
|
+
return { kind: 'ambiguous', families: [...families].sort() };
|
|
111
|
+
if (families.size === 1) {
|
|
112
|
+
const [family] = families;
|
|
113
|
+
if (family === 'object' || family === 'array')
|
|
114
|
+
return { kind: 'resolved', type: family };
|
|
115
|
+
}
|
|
116
|
+
return { kind: 'none' };
|
|
117
|
+
}
|
|
26
118
|
function extractConstraints(schema) {
|
|
27
119
|
const constraints = {};
|
|
28
120
|
if (typeof schema.minLength === 'number')
|
|
@@ -81,45 +173,244 @@ function dereference(node) {
|
|
|
81
173
|
return node.$ref ? node.resolveRef() : node;
|
|
82
174
|
}
|
|
83
175
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
176
|
+
* Every property key that could appear at this node's own instance location,
|
|
177
|
+
* across every branch that might apply to it.
|
|
178
|
+
*
|
|
179
|
+
* This is the static half of the inactive-node contract: a branch the data does
|
|
180
|
+
* not currently select still contributes its property pointers, which then
|
|
181
|
+
* project with `active: false`. The dynamic half is `reduceNode`, which decides
|
|
182
|
+
* which of them apply now, and the two must stay separate. Making the candidate
|
|
183
|
+
* set data-driven would delete a pointer the moment its branch stopped
|
|
184
|
+
* matching, which is exactly the flicker the contract exists to prevent.
|
|
185
|
+
*
|
|
186
|
+
* **Recursion is through applicators only, and only those acting on this same
|
|
187
|
+
* instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
|
|
188
|
+
* `dependentSchemas`, and whatever a `$ref` resolves to. draft-07
|
|
189
|
+
* `dependencies` needs no separate handling, because json-schema-library
|
|
190
|
+
* normalises it into `dependentSchemas` at parse time.
|
|
191
|
+
*
|
|
192
|
+
* Three things are deliberately not followed:
|
|
89
193
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
194
|
+
* - **`not`**, because a subschema inside it describes a shape the instance
|
|
195
|
+
* must *not* satisfy. Collecting its properties as candidates would invert
|
|
196
|
+
* its meaning.
|
|
197
|
+
* - **the values of `properties`**, because those describe a child location,
|
|
198
|
+
* not this one. `owner` is collected; the walk descends into `/owner`
|
|
199
|
+
* separately and collects `name` there. Following it here would make `name` a
|
|
200
|
+
* sibling of `owner`.
|
|
201
|
+
* - **`items` and `prefixItems`**, for the same reason: an array's items are
|
|
202
|
+
* their own locations.
|
|
203
|
+
*
|
|
204
|
+
* The cycle guard keys on `schemaLocation` rather than on node identity, which
|
|
205
|
+
* was measured rather than assumed and is the opposite of what it looks like it
|
|
206
|
+
* should be: `resolveRef()` returns a fresh `SchemaNode` on every call, and the
|
|
207
|
+
* raw `schema` object it wraps is fresh too, so an identity `Set` never matches
|
|
208
|
+
* and a recursive `$ref` would not terminate. `schemaLocation` is stable, is
|
|
209
|
+
* distinct for each inline branch position, and repeats when a reference closes
|
|
210
|
+
* a cycle, which is precisely the three properties needed. The set is per call,
|
|
211
|
+
* because deduplicating a schema location is only sound while collecting names
|
|
212
|
+
* for one instance location; the same schema legitimately recurs at a deeper
|
|
213
|
+
* pointer, and `walk` visits it again there.
|
|
93
214
|
*/
|
|
94
215
|
function collectCandidateProperties(node) {
|
|
95
|
-
const candidates =
|
|
96
|
-
const
|
|
97
|
-
|
|
216
|
+
const candidates = new Map();
|
|
217
|
+
const visited = new Set();
|
|
218
|
+
const record = (key, propNode, direct) => {
|
|
219
|
+
const entry = candidates.get(key) ?? { alternatives: [] };
|
|
220
|
+
if (direct)
|
|
221
|
+
entry.direct ??= propNode;
|
|
222
|
+
else
|
|
223
|
+
entry.alternatives.push(propNode);
|
|
224
|
+
candidates.set(key, entry);
|
|
225
|
+
};
|
|
226
|
+
const visit = (current, own) => {
|
|
227
|
+
const resolved = dereference(current);
|
|
228
|
+
// Keyed on `schemaLocation` where there is one, and on the node itself
|
|
229
|
+
// where there is not, which together cover every way the traversal can
|
|
230
|
+
// come back to where it started.
|
|
231
|
+
//
|
|
232
|
+
// Neither alone is sufficient and each covers what the other cannot. A
|
|
233
|
+
// cycle requires a `$ref`, because an inline schema cannot nest into
|
|
234
|
+
// itself, and `resolveRef()` returns a fresh `SchemaNode` on every call
|
|
235
|
+
// whose raw `schema` object is fresh too, so identity never matches across
|
|
236
|
+
// one; those nodes do carry a location. An inline branch is never resolved
|
|
237
|
+
// through a ref, so its identity is stable within one traversal.
|
|
238
|
+
//
|
|
239
|
+
// This deliberately replaced a depth cap. A cap terminates, but it does so
|
|
240
|
+
// by dropping candidates a valid schema declared, which is the silent
|
|
241
|
+
// disappearance the whole projection-diagnostic design exists to prevent:
|
|
242
|
+
// a field nested under 70 applicators is still a field.
|
|
243
|
+
const location = resolved.schemaLocation;
|
|
244
|
+
const key = typeof location === 'string' ? location : resolved;
|
|
245
|
+
if (visited.has(key))
|
|
98
246
|
return;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
247
|
+
visited.add(key);
|
|
248
|
+
for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
|
|
249
|
+
record(key, propNode, own);
|
|
102
250
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
251
|
+
for (const branch of [resolved.if, resolved.then, resolved.else]) {
|
|
252
|
+
if (branch)
|
|
253
|
+
visit(branch, false);
|
|
254
|
+
}
|
|
255
|
+
for (const branches of [resolved.allOf, resolved.anyOf, resolved.oneOf]) {
|
|
256
|
+
for (const branch of branches ?? [])
|
|
257
|
+
visit(branch, false);
|
|
258
|
+
}
|
|
259
|
+
for (const dependency of Object.values(resolved.dependentSchemas ?? {})) {
|
|
110
260
|
if (dependency && typeof dependency === 'object') {
|
|
111
|
-
|
|
261
|
+
visit(dependency, false);
|
|
112
262
|
}
|
|
113
263
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
merge(branch.properties);
|
|
117
|
-
for (const branch of node.oneOf ?? [])
|
|
118
|
-
merge(branch.properties);
|
|
119
|
-
for (const branch of node.anyOf ?? [])
|
|
120
|
-
merge(branch.properties);
|
|
264
|
+
};
|
|
265
|
+
visit(node, true);
|
|
121
266
|
return candidates;
|
|
122
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* The schema to project for a candidate when no branch is active.
|
|
270
|
+
*
|
|
271
|
+
* A hidden node is still compiled, so it needs some shape, and an inactive key
|
|
272
|
+
* defined differently by two branches has no single right answer. The rule is
|
|
273
|
+
* therefore stated rather than left to traversal order: the node's own
|
|
274
|
+
* declaration wins, and failing that the first branch the schema declares. It
|
|
275
|
+
* is a stable placeholder and deliberately not a claim that one branch matters
|
|
276
|
+
* more, which is why `CandidateProperty` keeps the alternatives instead of
|
|
277
|
+
* discarding them.
|
|
278
|
+
*
|
|
279
|
+
* Last of three, and only reached when no branch has claimed the location: an
|
|
280
|
+
* active branch gives `walk` the reduced node, and a provisionally selected one
|
|
281
|
+
* gives it `composeChild`, so this is consulted for neither.
|
|
282
|
+
*/
|
|
283
|
+
function candidatePrototype(candidate) {
|
|
284
|
+
return candidate.direct ?? candidate.alternatives[0];
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The two statements that both apply to a child location, as one node.
|
|
288
|
+
*
|
|
289
|
+
* A selected branch beats an unselected sibling, and it does not beat the
|
|
290
|
+
* node's own unconditional declaration: in JSON Schema the two are conjunctive,
|
|
291
|
+
* so the instance has to satisfy both. Replacing in either direction is wrong,
|
|
292
|
+
* and visibly so from the form. Dropping the base constraint makes the form
|
|
293
|
+
* accept what the submission rejects. Dropping the branch's makes an unrelated
|
|
294
|
+
* limit appear from nowhere the moment the branch activates, when all the user
|
|
295
|
+
* supplied was the property that completed it.
|
|
296
|
+
*
|
|
297
|
+
* `mergeNode` is the library's own routine, the one `reduceNode` uses to fold a
|
|
298
|
+
* reducer's result into the node it is building, called here in that same order:
|
|
299
|
+
* base first, branch second, so the branch wins a keyword both sides declare.
|
|
300
|
+
* Matching it is the point. It makes the provisional projection *equal* the
|
|
301
|
+
* active projection of the same branch rather than merely resemble it, which is
|
|
302
|
+
* what a hand-written conjunction could not promise.
|
|
303
|
+
*
|
|
304
|
+
* Both sides are dereferenced first because `mergeNode` carries `$ref` over
|
|
305
|
+
* from the base, and `walk` dereferences whatever it is handed: an unresolved
|
|
306
|
+
* `$ref` on the result would resolve back to the base alone and discard the
|
|
307
|
+
* branch.
|
|
308
|
+
*/
|
|
309
|
+
function composeChild(base, branch) {
|
|
310
|
+
if (!base || !branch)
|
|
311
|
+
return base ?? branch;
|
|
312
|
+
return mergeNode(dereference(base), dereference(branch));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The `oneOf` branch the current data uniquely identifies, when the evaluator
|
|
316
|
+
* selected none.
|
|
317
|
+
*
|
|
318
|
+
* `oneOf` selects on full validity, so a branch the data plainly identifies
|
|
319
|
+
* stays unselected while one of its own required properties is absent, and
|
|
320
|
+
* hiding it leaves the user no way to supply the property that would make it
|
|
321
|
+
* apply. This picks that branch so a form can expose it, and the node it
|
|
322
|
+
* returns is reported `provisional` rather than `active`: JSON Schema still
|
|
323
|
+
* says the branch does not apply, and that fact is not this function's to
|
|
324
|
+
* overwrite.
|
|
325
|
+
*
|
|
326
|
+
* Deliberately narrow, because a form that guesses is worse than one that shows
|
|
327
|
+
* nothing:
|
|
328
|
+
*
|
|
329
|
+
* - only an explicit `const` or `enum` on a property discriminates, and both
|
|
330
|
+
* together are conjunctive rather than alternatives
|
|
331
|
+
* - a discriminator has to be present in the data by own-property presence,
|
|
332
|
+
* never read from a `default` annotation, since a declared default is not a
|
|
333
|
+
* value anyone supplied
|
|
334
|
+
* - a key discriminates only if every branch constrains it, so branches that
|
|
335
|
+
* simply differ in shape do not vote
|
|
336
|
+
* - every present discriminator has to agree on one branch
|
|
337
|
+
* - zero or several surviving branches select nothing
|
|
338
|
+
*
|
|
339
|
+
* `type` is excluded: it says almost nothing about intent when every branch is
|
|
340
|
+
* an object. `anyOf` is excluded because several of its branches may apply at
|
|
341
|
+
* once, which is a different question from which one the user means.
|
|
342
|
+
*
|
|
343
|
+
* Nothing else about a branch participates. A branch stays identified when some
|
|
344
|
+
* other constraint of its own fails, or selection would quietly become validity
|
|
345
|
+
* again and the field that completes the branch would stay hidden for a second
|
|
346
|
+
* reason.
|
|
347
|
+
*/
|
|
348
|
+
function selectProvisionalBranch(node, dataRecord) {
|
|
349
|
+
const branches = node.oneOf;
|
|
350
|
+
if (!branches || branches.length === 0 || dataRecord === undefined)
|
|
351
|
+
return undefined;
|
|
352
|
+
const resolvedBranches = branches.map(dereference);
|
|
353
|
+
const discriminators = [...discriminatorKeys(resolvedBranches)].filter((key) => Object.prototype.hasOwnProperty.call(dataRecord, key));
|
|
354
|
+
if (discriminators.length === 0)
|
|
355
|
+
return undefined;
|
|
356
|
+
const accepted = resolvedBranches.filter((branch) => discriminators.every((key) => branchAccepts(branch, key, dataRecord[key])));
|
|
357
|
+
return accepted.length === 1 ? accepted[0] : undefined;
|
|
358
|
+
}
|
|
359
|
+
/** Keys every branch constrains with `const` or `enum`. */
|
|
360
|
+
function discriminatorKeys(branches) {
|
|
361
|
+
const first = branches[0];
|
|
362
|
+
if (!first)
|
|
363
|
+
return new Set();
|
|
364
|
+
const shared = new Set(Object.keys(first.schema.properties ?? {}).filter((key) => isDiscriminator(first, key)));
|
|
365
|
+
for (const branch of branches.slice(1)) {
|
|
366
|
+
for (const key of [...shared]) {
|
|
367
|
+
if (!isDiscriminator(branch, key))
|
|
368
|
+
shared.delete(key);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return shared;
|
|
372
|
+
}
|
|
373
|
+
function discriminatorSchema(branch, key) {
|
|
374
|
+
const property = branch.properties?.[key];
|
|
375
|
+
if (!property)
|
|
376
|
+
return undefined;
|
|
377
|
+
return dereference(property).schema;
|
|
378
|
+
}
|
|
379
|
+
function isDiscriminator(branch, key) {
|
|
380
|
+
const schema = discriminatorSchema(branch, key);
|
|
381
|
+
if (!schema)
|
|
382
|
+
return false;
|
|
383
|
+
return 'const' in schema || Array.isArray(schema.enum);
|
|
384
|
+
}
|
|
385
|
+
/** `const` and `enum` are separate constraints, so a value has to satisfy both. */
|
|
386
|
+
function branchAccepts(branch, key, value) {
|
|
387
|
+
const schema = discriminatorSchema(branch, key);
|
|
388
|
+
if (!schema)
|
|
389
|
+
return false;
|
|
390
|
+
if ('const' in schema && !deepEqual(schema.const, value))
|
|
391
|
+
return false;
|
|
392
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((member) => deepEqual(member, value))) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
function deepEqual(a, b) {
|
|
398
|
+
if (a === b)
|
|
399
|
+
return true;
|
|
400
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
401
|
+
return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));
|
|
402
|
+
}
|
|
403
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
404
|
+
const left = Object.keys(a);
|
|
405
|
+
const right = Object.keys(b);
|
|
406
|
+
return (left.length === right.length &&
|
|
407
|
+
left.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key])));
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
function isPlainObject(value) {
|
|
412
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
413
|
+
}
|
|
123
414
|
/**
|
|
124
415
|
* Computes the currently-required property keys for an object node given its data.
|
|
125
416
|
*
|
|
@@ -158,35 +449,53 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
|
|
|
158
449
|
* NodeProjection a form can render, with `active` reflecting whether that branch is
|
|
159
450
|
* currently selected for `data`.
|
|
160
451
|
*/
|
|
161
|
-
function walk(node, pointer, data, active,
|
|
452
|
+
function walk(node, pointer, data, active,
|
|
453
|
+
/**
|
|
454
|
+
* Whether an ancestor's provisionally selected branch exposes this node. A
|
|
455
|
+
* node is never both: `active` is what the schema says, and this is what the
|
|
456
|
+
* projection selected so the user can complete it.
|
|
457
|
+
*/
|
|
458
|
+
provisional, nodes, diagnostics) {
|
|
162
459
|
const original = dereference(node);
|
|
163
460
|
const originalSchema = original.schema;
|
|
164
461
|
if (typeof originalSchema !== 'object' || originalSchema === null)
|
|
165
462
|
return;
|
|
166
463
|
let resolved = original;
|
|
167
464
|
let schema = originalSchema;
|
|
168
|
-
let type =
|
|
465
|
+
let type = resolveExplicitType(schema);
|
|
169
466
|
// Whether this node's own active branch could be determined. Stays true for every
|
|
170
467
|
// node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
|
|
171
|
-
// (see below)
|
|
172
|
-
//
|
|
468
|
+
// (see below), where the node's own presence in the projection is the thing in
|
|
469
|
+
// doubt rather than only its children's.
|
|
173
470
|
let branchResolved = true;
|
|
174
471
|
// A node whose own schema carries no `type` keyword, only `oneOf`/`anyOf` branches
|
|
175
472
|
// (e.g. a property schema like `{ oneOf: [{ type: 'object', ... }, ...] }`), has no
|
|
176
473
|
// type until its active branch is resolved against `data`. `original` is kept
|
|
177
474
|
// separately so every branch's properties can still be collected below for the
|
|
178
475
|
// inactive-node contract, even though only the matching branch's schema is used here.
|
|
476
|
+
// Whether a branch of this node's own `oneOf`/`anyOf` was resolved against
|
|
477
|
+
// `data`. Recorded so an unresolved shape can be attributed correctly below:
|
|
478
|
+
// on this path, "no shape" means the current value matches no branch, which
|
|
479
|
+
// is a fact about the data rather than about the schema.
|
|
480
|
+
let composedAgainstData = false;
|
|
481
|
+
/**
|
|
482
|
+
* Whether a typeless wrapper's branch was picked by the discriminator rather
|
|
483
|
+
* than by the evaluator. The wrapper is then shown provisionally: the schema
|
|
484
|
+
* still applies no branch, and that is what `active` reports.
|
|
485
|
+
*/
|
|
486
|
+
let wrapperIdentified = false;
|
|
179
487
|
if (!type && (original.oneOf || original.anyOf)) {
|
|
488
|
+
composedAgainstData = true;
|
|
180
489
|
const { node: branchNode } = original.reduceNode(data);
|
|
181
490
|
if (branchNode) {
|
|
182
491
|
resolved = branchNode;
|
|
183
492
|
schema = branchNode.schema;
|
|
184
|
-
type =
|
|
493
|
+
type = resolveExplicitType(schema);
|
|
185
494
|
}
|
|
186
495
|
else {
|
|
187
496
|
// reduceNode() returned no node at all: this is oneOf's behavior when data
|
|
188
497
|
// matches zero or multiple branches (anyOf instead returns a node with an
|
|
189
|
-
// empty merged schema in that case, which
|
|
498
|
+
// empty merged schema in that case, which carries no explicit type either,
|
|
190
499
|
// but which does not reach this branch). This test suite's oneOf wrappers put
|
|
191
500
|
// an object schema on every branch, so 'object' is a safe stand-in type here:
|
|
192
501
|
// it lets this pointer and every candidate branch property still appear in the
|
|
@@ -195,11 +504,102 @@ function walk(node, pointer, data, active, nodes) {
|
|
|
195
504
|
// primitives (not objects) is not handled by this fallback.
|
|
196
505
|
type = 'object';
|
|
197
506
|
branchResolved = false;
|
|
507
|
+
// The wrapper's own branch could not be resolved, so the schema applies
|
|
508
|
+
// none of them. It may still be identifiable: a discriminator present in
|
|
509
|
+
// the data picks one, and the wrapper is then exposed provisionally
|
|
510
|
+
// rather than not at all. Without that, the same schema behaves
|
|
511
|
+
// differently for having declared `type` or not, since the typed path
|
|
512
|
+
// reaches the selector below and this one used to stop here.
|
|
513
|
+
//
|
|
514
|
+
// Only the flag is recorded, deliberately. Assigning the branch to
|
|
515
|
+
// `resolved` erases the difference between what the evaluator resolved
|
|
516
|
+
// and what this projection selected, and the object path reads `resolved`
|
|
517
|
+
// as the former: it would take the branch's `required` as active
|
|
518
|
+
// requiredness, find `nodeActive` false because no branch applies, and
|
|
519
|
+
// report the field as neither required nor provisionally required.
|
|
520
|
+
// Left alone, that path selects the same branch through
|
|
521
|
+
// `selectProvisionalBranch` and attributes it to the right one of the two.
|
|
522
|
+
//
|
|
523
|
+
// Gated on this node already being exposed, for the reason the object
|
|
524
|
+
// path is: selection is local, exposure is the ancestor's to grant.
|
|
525
|
+
wrapperIdentified =
|
|
526
|
+
(active || provisional) &&
|
|
527
|
+
selectProvisionalBranch(original, typeof data === 'object' && data !== null ? data : undefined) !== undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Whether the absence of a shape here is only the current value's fault.
|
|
532
|
+
*
|
|
533
|
+
* Two conditions, and both are needed. Nothing was merged in from a branch,
|
|
534
|
+
* which is what reducing against a value that satisfies none of them leaves
|
|
535
|
+
* behind; and some branch could have been rendered for a value that did
|
|
536
|
+
* satisfy it, so the composition is not unrenderable in itself.
|
|
537
|
+
*
|
|
538
|
+
* The first condition is what a coarser check missed. Entering the
|
|
539
|
+
* composition path says nothing on its own: a branch can be selected and
|
|
540
|
+
* still supply no shape, as `{ anyOf: [{ minLength: 1 }, { pattern: '^a' }] }`
|
|
541
|
+
* does for `"abc"`, where both branches match and neither describes anything
|
|
542
|
+
* this adapter renders, because scalar shapes are deliberately not inferred.
|
|
543
|
+
* That schema is genuinely unprojectable and has to be reported.
|
|
544
|
+
*/
|
|
545
|
+
const isTransientBranchMiss = () => Object.keys(schema).length === 0 && compositionHasRenderableAlternative(original);
|
|
546
|
+
// Last resort, after the oneOf/anyOf branch above has had its chance: that
|
|
547
|
+
// path handles a typeless wrapper whose type only exists once a branch is
|
|
548
|
+
// chosen, and inferring first would take a schema carrying both `properties`
|
|
549
|
+
// and `oneOf` down the object path without ever resolving its branch.
|
|
550
|
+
if (!type) {
|
|
551
|
+
const shape = inferProjectionShape(schema);
|
|
552
|
+
if (shape.kind === 'resolved') {
|
|
553
|
+
type = shape.type;
|
|
554
|
+
}
|
|
555
|
+
else if (shape.kind === 'none' && composedAgainstData && isTransientBranchMiss()) {
|
|
556
|
+
// Deliberately silent, and only for this one case. A projection
|
|
557
|
+
// diagnostic describes a schema this adapter cannot turn into a shape,
|
|
558
|
+
// and a value that matches none of a composition's branches is not that:
|
|
559
|
+
// the same schema renders for a value that does match one. That is
|
|
560
|
+
// validation's subject, it is already reported there, and a form's data
|
|
561
|
+
// is in this state for most of the time someone is filling it in, so a
|
|
562
|
+
// diagnostic here would appear and disappear on each keystroke and train
|
|
563
|
+
// a caller to ignore the channel.
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
// Reported rather than dropped in silence. The field cannot be drawn
|
|
567
|
+
// without a shape, so the caller is told which pointer was skipped and
|
|
568
|
+
// why, instead of finding out from a form that never collected the
|
|
569
|
+
// value.
|
|
570
|
+
diagnostics.push(shape.kind === 'ambiguous'
|
|
571
|
+
? {
|
|
572
|
+
pointer: toPointer(pointer),
|
|
573
|
+
code: 'ambiguous-projection-shape',
|
|
574
|
+
message: `No explicit "type", and keywords from more than one type apply ` +
|
|
575
|
+
`(${shape.families.join(', ')}), so the shape to render is undecidable. ` +
|
|
576
|
+
`Declare "type" on this schema to resolve it.`,
|
|
577
|
+
}
|
|
578
|
+
: {
|
|
579
|
+
pointer: toPointer(pointer),
|
|
580
|
+
code: 'unresolved-projection-shape',
|
|
581
|
+
message: `No explicit "type", and no keyword that implies one, so there is no ` +
|
|
582
|
+
`shape to render. Declare "type" on this schema.` +
|
|
583
|
+
// Said only where it applies, because inferring `string` from
|
|
584
|
+
// an enum is the tempting wrong rule and the reason deserves
|
|
585
|
+
// to travel with the case rather than every message.
|
|
586
|
+
(schema.enum !== undefined
|
|
587
|
+
? ` An "enum" alone does not imply a type, because its members may be of different types.`
|
|
588
|
+
: ''),
|
|
589
|
+
});
|
|
198
590
|
}
|
|
199
591
|
}
|
|
200
592
|
if (!type)
|
|
201
593
|
return;
|
|
202
594
|
const nodeActive = active && branchResolved;
|
|
595
|
+
// A node the schema applies is never also provisional; the two report
|
|
596
|
+
// different facts and only the second is a choice this projection made. The
|
|
597
|
+
// `!nodeActive` term is belt and braces: `provisional` only ever arrives true
|
|
598
|
+
// from a parent that computed `!childActive`, so no caller can reach here
|
|
599
|
+
// with both, and dropping the term changes no observable behaviour.
|
|
600
|
+
const nodeProvisional = !nodeActive && (provisional || wrapperIdentified) && (branchResolved || wrapperIdentified);
|
|
601
|
+
// Whether a form shows this node at all, which is what a descendant inherits.
|
|
602
|
+
const nodeExposed = nodeActive || nodeProvisional;
|
|
203
603
|
if (type === 'object') {
|
|
204
604
|
const dataRecord = typeof data === 'object' && data !== null ? data : undefined;
|
|
205
605
|
// reduceNode() resolves if/then/else, dependentSchemas (which also covers
|
|
@@ -222,17 +622,45 @@ function walk(node, pointer, data, active, nodes) {
|
|
|
222
622
|
{};
|
|
223
623
|
const activeKeys = new Set(Object.keys(reducedProperties));
|
|
224
624
|
const requiredSet = computeRequiredSet(resolved, reducedSchema, dataRecord);
|
|
625
|
+
// Two conditions, and they are not the same kind of condition.
|
|
626
|
+
//
|
|
627
|
+
// Exposure is load-bearing. Selection is local, but whether this node is
|
|
628
|
+
// shown at all is its ancestors' to decide, and a subtree nothing exposes
|
|
629
|
+
// must not select a branch off data it happens to retain: the compiler
|
|
630
|
+
// collapses `required || provisionalRequired` into the one flag a binding
|
|
631
|
+
// reads, so a hidden field would acquire a requirement.
|
|
632
|
+
//
|
|
633
|
+
// The evaluator having selected nothing is where provisional selection
|
|
634
|
+
// exists to help, and that half is unobservable rather than wrong: a
|
|
635
|
+
// resolved branch is fully valid, so it accepts every present discriminator
|
|
636
|
+
// and the selector would return that same branch, whose keys are already
|
|
637
|
+
// the active set. Kept because doing the work is pointless and the
|
|
638
|
+
// condition states the intent.
|
|
639
|
+
const provisionalBranch = nodeExposed && reducedNode === undefined
|
|
640
|
+
? selectProvisionalBranch(original, dataRecord)
|
|
641
|
+
: undefined;
|
|
642
|
+
const provisionalSchema = provisionalBranch?.schema;
|
|
643
|
+
const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
|
|
644
|
+
const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
|
|
225
645
|
// Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
|
|
226
646
|
// branch's properties are represented (the matching branch alone, via `resolved`,
|
|
227
647
|
// would only expose its own properties).
|
|
228
648
|
const candidateProps = collectCandidateProperties(original);
|
|
229
|
-
const propKeys =
|
|
649
|
+
const propKeys = [...candidateProps.keys()];
|
|
230
650
|
const children = propKeys.length > 0
|
|
231
|
-
? propKeys.map((key) =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
651
|
+
? propKeys.map((key) => {
|
|
652
|
+
// Gated on the node applying at all. A branch that does not apply
|
|
653
|
+
// demands nothing, so reporting its `required` array as a
|
|
654
|
+
// requirement would attribute to the validator something it is not
|
|
655
|
+
// asking for.
|
|
656
|
+
const required = nodeActive && requiredSet.has(key);
|
|
657
|
+
return {
|
|
658
|
+
pointer: toPointer(`${pointer}/${escapeSegment(key)}`),
|
|
659
|
+
key,
|
|
660
|
+
required,
|
|
661
|
+
provisionalRequired: !required && provisionalRequired.has(key) ? true : undefined,
|
|
662
|
+
};
|
|
663
|
+
})
|
|
236
664
|
: undefined;
|
|
237
665
|
nodes.set(toPointer(pointer), {
|
|
238
666
|
type,
|
|
@@ -241,14 +669,33 @@ function walk(node, pointer, data, active, nodes) {
|
|
|
241
669
|
children,
|
|
242
670
|
enumValues: extractEnumValues(schema),
|
|
243
671
|
active: nodeActive,
|
|
672
|
+
provisional: nodeProvisional ? true : undefined,
|
|
244
673
|
annotations: extractAnnotations(schema),
|
|
245
674
|
});
|
|
246
675
|
for (const key of propKeys) {
|
|
247
676
|
const childPointer = `${pointer}/${escapeSegment(key)}`;
|
|
248
677
|
const childActive = nodeActive && activeKeys.has(key);
|
|
678
|
+
// A descendant inherits exposure, not activity: under a provisionally
|
|
679
|
+
// selected ancestor its own locally applicable properties are
|
|
680
|
+
// provisional too, while a locally inactive one stays inactive.
|
|
681
|
+
const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
|
|
249
682
|
const reducedChildNode = reducedNode?.properties?.[key];
|
|
250
|
-
|
|
251
|
-
|
|
683
|
+
// Precedence matters as much as the selection does. Marking the right
|
|
684
|
+
// branch provisional while taking its shape from `candidatePrototype`,
|
|
685
|
+
// which is first-wins across branches, would render another branch's
|
|
686
|
+
// widget and annotations under the selected branch's name, and would hand
|
|
687
|
+
// ADR-003's pass another branch's `default`.
|
|
688
|
+
const provisionalChildNode = provisionalBranch?.properties?.[key];
|
|
689
|
+
const candidate = candidateProps.get(key);
|
|
690
|
+
// Three states, in order of authority. What the evaluator reduced against
|
|
691
|
+
// the data is the schema as it actually applies. Failing that, the node's
|
|
692
|
+
// own declaration composed with the selected branch, which is what the
|
|
693
|
+
// reduction would have produced had the branch been complete. Failing
|
|
694
|
+
// both, a stated placeholder for a location no branch has claimed.
|
|
695
|
+
const childNode = reducedChildNode ??
|
|
696
|
+
composeChild(candidate.direct, provisionalChildNode) ??
|
|
697
|
+
candidatePrototype(candidate);
|
|
698
|
+
walk(childNode, childPointer, dataRecord?.[key], childActive, childProvisional, nodes, diagnostics);
|
|
252
699
|
}
|
|
253
700
|
return;
|
|
254
701
|
}
|
|
@@ -259,6 +706,7 @@ function walk(node, pointer, data, active, nodes) {
|
|
|
259
706
|
children: undefined,
|
|
260
707
|
enumValues: extractEnumValues(schema),
|
|
261
708
|
active: nodeActive,
|
|
709
|
+
provisional: nodeProvisional ? true : undefined,
|
|
262
710
|
annotations: extractAnnotations(schema),
|
|
263
711
|
itemAnnotations: type === 'array' && resolved.items
|
|
264
712
|
? extractAnnotations(dereference(resolved.items).schema)
|
|
@@ -266,7 +714,7 @@ function walk(node, pointer, data, active, nodes) {
|
|
|
266
714
|
});
|
|
267
715
|
if (type === 'array' && resolved.items && Array.isArray(data)) {
|
|
268
716
|
data.forEach((item, index) => {
|
|
269
|
-
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodes);
|
|
717
|
+
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics);
|
|
270
718
|
});
|
|
271
719
|
}
|
|
272
720
|
}
|
|
@@ -282,7 +730,8 @@ function walk(node, pointer, data, active, nodes) {
|
|
|
282
730
|
*/
|
|
283
731
|
export function buildProjection(root, data) {
|
|
284
732
|
const nodes = new Map();
|
|
285
|
-
|
|
286
|
-
|
|
733
|
+
const diagnostics = [];
|
|
734
|
+
walk(root, '', data, true, false, nodes, diagnostics);
|
|
735
|
+
return { nodes, diagnostics };
|
|
287
736
|
}
|
|
288
737
|
//# sourceMappingURL=projection.js.map
|