@texaryn/schema-json 0.6.3 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +34 -2
- package/dist/adapter.d.ts +2 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +42 -25
- 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 +26 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +280 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/normalize.d.ts +3 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +118 -0
- package/dist/normalize.js.map +1 -0
- 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.d.ts +7 -1
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +202 -111
- package/dist/projection.js.map +1 -1
- package/dist/root-reference.d.ts +4 -0
- package/dist/root-reference.d.ts.map +1 -0
- package/dist/root-reference.js +86 -0
- package/dist/root-reference.js.map +1 -0
- package/dist/schema-graph.d.ts +29 -0
- package/dist/schema-graph.d.ts.map +1 -0
- package/dist/schema-graph.js +365 -0
- package/dist/schema-graph.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Every family takes part in detecting a conflict, but only `object` and `array` are ever inferred:
|
|
2
|
+
// a wrong scalar guess selects the wrong widget. `format` is absent because it describes string contents.
|
|
3
|
+
const KEYWORD_FAMILIES = {
|
|
4
|
+
object: [
|
|
5
|
+
'properties',
|
|
6
|
+
'patternProperties',
|
|
7
|
+
'additionalProperties',
|
|
8
|
+
'propertyNames',
|
|
9
|
+
'required',
|
|
10
|
+
'minProperties',
|
|
11
|
+
'maxProperties',
|
|
12
|
+
'dependentSchemas',
|
|
13
|
+
'dependentRequired',
|
|
14
|
+
'dependencies',
|
|
15
|
+
'unevaluatedProperties',
|
|
16
|
+
],
|
|
17
|
+
array: [
|
|
18
|
+
'items',
|
|
19
|
+
'prefixItems',
|
|
20
|
+
'additionalItems',
|
|
21
|
+
'contains',
|
|
22
|
+
'minItems',
|
|
23
|
+
'maxItems',
|
|
24
|
+
'uniqueItems',
|
|
25
|
+
'minContains',
|
|
26
|
+
'maxContains',
|
|
27
|
+
'unevaluatedItems',
|
|
28
|
+
],
|
|
29
|
+
string: ['minLength', 'maxLength', 'pattern'],
|
|
30
|
+
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
|
|
31
|
+
};
|
|
32
|
+
export function projectionTypeFamilies(schema) {
|
|
33
|
+
const families = new Set();
|
|
34
|
+
for (const [family, keywords] of Object.entries(KEYWORD_FAMILIES)) {
|
|
35
|
+
if (keywords.some((keyword) => schema[keyword] !== undefined)) {
|
|
36
|
+
families.add(family);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return families;
|
|
40
|
+
}
|
|
41
|
+
export function shapeOfFamilies(families) {
|
|
42
|
+
if (families.size > 1)
|
|
43
|
+
return { kind: 'ambiguous', families: [...families].sort() };
|
|
44
|
+
if (families.size === 1) {
|
|
45
|
+
const [family] = families;
|
|
46
|
+
if (family === 'object' || family === 'array')
|
|
47
|
+
return { kind: 'resolved', type: family };
|
|
48
|
+
}
|
|
49
|
+
return { kind: 'none' };
|
|
50
|
+
}
|
|
51
|
+
export function inferProjectionShape(schema) {
|
|
52
|
+
return shapeOfFamilies(projectionTypeFamilies(schema));
|
|
53
|
+
}
|
|
54
|
+
export function shapeDiagnostic(pointer, shape, enumDeclared) {
|
|
55
|
+
if (shape.kind === 'ambiguous') {
|
|
56
|
+
return {
|
|
57
|
+
pointer,
|
|
58
|
+
code: 'ambiguous-projection-shape',
|
|
59
|
+
message: `No explicit "type", and keywords from more than one type apply ` +
|
|
60
|
+
`(${shape.families.join(', ')}), so the shape to render is undecidable. ` +
|
|
61
|
+
`Declare "type" on this schema to resolve it.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
pointer,
|
|
66
|
+
code: 'unresolved-projection-shape',
|
|
67
|
+
message: `No explicit "type", and no keyword that implies one, so there is no ` +
|
|
68
|
+
`shape to render. Declare "type" on this schema.` +
|
|
69
|
+
(enumDeclared
|
|
70
|
+
? ` An "enum" alone does not imply a type, because its members may be of different types.`
|
|
71
|
+
: ''),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=projection-shape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projection-shape.js","sourceRoot":"","sources":["../src/projection-shape.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,0GAA0G;AAC1G,MAAM,gBAAgB,GAAG;IACvB,MAAM,EAAE;QACN,YAAY;QACZ,mBAAmB;QACnB,sBAAsB;QACtB,eAAe;QACf,UAAU;QACV,eAAe;QACf,eAAe;QACf,kBAAkB;QAClB,mBAAmB;QACnB,cAAc;QACd,uBAAuB;KACxB;IACD,KAAK,EAAE;QACL,OAAO;QACP,aAAa;QACb,iBAAiB;QACjB,UAAU;QACV,UAAU;QACV,UAAU;QACV,aAAa;QACb,aAAa;QACb,aAAa;QACb,kBAAkB;KACnB;IACD,MAAM,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC;IAC7C,MAAM,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,CAAC;CAChC,CAAA;AAStD,MAAM,UAAU,sBAAsB,CAAC,MAA+B;IACpE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAA;IACzC,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClE,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC,EAAE,CAAC;YAC9D,QAAQ,CAAC,GAAG,CAAC,MAAuB,CAAC,CAAA;QACvC,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAoC;IAClE,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAA;IAEnF,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAA;QACzB,IAAI,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IAC1F,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAA+B;IAClE,OAAO,eAAe,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,OAAoB,EACpB,KAAqD,EACrD,YAAqB;IAErB,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC/B,OAAO;YACL,OAAO;YACP,IAAI,EAAE,4BAA4B;YAClC,OAAO,EACL,iEAAiE;gBACjE,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,4CAA4C;gBACzE,8CAA8C;SACjD,CAAA;IACH,CAAC;IACD,OAAO;QACL,OAAO;QACP,IAAI,EAAE,6BAA6B;QACnC,OAAO,EACL,sEAAsE;YACtE,iDAAiD;YACjD,CAAC,YAAY;gBACX,CAAC,CAAC,wFAAwF;gBAC1F,CAAC,CAAC,EAAE,CAAC;KACV,CAAA;AACH,CAAC"}
|
package/dist/projection.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type SchemaNode } from 'json-schema-library';
|
|
2
2
|
import type { SchemaProjection } from '@texaryn/core';
|
|
3
|
+
import { type ProjectionCache } from './identity.js';
|
|
4
|
+
export interface ProjectionLimits {
|
|
5
|
+
readonly objects: number;
|
|
6
|
+
readonly nodes: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const DEFAULT_LIMITS: ProjectionLimits;
|
|
3
9
|
/**
|
|
4
10
|
* Maps a compiled json-schema-library root node to a SchemaProjection.
|
|
5
11
|
* Called by adapter.project() on every data change.
|
|
@@ -10,5 +16,5 @@ import type { SchemaProjection } from '@texaryn/core';
|
|
|
10
16
|
* contract), while array items are walked from `data` (an array's length is a data-time
|
|
11
17
|
* fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
|
|
12
18
|
*/
|
|
13
|
-
export declare function buildProjection(root: SchemaNode, data: unknown): SchemaProjection;
|
|
19
|
+
export declare function buildProjection(root: SchemaNode, data: unknown, cache: ProjectionCache, limits?: ProjectionLimits): SchemaProjection;
|
|
14
20
|
//# sourceMappingURL=projection.d.ts.map
|
package/dist/projection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../src/projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAC9E,OAAO,KAAK,EACV,gBAAgB,EAUjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAQL,KAAK,eAAe,EACrB,MAAM,eAAe,CAAA;AAwJtB,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,cAAc,EAAE,gBAA8C,CAAA;AAqiC3E;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,eAAe,EACtB,MAAM,GAAE,gBAAiC,GACxC,gBAAgB,CAiDlB"}
|
package/dist/projection.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { mergeNode } from 'json-schema-library';
|
|
1
|
+
import { mergeNode, isSchemaNode } from 'json-schema-library';
|
|
2
|
+
import { childDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, } from './identity.js';
|
|
3
|
+
import { inferProjectionShape, shapeDiagnostic } from './projection-shape.js';
|
|
4
|
+
import { POSITION } from './schema-graph.js';
|
|
2
5
|
const VALID_TYPES = new Set([
|
|
3
6
|
'string',
|
|
4
7
|
'number',
|
|
@@ -25,64 +28,6 @@ function resolveExplicitType(schema) {
|
|
|
25
28
|
}
|
|
26
29
|
return undefined;
|
|
27
30
|
}
|
|
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
31
|
/**
|
|
87
32
|
* Whether any branch of this node's `oneOf`/`anyOf` could be given a shape at
|
|
88
33
|
* all, for some value.
|
|
@@ -104,17 +49,6 @@ function compositionHasRenderableAlternative(node) {
|
|
|
104
49
|
return inferProjectionShape(schema).kind === 'resolved';
|
|
105
50
|
});
|
|
106
51
|
}
|
|
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
|
-
}
|
|
118
52
|
function extractConstraints(schema) {
|
|
119
53
|
const constraints = {};
|
|
120
54
|
if (typeof schema.minLength === 'number')
|
|
@@ -174,9 +108,80 @@ function extractEnumValues(schema) {
|
|
|
174
108
|
return undefined;
|
|
175
109
|
return schema.enum.map((value) => ({ value }));
|
|
176
110
|
}
|
|
111
|
+
function dereferenceChecked(node) {
|
|
112
|
+
let current = node;
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
while (typeof current.$ref === 'string') {
|
|
115
|
+
const position = positionOf(current);
|
|
116
|
+
if (seen.has(position))
|
|
117
|
+
return { node: current, cycle: true };
|
|
118
|
+
seen.add(position);
|
|
119
|
+
const next = followRef(current);
|
|
120
|
+
if (!next) {
|
|
121
|
+
const raw = current.resolveRef();
|
|
122
|
+
return { node: (raw ?? current), cycle: false };
|
|
123
|
+
}
|
|
124
|
+
current = next;
|
|
125
|
+
if (!onlyPoints(current))
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
return { node: current, cycle: false };
|
|
129
|
+
}
|
|
177
130
|
/** Follows a $ref to the node it points at; returns the node unchanged otherwise. */
|
|
178
131
|
function dereference(node) {
|
|
179
|
-
return node
|
|
132
|
+
return dereferenceChecked(node).node;
|
|
133
|
+
}
|
|
134
|
+
// A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
|
|
135
|
+
function resolveSelectedBranch(original, reduced, data) {
|
|
136
|
+
const reducedSchema = reduced.schema;
|
|
137
|
+
if (reducedSchema && typeof reducedSchema === 'object') {
|
|
138
|
+
if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
|
|
139
|
+
return reduced;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const selected = [];
|
|
143
|
+
if (original.oneOf) {
|
|
144
|
+
const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
|
|
145
|
+
if (matching.length === 1)
|
|
146
|
+
selected.push(matching[0]);
|
|
147
|
+
}
|
|
148
|
+
for (const branch of original.anyOf ?? []) {
|
|
149
|
+
if (branchApplies(branch, data))
|
|
150
|
+
selected.push(branch);
|
|
151
|
+
}
|
|
152
|
+
if (!selected.some((branch) => typeof branch.$ref === 'string'))
|
|
153
|
+
return reduced;
|
|
154
|
+
let merged = reduced;
|
|
155
|
+
for (const branch of selected) {
|
|
156
|
+
const target = dereference(branch);
|
|
157
|
+
if (!isSchemaNode(target))
|
|
158
|
+
continue;
|
|
159
|
+
const targetReduced = target.reduceNode(data).node ?? target;
|
|
160
|
+
merged = mergeNode(merged, targetReduced) ?? merged;
|
|
161
|
+
}
|
|
162
|
+
return merged;
|
|
163
|
+
}
|
|
164
|
+
export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
|
|
165
|
+
function walkOrder(a, b) {
|
|
166
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
167
|
+
if (a[i] !== b[i])
|
|
168
|
+
return a[i] - b[i];
|
|
169
|
+
return a.length - b.length;
|
|
170
|
+
}
|
|
171
|
+
function addBoundary(ctx, pointer, reason) {
|
|
172
|
+
const reasons = ctx.boundaries.get(pointer) ?? new Set();
|
|
173
|
+
reasons.add(reason);
|
|
174
|
+
ctx.boundaries.set(pointer, reasons);
|
|
175
|
+
}
|
|
176
|
+
// A leaf needs no reduction, so it rides with its parent's budget unit.
|
|
177
|
+
function isLeafSchema(schema) {
|
|
178
|
+
if (typeof schema !== 'object' || schema === null)
|
|
179
|
+
return true;
|
|
180
|
+
const record = schema;
|
|
181
|
+
const type = resolveExplicitType(record);
|
|
182
|
+
if (type !== undefined)
|
|
183
|
+
return type !== 'object';
|
|
184
|
+
return ('enum' in record || 'const' in record) && !('properties' in record);
|
|
180
185
|
}
|
|
181
186
|
/**
|
|
182
187
|
* The position of a schema within the document it was authored in, as a JSON
|
|
@@ -334,7 +339,7 @@ function collectApplicableDefaults(roots, data) {
|
|
|
334
339
|
return declarationsFrom((visitor) => eachApplicable(roots, data, visitor));
|
|
335
340
|
}
|
|
336
341
|
/** The positions that declare one property of `roots`, by both reachability rules. */
|
|
337
|
-
function childPositions(roots, key, data) {
|
|
342
|
+
function childPositions(roots, key, data, declaring) {
|
|
338
343
|
const unconditional = [];
|
|
339
344
|
eachUnconditional(roots.unconditional, (node) => {
|
|
340
345
|
const child = node.properties?.[key];
|
|
@@ -347,10 +352,10 @@ function childPositions(roots, key, data) {
|
|
|
347
352
|
if (child)
|
|
348
353
|
applicable.push(child);
|
|
349
354
|
});
|
|
350
|
-
return { unconditional, applicable };
|
|
355
|
+
return { unconditional, applicable, declaring };
|
|
351
356
|
}
|
|
352
357
|
/** The positions that declare the elements of `roots`, by both reachability rules. */
|
|
353
|
-
function itemPositions(roots, data) {
|
|
358
|
+
function itemPositions(roots, data, declaring) {
|
|
354
359
|
const unconditional = [];
|
|
355
360
|
eachUnconditional(roots.unconditional, (node) => {
|
|
356
361
|
if (node.items)
|
|
@@ -361,7 +366,7 @@ function itemPositions(roots, data) {
|
|
|
361
366
|
if (node.items)
|
|
362
367
|
applicable.push(node.items);
|
|
363
368
|
});
|
|
364
|
-
return { unconditional, applicable };
|
|
369
|
+
return { unconditional, applicable, declaring };
|
|
365
370
|
}
|
|
366
371
|
/**
|
|
367
372
|
* The declarations to report, or nothing where the schema states one answer.
|
|
@@ -641,6 +646,8 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
|
|
|
641
646
|
}
|
|
642
647
|
return required;
|
|
643
648
|
}
|
|
649
|
+
// An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
|
|
650
|
+
const budgeted = (info) => info.cyclic || info.key === '';
|
|
644
651
|
/**
|
|
645
652
|
* Walks the compiled schema statically (via node.properties/node.items plus every
|
|
646
653
|
* conditional branch), only descending into array items that are actually present in
|
|
@@ -664,9 +671,57 @@ provisional, nodes, diagnostics,
|
|
|
664
671
|
* supply: by the time the caller has a node to walk it holds the merge of
|
|
665
672
|
* them, and the merge is what loses a disagreement.
|
|
666
673
|
*/
|
|
667
|
-
declaredAt
|
|
668
|
-
|
|
674
|
+
declaredAt, ctx,
|
|
675
|
+
/** Entered as an object member, which is the only way to be past the data. */
|
|
676
|
+
member,
|
|
677
|
+
/** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
|
|
678
|
+
lineage, path) {
|
|
679
|
+
const info = locationInfo(declaredAt.declaring, ctx.cache);
|
|
680
|
+
const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
|
|
681
|
+
if (info.cycle || referenceCycle) {
|
|
682
|
+
diagnostics.push({
|
|
683
|
+
pointer: toPointer(pointer),
|
|
684
|
+
code: 'unresolved-projection-shape',
|
|
685
|
+
message: `A "$ref" or in-place applicator cycle never leaves this location, so there is no ` +
|
|
686
|
+
`shape to render.`,
|
|
687
|
+
});
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const pastData = member && (data === undefined || data === null);
|
|
691
|
+
const recursive = pastData && ((lineage?.recursive ?? false) || budgeted(info));
|
|
692
|
+
for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
|
|
693
|
+
if (info.key !== '' && ancestor.key === info.key) {
|
|
694
|
+
addBoundary(ctx, ancestor.pointer, 'recursion');
|
|
695
|
+
ctx.removed.add(pointer);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
669
699
|
const originalSchema = original.schema;
|
|
700
|
+
if (recursive && lineage !== undefined) {
|
|
701
|
+
const parent = pointer.slice(0, pointer.lastIndexOf('/'));
|
|
702
|
+
let admit;
|
|
703
|
+
if (isLeafSchema(originalSchema)) {
|
|
704
|
+
const covered = ctx.reserved.has(parent);
|
|
705
|
+
admit = covered || ctx.nodesUsed < ctx.limits.nodes;
|
|
706
|
+
if (admit && !covered)
|
|
707
|
+
ctx.nodesUsed += 1;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const leaves = [...collectCandidateProperties(original).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
|
|
711
|
+
admit = ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes;
|
|
712
|
+
if (admit) {
|
|
713
|
+
ctx.objectsUsed += 1;
|
|
714
|
+
ctx.nodesUsed += 1 + leaves;
|
|
715
|
+
ctx.reserved.add(pointer);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (!admit) {
|
|
719
|
+
addBoundary(ctx, parent, 'budget');
|
|
720
|
+
ctx.removed.add(pointer);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
ctx.flagged.add(pointer);
|
|
724
|
+
}
|
|
670
725
|
if (typeof originalSchema !== 'object' || originalSchema === null)
|
|
671
726
|
return;
|
|
672
727
|
let resolved = original;
|
|
@@ -697,8 +752,8 @@ declaredAt) {
|
|
|
697
752
|
composedAgainstData = true;
|
|
698
753
|
const { node: branchNode } = original.reduceNode(data);
|
|
699
754
|
if (branchNode) {
|
|
700
|
-
resolved = branchNode;
|
|
701
|
-
schema =
|
|
755
|
+
resolved = resolveSelectedBranch(original, branchNode, data);
|
|
756
|
+
schema = resolved.schema;
|
|
702
757
|
type = resolveExplicitType(schema);
|
|
703
758
|
}
|
|
704
759
|
else {
|
|
@@ -751,7 +806,7 @@ declaredAt) {
|
|
|
751
806
|
* this adapter renders, because scalar shapes are deliberately not inferred.
|
|
752
807
|
* That schema is genuinely unprojectable and has to be reported.
|
|
753
808
|
*/
|
|
754
|
-
const isTransientBranchMiss = () => Object.keys(schema).
|
|
809
|
+
const isTransientBranchMiss = () => Object.keys(schema).every((keyword) => keyword === POSITION) && compositionHasRenderableAlternative(original);
|
|
755
810
|
// Last resort, after the oneOf/anyOf branch above has had its chance: that
|
|
756
811
|
// path handles a typeless wrapper whose type only exists once a branch is
|
|
757
812
|
// chosen, and inferring first would take a schema carrying both `properties`
|
|
@@ -776,30 +831,14 @@ declaredAt) {
|
|
|
776
831
|
// without a shape, so the caller is told which pointer was skipped and
|
|
777
832
|
// why, instead of finding out from a form that never collected the
|
|
778
833
|
// value.
|
|
779
|
-
diagnostics.push(shape.
|
|
780
|
-
? {
|
|
781
|
-
pointer: toPointer(pointer),
|
|
782
|
-
code: 'ambiguous-projection-shape',
|
|
783
|
-
message: `No explicit "type", and keywords from more than one type apply ` +
|
|
784
|
-
`(${shape.families.join(', ')}), so the shape to render is undecidable. ` +
|
|
785
|
-
`Declare "type" on this schema to resolve it.`,
|
|
786
|
-
}
|
|
787
|
-
: {
|
|
788
|
-
pointer: toPointer(pointer),
|
|
789
|
-
code: 'unresolved-projection-shape',
|
|
790
|
-
message: `No explicit "type", and no keyword that implies one, so there is no ` +
|
|
791
|
-
`shape to render. Declare "type" on this schema.` +
|
|
792
|
-
// Said only where it applies, because inferring `string` from
|
|
793
|
-
// an enum is the tempting wrong rule and the reason deserves
|
|
794
|
-
// to travel with the case rather than every message.
|
|
795
|
-
(schema.enum !== undefined
|
|
796
|
-
? ` An "enum" alone does not imply a type, because its members may be of different types.`
|
|
797
|
-
: ''),
|
|
798
|
-
});
|
|
834
|
+
diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
|
|
799
835
|
}
|
|
800
836
|
}
|
|
801
837
|
if (!type)
|
|
802
838
|
return;
|
|
839
|
+
const selfLineage = pastData
|
|
840
|
+
? { key: info.key, pointer, recursive, depth: (lineage?.depth ?? 0) + 1, parent: lineage }
|
|
841
|
+
: undefined;
|
|
803
842
|
// Reported at the location it is about, and after the shape gate above: a
|
|
804
843
|
// pointer with no node has nothing to omit an annotation from, and saying
|
|
805
844
|
// that its `default` is undecidable on top of saying it cannot be drawn at
|
|
@@ -819,9 +858,13 @@ declaredAt) {
|
|
|
819
858
|
// selects. A superset of the diagnostic's: every unconditional disagreement
|
|
820
859
|
// is also an applicable one, so the node carries both kinds and a consumer
|
|
821
860
|
// reads one place.
|
|
822
|
-
const
|
|
861
|
+
const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, data);
|
|
862
|
+
const applicableConflict = disagreeingDefaults(applicableDeclarations);
|
|
823
863
|
const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
|
|
824
864
|
const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
|
|
865
|
+
const defaultSources = !conflictedDefault && 'default' in schema && info.cyclic
|
|
866
|
+
? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
|
|
867
|
+
: undefined;
|
|
825
868
|
const nodeActive = active && branchResolved;
|
|
826
869
|
// A node the schema applies is never also provisional; the two report
|
|
827
870
|
// different facts and only the second is a choice this projection made. The
|
|
@@ -846,7 +889,13 @@ declaredAt) {
|
|
|
846
889
|
// wrapper case), it has already been reduced against the real `data` at this
|
|
847
890
|
// pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
|
|
848
891
|
// for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
|
|
849
|
-
|
|
892
|
+
let reducedNode = resolved === original ? resolved.reduceNode(dataRecord ?? {}).node : resolved;
|
|
893
|
+
if (reducedNode &&
|
|
894
|
+
resolved === original &&
|
|
895
|
+
!resolveExplicitType(originalSchema) &&
|
|
896
|
+
(original.oneOf || original.anyOf)) {
|
|
897
|
+
reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
|
|
898
|
+
}
|
|
850
899
|
const reducedSchema = reducedNode?.schema;
|
|
851
900
|
const reducedProperties = reducedSchema?.properties ??
|
|
852
901
|
schema.properties ??
|
|
@@ -903,8 +952,9 @@ declaredAt) {
|
|
|
903
952
|
provisional: nodeProvisional ? true : undefined,
|
|
904
953
|
defaultConflict,
|
|
905
954
|
annotations: extractAnnotations(schema, conflictedDefault),
|
|
955
|
+
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
906
956
|
});
|
|
907
|
-
for (const key of propKeys) {
|
|
957
|
+
for (const [index, key] of propKeys.entries()) {
|
|
908
958
|
const childPointer = `${pointer}/${escapeSegment(key)}`;
|
|
909
959
|
const childActive = nodeActive && activeKeys.has(key);
|
|
910
960
|
// A descendant inherits exposure, not activity: under a provisionally
|
|
@@ -927,15 +977,23 @@ declaredAt) {
|
|
|
927
977
|
const childNode = reducedChildNode ??
|
|
928
978
|
composeChild(candidate.direct, provisionalChildNode) ??
|
|
929
979
|
candidatePrototype(candidate);
|
|
930
|
-
|
|
980
|
+
const positions = childPositions(declaredAt, key,
|
|
931
981
|
// This node's own data, not the child's: the branches being entered
|
|
932
982
|
// are this location's, so they are decided by the instance here. The
|
|
933
983
|
// child's own conditional edges are entered by the child's walk, with
|
|
934
984
|
// the child's data.
|
|
935
|
-
|
|
985
|
+
data, childDeclaring(info, key));
|
|
986
|
+
const enter = () => walk(childNode, childPointer, dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index]);
|
|
987
|
+
// Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
|
|
988
|
+
const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
|
|
989
|
+
if (counted)
|
|
990
|
+
(ctx.queue[selfLineage.depth] ??= []).push({ path: [...path, index], enter });
|
|
991
|
+
else
|
|
992
|
+
enter();
|
|
936
993
|
}
|
|
937
994
|
return;
|
|
938
995
|
}
|
|
996
|
+
const itemSchema = type === 'array' && resolved.items ? dereference(resolved.items).schema : undefined;
|
|
939
997
|
nodes.set(toPointer(pointer), {
|
|
940
998
|
type,
|
|
941
999
|
format: typeof schema.format === 'string' ? schema.format : undefined,
|
|
@@ -946,20 +1004,21 @@ declaredAt) {
|
|
|
946
1004
|
provisional: nodeProvisional ? true : undefined,
|
|
947
1005
|
defaultConflict,
|
|
948
1006
|
annotations: extractAnnotations(schema, conflictedDefault),
|
|
1007
|
+
...(defaultSources !== undefined ? { defaultSources } : {}),
|
|
949
1008
|
// Deliberately not given the flag above. A conflict under `items` belongs
|
|
950
1009
|
// to the element locations, and each of those reports its own; this field
|
|
951
1010
|
// describes the item schema rather than being one of those locations, so
|
|
952
1011
|
// omitting its `default` would be an omission no pointer could report. It
|
|
953
1012
|
// is also the only annotation set nothing reads a `default` from, and the
|
|
954
1013
|
// only one the other adapter does not produce at all.
|
|
955
|
-
itemAnnotations:
|
|
956
|
-
? extractAnnotations(
|
|
1014
|
+
itemAnnotations: typeof itemSchema === 'object' && itemSchema !== null
|
|
1015
|
+
? extractAnnotations(itemSchema)
|
|
957
1016
|
: undefined,
|
|
958
1017
|
});
|
|
959
1018
|
if (type === 'array' && resolved.items && Array.isArray(data)) {
|
|
960
|
-
const elementPositions = itemPositions(declaredAt, data);
|
|
1019
|
+
const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
|
|
961
1020
|
data.forEach((item, index) => {
|
|
962
|
-
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions);
|
|
1021
|
+
walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index]);
|
|
963
1022
|
});
|
|
964
1023
|
}
|
|
965
1024
|
}
|
|
@@ -973,10 +1032,42 @@ declaredAt) {
|
|
|
973
1032
|
* contract), while array items are walked from `data` (an array's length is a data-time
|
|
974
1033
|
* fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
|
|
975
1034
|
*/
|
|
976
|
-
export function buildProjection(root, data) {
|
|
1035
|
+
export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
|
|
977
1036
|
const nodes = new Map();
|
|
978
1037
|
const diagnostics = [];
|
|
979
|
-
|
|
1038
|
+
const ctx = {
|
|
1039
|
+
cache,
|
|
1040
|
+
limits,
|
|
1041
|
+
boundaries: new Map(),
|
|
1042
|
+
removed: new Set(),
|
|
1043
|
+
flagged: new Set(),
|
|
1044
|
+
reserved: new Set(),
|
|
1045
|
+
queue: [],
|
|
1046
|
+
objectsUsed: 0,
|
|
1047
|
+
nodesUsed: 0,
|
|
1048
|
+
};
|
|
1049
|
+
walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, []);
|
|
1050
|
+
for (const level of ctx.queue)
|
|
1051
|
+
for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
|
|
1052
|
+
enter();
|
|
1053
|
+
for (const node of nodes.values()) {
|
|
1054
|
+
if (node.children?.some((child) => ctx.removed.has(child.pointer))) {
|
|
1055
|
+
;
|
|
1056
|
+
node.children = node.children.filter((child) => !ctx.removed.has(child.pointer));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
for (const pointer of ctx.flagged) {
|
|
1060
|
+
const node = nodes.get(toPointer(pointer));
|
|
1061
|
+
if (node)
|
|
1062
|
+
node.recursiveExpansion = true;
|
|
1063
|
+
}
|
|
1064
|
+
for (const [pointer, reasons] of ctx.boundaries) {
|
|
1065
|
+
const node = nodes.get(toPointer(pointer));
|
|
1066
|
+
if (node) {
|
|
1067
|
+
;
|
|
1068
|
+
node.boundaries = ['recursion', 'budget'].filter((reason) => reasons.has(reason));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
980
1071
|
return { nodes, diagnostics };
|
|
981
1072
|
}
|
|
982
1073
|
//# sourceMappingURL=projection.js.map
|