@json-schema-engine/core 0.0.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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/coverage.d.ts +30 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +81 -0
- package/dist/coverage.js.map +1 -0
- package/dist/cursor.d.ts +16 -0
- package/dist/cursor.d.ts.map +1 -0
- package/dist/cursor.js +28 -0
- package/dist/cursor.js.map +1 -0
- package/dist/dialect.d.ts +316 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +135 -0
- package/dist/dialect.js.map +1 -0
- package/dist/engine.d.ts +203 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +493 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +49 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +128 -0
- package/dist/json.js.map +1 -0
- package/dist/keywords/applicator.d.ts +57 -0
- package/dist/keywords/applicator.d.ts.map +1 -0
- package/dist/keywords/applicator.js +789 -0
- package/dist/keywords/applicator.js.map +1 -0
- package/dist/keywords/core.d.ts +42 -0
- package/dist/keywords/core.d.ts.map +1 -0
- package/dist/keywords/core.js +139 -0
- package/dist/keywords/core.js.map +1 -0
- package/dist/keywords/format.d.ts +43 -0
- package/dist/keywords/format.d.ts.map +1 -0
- package/dist/keywords/format.js +101 -0
- package/dist/keywords/format.js.map +1 -0
- package/dist/keywords/metaschemas2019.d.ts +3 -0
- package/dist/keywords/metaschemas2019.d.ts.map +1 -0
- package/dist/keywords/metaschemas2019.js +406 -0
- package/dist/keywords/metaschemas2019.js.map +1 -0
- package/dist/keywords/metaschemas2020.d.ts +3 -0
- package/dist/keywords/metaschemas2020.d.ts.map +1 -0
- package/dist/keywords/metaschemas2020.js +441 -0
- package/dist/keywords/metaschemas2020.js.map +1 -0
- package/dist/keywords/metaschemas6.d.ts +3 -0
- package/dist/keywords/metaschemas6.d.ts.map +1 -0
- package/dist/keywords/metaschemas6.js +221 -0
- package/dist/keywords/metaschemas6.js.map +1 -0
- package/dist/keywords/metaschemas7.d.ts +3 -0
- package/dist/keywords/metaschemas7.d.ts.map +1 -0
- package/dist/keywords/metaschemas7.js +248 -0
- package/dist/keywords/metaschemas7.js.map +1 -0
- package/dist/keywords/unevaluated.d.ts +14 -0
- package/dist/keywords/unevaluated.d.ts.map +1 -0
- package/dist/keywords/unevaluated.js +296 -0
- package/dist/keywords/unevaluated.js.map +1 -0
- package/dist/keywords/validation.d.ts +21 -0
- package/dist/keywords/validation.d.ts.map +1 -0
- package/dist/keywords/validation.js +332 -0
- package/dist/keywords/validation.js.map +1 -0
- package/dist/keywords/vocab2019.d.ts +36 -0
- package/dist/keywords/vocab2019.d.ts.map +1 -0
- package/dist/keywords/vocab2019.js +585 -0
- package/dist/keywords/vocab2019.js.map +1 -0
- package/dist/keywords/vocab2020.d.ts +20 -0
- package/dist/keywords/vocab2020.d.ts.map +1 -0
- package/dist/keywords/vocab2020.js +55 -0
- package/dist/keywords/vocab2020.js.map +1 -0
- package/dist/keywords/vocab7.d.ts +46 -0
- package/dist/keywords/vocab7.d.ts.map +1 -0
- package/dist/keywords/vocab7.js +305 -0
- package/dist/keywords/vocab7.js.map +1 -0
- package/dist/loader.d.ts +41 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +13 -0
- package/dist/loader.js.map +1 -0
- package/dist/lowering.d.ts +371 -0
- package/dist/lowering.d.ts.map +1 -0
- package/dist/lowering.js +59 -0
- package/dist/lowering.js.map +1 -0
- package/dist/output.d.ts +251 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +357 -0
- package/dist/output.js.map +1 -0
- package/dist/records.d.ts +71 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +150 -0
- package/dist/records.js.map +1 -0
- package/dist/ref.d.ts +10 -0
- package/dist/ref.d.ts.map +1 -0
- package/dist/ref.js +5 -0
- package/dist/ref.js.map +1 -0
- package/dist/regex.d.ts +45 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +163 -0
- package/dist/regex.js.map +1 -0
- package/dist/registry.d.ts +141 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +407 -0
- package/dist/registry.js.map +1 -0
- package/dist/result.d.ts +109 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +112 -0
- package/dist/result.js.map +1 -0
- package/dist/uri.d.ts +18 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +27 -0
- package/dist/uri.js.map +1 -0
- package/dist/walk.d.ts +25 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +65 -0
- package/dist/walk.js.map +1 -0
- package/package.json +32 -0
- package/src/coverage.ts +88 -0
- package/src/cursor.ts +42 -0
- package/src/dialect.ts +428 -0
- package/src/engine.ts +741 -0
- package/src/index.ts +699 -0
- package/src/json.ts +137 -0
- package/src/keywords/applicator.ts +895 -0
- package/src/keywords/core.ts +168 -0
- package/src/keywords/format.ts +143 -0
- package/src/keywords/metaschemas2019.ts +410 -0
- package/src/keywords/metaschemas2020.ts +443 -0
- package/src/keywords/metaschemas6.ts +226 -0
- package/src/keywords/metaschemas7.ts +253 -0
- package/src/keywords/unevaluated.ts +329 -0
- package/src/keywords/validation.ts +627 -0
- package/src/keywords/vocab2019.ts +676 -0
- package/src/keywords/vocab2020.ts +82 -0
- package/src/keywords/vocab7.ts +393 -0
- package/src/loader.ts +60 -0
- package/src/lowering.ts +414 -0
- package/src/output.ts +651 -0
- package/src/records.ts +226 -0
- package/src/ref.ts +14 -0
- package/src/regex.ts +171 -0
- package/src/registry.ts +481 -0
- package/src/result.ts +252 -0
- package/src/uri.ts +35 -0
- package/src/walk.ts +88 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// Schema registration and static reference resolution.
|
|
2
|
+
//
|
|
3
|
+
// The registration walk descends only into *schema positions*, so `$id` or
|
|
4
|
+
// `$anchor` inside `enum` data or an unknown keyword's value is never treated
|
|
5
|
+
// as an identifier. Unlike the F2 prototype, the positions are not hard-coded:
|
|
6
|
+
// the walk asks each keyword behavior's analyze() for its subschema positions
|
|
7
|
+
// (DESIGN.md D1/D2), so a custom applicator registered through the dialect
|
|
8
|
+
// registry gets correct identifier handling for free.
|
|
9
|
+
|
|
10
|
+
import { JsonValue, isObject, escapeSegment, unescapeSegment } from "./json.js";
|
|
11
|
+
import { resolveUri, splitFragment, UnresolvableRefError } from "./uri.js";
|
|
12
|
+
import { SchemaRef } from "./ref.js";
|
|
13
|
+
import { Dialect, DialectRegistry, ReadOnlyRegistryError } from "./dialect.js";
|
|
14
|
+
import { SourceRange } from "./loader.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Where a schema resource physically lives: the registered document
|
|
18
|
+
* containing it and the JSON Pointer from that document's root to the
|
|
19
|
+
* resource's root (D17 bridge; see loader.ts).
|
|
20
|
+
*/
|
|
21
|
+
export interface DocumentLocation {
|
|
22
|
+
documentUri: string;
|
|
23
|
+
pointer: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A value that is not a schema (neither an object nor a boolean) was found
|
|
28
|
+
* where a schema is required (D19): in a keyword-claimed schema position at
|
|
29
|
+
* registration, or applied as a schema during evaluation. Keyword-value
|
|
30
|
+
* validity beyond schema shape is not checked here — that is metaschema
|
|
31
|
+
* validation's job ({@link EngineOptions.validateSchemas}).
|
|
32
|
+
*
|
|
33
|
+
* When thrown during registration, the document may be partially indexed;
|
|
34
|
+
* re-register a corrected document under the same URI, or discard the
|
|
35
|
+
* engine.
|
|
36
|
+
*/
|
|
37
|
+
export class InvalidSchemaError extends Error {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Schema nesting (at registration) or schema-application nesting (at
|
|
41
|
+
* evaluation) exceeded {@link EngineOptions.maxDepth}. A bounded, catchable
|
|
42
|
+
* failure that replaces the native stack overflow deep input would otherwise
|
|
43
|
+
* cause; the engine remains usable afterward (each evaluation runs in fresh
|
|
44
|
+
* state). Raise `maxDepth` for legitimately deep documents, within what the
|
|
45
|
+
* runtime's own stack allows.
|
|
46
|
+
*/
|
|
47
|
+
export class MaxDepthExceededError extends Error {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default schema/application nesting bound. Chosen below the native
|
|
51
|
+
* call-stack ceiling so the typed {@link MaxDepthExceededError} fires before
|
|
52
|
+
* a `RangeError`, while staying generous for real-world documents.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_MAX_DEPTH = 512;
|
|
55
|
+
|
|
56
|
+
/** One-line description of a non-schema value for error messages. */
|
|
57
|
+
export function describeNonSchema(node: JsonValue): string {
|
|
58
|
+
if (node === null) return "null";
|
|
59
|
+
return Array.isArray(node) ? "array" : typeof node;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Schema registration, identifier indexing, and reference resolution. */
|
|
63
|
+
export class SchemaRegistry {
|
|
64
|
+
private documents = new Map<string, JsonValue>(); // resource URI -> schema node
|
|
65
|
+
private anchors = new Map<string, SchemaRef>(); // "resource#anchor"
|
|
66
|
+
private dynamicAnchors = new Map<string, SchemaRef>(); // $dynamicAnchor only (D8)
|
|
67
|
+
private recursiveRoots = new Set<string>(); // 2019-09 $recursiveAnchor at root
|
|
68
|
+
// Unions of StaticFacts.produces / consumes over every registered keyword
|
|
69
|
+
// occurrence: produce()'s declaration guard and the elision predicate's
|
|
70
|
+
// "someone might read this" side (D5/M5.5).
|
|
71
|
+
private producedBehaviorIds = new Set<string>();
|
|
72
|
+
private consumedBehaviorIds = new Set<string>();
|
|
73
|
+
// Consumed ids whose producer declares evaluated coverage (D9a facts): the
|
|
74
|
+
// only dependency data the coverage channel's shape-dispatching folds
|
|
75
|
+
// understand (coverage.ts).
|
|
76
|
+
private coverageProducerIds = new Set<string>();
|
|
77
|
+
private coverageConsumedIds = new Set<string>();
|
|
78
|
+
private documentDialects = new Map<string, string>(); // resource URI -> dialect URI
|
|
79
|
+
private resourceLocations = new Map<string, DocumentLocation>();
|
|
80
|
+
// Retrieval URI -> declared $id base, when they differ: the document must
|
|
81
|
+
// be reachable under both, but anchors and lexical bases live under $id.
|
|
82
|
+
private aliases = new Map<string, string>();
|
|
83
|
+
// External resources seen in reference values during registration walks,
|
|
84
|
+
// drained by the load closure (D7).
|
|
85
|
+
private pendingResources = new Set<string>();
|
|
86
|
+
private documentRanges = new Map<
|
|
87
|
+
string,
|
|
88
|
+
(pointer: string) => SourceRange | undefined
|
|
89
|
+
>();
|
|
90
|
+
/**
|
|
91
|
+
* Called for each `pattern`/`patternProperties` regex during a
|
|
92
|
+
* registration walk, when set. The Engine installs this (after registering
|
|
93
|
+
* its trusted metaschemas) to enforce `rejectUnsafeRegex`.
|
|
94
|
+
*/
|
|
95
|
+
onRegex?: (pattern: string, location: string) => void;
|
|
96
|
+
// Snapshots share the indexes above copy-on-write: the source copies them
|
|
97
|
+
// before its first registration after a snapshot, so a view stays frozen
|
|
98
|
+
// at no cost until the source changes.
|
|
99
|
+
private shared = false;
|
|
100
|
+
private readOnly = false;
|
|
101
|
+
|
|
102
|
+
constructor(
|
|
103
|
+
private dialectRegistry: DialectRegistry,
|
|
104
|
+
private defaultDialectUri: string,
|
|
105
|
+
private maxDepth: number = DEFAULT_MAX_DEPTH,
|
|
106
|
+
) {}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A read-only view of the registry's current contents, over a view of the
|
|
110
|
+
* dialect registry. Later registrations on this registry are invisible to
|
|
111
|
+
* the view, and registering into the view throws
|
|
112
|
+
* {@link ReadOnlyRegistryError}. A compiled artifact binds to one so that
|
|
113
|
+
* a compilation boundary cannot change reference resolution (E1).
|
|
114
|
+
*/
|
|
115
|
+
snapshot(): SchemaRegistry {
|
|
116
|
+
const view = new SchemaRegistry(
|
|
117
|
+
this.dialectRegistry.snapshot(),
|
|
118
|
+
this.defaultDialectUri,
|
|
119
|
+
this.maxDepth,
|
|
120
|
+
);
|
|
121
|
+
view.documents = this.documents;
|
|
122
|
+
view.anchors = this.anchors;
|
|
123
|
+
view.dynamicAnchors = this.dynamicAnchors;
|
|
124
|
+
view.recursiveRoots = this.recursiveRoots;
|
|
125
|
+
view.producedBehaviorIds = this.producedBehaviorIds;
|
|
126
|
+
view.consumedBehaviorIds = this.consumedBehaviorIds;
|
|
127
|
+
view.coverageProducerIds = this.coverageProducerIds;
|
|
128
|
+
view.coverageConsumedIds = this.coverageConsumedIds;
|
|
129
|
+
view.documentDialects = this.documentDialects;
|
|
130
|
+
view.resourceLocations = this.resourceLocations;
|
|
131
|
+
view.aliases = this.aliases;
|
|
132
|
+
view.documentRanges = this.documentRanges;
|
|
133
|
+
view.readOnly = true;
|
|
134
|
+
this.shared = true;
|
|
135
|
+
return view;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private mutable(): void {
|
|
139
|
+
if (this.readOnly) {
|
|
140
|
+
throw new ReadOnlyRegistryError("a registry snapshot is read-only");
|
|
141
|
+
}
|
|
142
|
+
if (!this.shared) return;
|
|
143
|
+
this.documents = new Map(this.documents);
|
|
144
|
+
this.anchors = new Map(this.anchors);
|
|
145
|
+
this.dynamicAnchors = new Map(this.dynamicAnchors);
|
|
146
|
+
this.recursiveRoots = new Set(this.recursiveRoots);
|
|
147
|
+
this.producedBehaviorIds = new Set(this.producedBehaviorIds);
|
|
148
|
+
this.consumedBehaviorIds = new Set(this.consumedBehaviorIds);
|
|
149
|
+
this.coverageProducerIds = new Set(this.coverageProducerIds);
|
|
150
|
+
this.coverageConsumedIds = new Set(this.coverageConsumedIds);
|
|
151
|
+
this.documentDialects = new Map(this.documentDialects);
|
|
152
|
+
this.resourceLocations = new Map(this.resourceLocations);
|
|
153
|
+
this.aliases = new Map(this.aliases);
|
|
154
|
+
this.documentRanges = new Map(this.documentRanges);
|
|
155
|
+
this.shared = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register a schema document. The dialect comes from `$schema` when present
|
|
160
|
+
* (and must already be registered), else `dialectUri`, else the default.
|
|
161
|
+
* Returns the document's canonical base URI.
|
|
162
|
+
*/
|
|
163
|
+
register(
|
|
164
|
+
schema: JsonValue,
|
|
165
|
+
retrievalUri: string,
|
|
166
|
+
dialectUri?: string,
|
|
167
|
+
getRange?: (pointer: string) => SourceRange | undefined,
|
|
168
|
+
): string {
|
|
169
|
+
this.mutable();
|
|
170
|
+
// Dialect URIs are compared fragment-free: "…/draft-07/schema#" (the
|
|
171
|
+
// canonical in-the-wild $schema spelling) names the same dialect.
|
|
172
|
+
let effectiveDialect = splitFragment(
|
|
173
|
+
dialectUri ?? this.defaultDialectUri,
|
|
174
|
+
).resource;
|
|
175
|
+
if (isObject(schema) && typeof schema.$schema === "string") {
|
|
176
|
+
effectiveDialect = splitFragment(
|
|
177
|
+
resolveUri(schema.$schema, retrievalUri),
|
|
178
|
+
).resource;
|
|
179
|
+
}
|
|
180
|
+
const dialect = this.dialectRegistry.getDialect(effectiveDialect);
|
|
181
|
+
|
|
182
|
+
const retrievalResource = splitFragment(retrievalUri).resource;
|
|
183
|
+
let baseUri = retrievalResource;
|
|
184
|
+
const rootIds = isObject(schema) ? dialect.identifiers(schema) : {};
|
|
185
|
+
if (rootIds.baseId !== undefined) {
|
|
186
|
+
baseUri = splitFragment(resolveUri(rootIds.baseId, baseUri)).resource;
|
|
187
|
+
}
|
|
188
|
+
if (baseUri !== retrievalResource)
|
|
189
|
+
this.aliases.set(retrievalResource, baseUri);
|
|
190
|
+
this.documents.set(baseUri, schema);
|
|
191
|
+
this.documentDialects.set(baseUri, effectiveDialect);
|
|
192
|
+
this.resourceLocations.set(baseUri, { documentUri: baseUri, pointer: "" });
|
|
193
|
+
if (getRange) this.documentRanges.set(baseUri, getRange);
|
|
194
|
+
this.walk(schema, baseUri, "", baseUri, "", dialect, 0);
|
|
195
|
+
return baseUri;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private walk(
|
|
199
|
+
node: JsonValue,
|
|
200
|
+
baseUri: string,
|
|
201
|
+
pointer: string,
|
|
202
|
+
documentUri: string,
|
|
203
|
+
docPointer: string, // pointer from the registered document's root
|
|
204
|
+
dialect: Dialect,
|
|
205
|
+
depth: number,
|
|
206
|
+
): void {
|
|
207
|
+
if (depth > this.maxDepth) {
|
|
208
|
+
throw new MaxDepthExceededError(
|
|
209
|
+
`schema nesting exceeds maxDepth (${this.maxDepth}) at ` +
|
|
210
|
+
`'${baseUri}#${pointer}'`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (typeof node === "boolean") return;
|
|
214
|
+
if (!isObject(node)) {
|
|
215
|
+
throw new InvalidSchemaError(
|
|
216
|
+
`non-schema value (${describeNonSchema(node)}) in schema position ` +
|
|
217
|
+
`'${baseUri}#${pointer}'`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const ids = dialect.identifiers(node);
|
|
222
|
+
if (pointer !== "" && ids.baseId !== undefined) {
|
|
223
|
+
baseUri = splitFragment(resolveUri(ids.baseId, baseUri)).resource;
|
|
224
|
+
pointer = "";
|
|
225
|
+
this.documents.set(baseUri, node);
|
|
226
|
+
this.documentDialects.set(baseUri, dialect.uri);
|
|
227
|
+
this.resourceLocations.set(baseUri, { documentUri, pointer: docPointer });
|
|
228
|
+
}
|
|
229
|
+
for (const anchor of ids.anchors ?? []) {
|
|
230
|
+
this.anchors.set(`${baseUri}#${anchor}`, { node, baseUri, pointer });
|
|
231
|
+
}
|
|
232
|
+
// A dynamic anchor is also a plain anchor for $ref purposes; only the
|
|
233
|
+
// dynamic-anchor index participates in $dynamicRef rebinding (D8).
|
|
234
|
+
if (ids.dynamicAnchor !== undefined) {
|
|
235
|
+
const ref = { node, baseUri, pointer };
|
|
236
|
+
this.anchors.set(`${baseUri}#${ids.dynamicAnchor}`, ref);
|
|
237
|
+
this.dynamicAnchors.set(`${baseUri}#${ids.dynamicAnchor}`, ref);
|
|
238
|
+
}
|
|
239
|
+
// $recursiveAnchor participates in rebinding only at a resource root.
|
|
240
|
+
if (ids.recursiveAnchor === true && pointer === "") {
|
|
241
|
+
this.recursiveRoots.add(baseUri);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const [name, value] of Object.entries(node)) {
|
|
245
|
+
const behavior = dialect.keywords.get(name)?.behavior;
|
|
246
|
+
const facts = behavior?.analyze?.(value, { schema: node });
|
|
247
|
+
if (!facts) continue;
|
|
248
|
+
for (const p of facts.produces ?? []) this.producedBehaviorIds.add(p);
|
|
249
|
+
for (const c of facts.consumes ?? []) {
|
|
250
|
+
this.consumedBehaviorIds.add(c);
|
|
251
|
+
if (this.coverageProducerIds.has(c)) this.coverageConsumedIds.add(c);
|
|
252
|
+
}
|
|
253
|
+
if (
|
|
254
|
+
facts.evaluatesNames !== undefined ||
|
|
255
|
+
facts.evaluatesIndexes !== undefined
|
|
256
|
+
) {
|
|
257
|
+
this.coverageProducerIds.add(behavior!.id);
|
|
258
|
+
if (this.consumedBehaviorIds.has(behavior!.id)) {
|
|
259
|
+
this.coverageConsumedIds.add(behavior!.id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (this.onRegex) {
|
|
263
|
+
const keywordLocation = `${baseUri}#${pointer}/${escapeSegment(name)}`;
|
|
264
|
+
for (const rx of facts.regexes ?? []) this.onRegex(rx, keywordLocation);
|
|
265
|
+
}
|
|
266
|
+
for (const ref of facts.references ?? []) {
|
|
267
|
+
try {
|
|
268
|
+
this.pendingResources.add(
|
|
269
|
+
splitFragment(resolveUri(ref, baseUri)).resource,
|
|
270
|
+
);
|
|
271
|
+
} catch {
|
|
272
|
+
// Unresolvable now is not an error: evaluation reports it if the
|
|
273
|
+
// reference is actually followed.
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const positions = facts.subschemas;
|
|
277
|
+
if (!positions) continue;
|
|
278
|
+
for (const relPath of positions) {
|
|
279
|
+
let child: JsonValue = value;
|
|
280
|
+
let suffix = "/" + escapeSegment(name);
|
|
281
|
+
for (const seg of relPath) {
|
|
282
|
+
child = (
|
|
283
|
+
Array.isArray(child)
|
|
284
|
+
? child[seg as number]
|
|
285
|
+
: (child as Record<string, JsonValue>)[seg as string]
|
|
286
|
+
) as JsonValue;
|
|
287
|
+
suffix += "/" + escapeSegment(String(seg));
|
|
288
|
+
}
|
|
289
|
+
this.walk(
|
|
290
|
+
child,
|
|
291
|
+
baseUri,
|
|
292
|
+
pointer + suffix,
|
|
293
|
+
documentUri,
|
|
294
|
+
docPointer + suffix,
|
|
295
|
+
dialect,
|
|
296
|
+
depth + 1,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The registered document containing a schema resource, and the resource
|
|
304
|
+
* root's pointer from that document's root (D17 bridge; see loader.ts).
|
|
305
|
+
* Undefined for resources the registration walk never saw (e.g. an `$id`
|
|
306
|
+
* inside an unknown keyword reached only by pointer navigation).
|
|
307
|
+
*/
|
|
308
|
+
documentLocation(resourceUri: string): DocumentLocation | undefined {
|
|
309
|
+
return this.resourceLocations.get(this.canonical(resourceUri));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Source range for a document-rooted pointer, when the loader supplied one (D17). */
|
|
313
|
+
range(documentUri: string, pointer: string): SourceRange | undefined {
|
|
314
|
+
return this.documentRanges.get(documentUri)?.(pointer);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** True if a resource is registered, directly or via a retrieval-URI alias. */
|
|
318
|
+
has(resourceUri: string): boolean {
|
|
319
|
+
return this.documents.has(resourceUri) || this.aliases.has(resourceUri);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The schema node at a resource's root, if registered. */
|
|
323
|
+
document(resourceUri: string): JsonValue | undefined {
|
|
324
|
+
return this.documents.get(this.canonical(resourceUri));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** External resources referenced but not yet registered; drained per call. */
|
|
328
|
+
takeUnresolved(): string[] {
|
|
329
|
+
const missing = [...this.pendingResources].filter((r) => !this.has(r));
|
|
330
|
+
this.pendingResources.clear();
|
|
331
|
+
return missing;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The `$dynamicAnchor` target for a name in a resource, if one was registered (D8). */
|
|
335
|
+
dynamicAnchor(resourceUri: string, name: string): SchemaRef | undefined {
|
|
336
|
+
return this.dynamicAnchors.get(`${this.canonical(resourceUri)}#${name}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** True if a resource's root carries 2019-09 `$recursiveAnchor: true`. */
|
|
340
|
+
hasRecursiveRoot(resourceUri: string): boolean {
|
|
341
|
+
return this.recursiveRoots.has(this.canonical(resourceUri));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Behavior ids some registered keyword declares it produces dependency data under. */
|
|
345
|
+
producedIds(): ReadonlySet<string> {
|
|
346
|
+
return this.producedBehaviorIds;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Behavior ids some registered keyword declares it consumes. */
|
|
350
|
+
consumedIds(): ReadonlySet<string> {
|
|
351
|
+
return this.consumedBehaviorIds;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** The consumed subset of {@link consumedIds} whose producer declares evaluated coverage: what the coverage channel carries. */
|
|
355
|
+
coverageIds(): ReadonlySet<string> {
|
|
356
|
+
return this.coverageConsumedIds;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private canonical(resourceUri: string): string {
|
|
360
|
+
return this.aliases.get(resourceUri) ?? resourceUri;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The dialect URI a resource was registered under.
|
|
365
|
+
* @throws UnresolvableRefError if the resource is not registered.
|
|
366
|
+
*/
|
|
367
|
+
dialectUriFor(baseUri: string): string {
|
|
368
|
+
const uri = this.documentDialects.get(this.canonical(baseUri));
|
|
369
|
+
if (uri === undefined)
|
|
370
|
+
throw new UnresolvableRefError(`unknown schema '${baseUri}'`);
|
|
371
|
+
return uri;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The dialect a resource was registered under.
|
|
376
|
+
* @throws UnresolvableRefError if the resource is not registered.
|
|
377
|
+
*/
|
|
378
|
+
dialectFor(baseUri: string): Dialect {
|
|
379
|
+
return this.dialectRegistry.getDialect(this.dialectUriFor(baseUri));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Resolves a URI to its resource's root schema.
|
|
384
|
+
* @throws UnresolvableRefError if the resource is not registered.
|
|
385
|
+
*/
|
|
386
|
+
rootRef(uri: string): SchemaRef {
|
|
387
|
+
const { resource: rawResource, fragment } = splitFragment(uri);
|
|
388
|
+
const resource = this.canonical(rawResource);
|
|
389
|
+
if (fragment !== null && fragment !== "") {
|
|
390
|
+
return this.resolveRef(uri, resource);
|
|
391
|
+
}
|
|
392
|
+
const node = this.documents.get(resource);
|
|
393
|
+
if (node === undefined)
|
|
394
|
+
throw new UnresolvableRefError(`unknown schema '${resource}'`);
|
|
395
|
+
return { node, baseUri: resource, pointer: "" };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolves a reference value against the referring schema's base URI.
|
|
400
|
+
* @throws UnresolvableRefError if the resource, anchor, or pointer target
|
|
401
|
+
* does not exist.
|
|
402
|
+
*/
|
|
403
|
+
resolveRef(ref: string, currentBase: string): SchemaRef {
|
|
404
|
+
const resolved = splitFragment(resolveUri(ref, currentBase));
|
|
405
|
+
const resource = this.canonical(resolved.resource);
|
|
406
|
+
const fragment = resolved.fragment;
|
|
407
|
+
|
|
408
|
+
if (fragment !== null && fragment !== "" && !fragment.startsWith("/")) {
|
|
409
|
+
const hit = this.anchors.get(`${resource}#${fragment}`);
|
|
410
|
+
if (!hit)
|
|
411
|
+
throw new UnresolvableRefError(
|
|
412
|
+
`unknown anchor '${resource}#${fragment}'`,
|
|
413
|
+
);
|
|
414
|
+
return hit;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const root = this.documents.get(resource);
|
|
418
|
+
if (root === undefined)
|
|
419
|
+
throw new UnresolvableRefError(`unknown schema '${resource}'`);
|
|
420
|
+
if (fragment === null || fragment === "") {
|
|
421
|
+
return { node: root, baseUri: resource, pointer: "" };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// JSON Pointer navigation, tracking identifier-induced base changes on
|
|
425
|
+
// the way, per the target document's dialect (D18).
|
|
426
|
+
const identifiers = this.dialectFor(resource).identifiers;
|
|
427
|
+
let node: JsonValue | undefined = root;
|
|
428
|
+
let baseUri = resource;
|
|
429
|
+
let pointer = "";
|
|
430
|
+
for (const rawSeg of fragment.slice(1).split("/")) {
|
|
431
|
+
const seg = unescapeSegment(rawSeg);
|
|
432
|
+
if (Array.isArray(node)) {
|
|
433
|
+
node = node[Number(seg)];
|
|
434
|
+
} else if (isObject(node) && Object.hasOwn(node, seg)) {
|
|
435
|
+
node = node[seg];
|
|
436
|
+
} else {
|
|
437
|
+
node = undefined;
|
|
438
|
+
}
|
|
439
|
+
if (node === undefined) {
|
|
440
|
+
throw new UnresolvableRefError(
|
|
441
|
+
`pointer '${fragment}' not found in '${resource}'`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
pointer += "/" + escapeSegment(seg);
|
|
445
|
+
if (isObject(node)) {
|
|
446
|
+
const baseId = identifiers(node).baseId;
|
|
447
|
+
if (baseId !== undefined) {
|
|
448
|
+
baseUri = splitFragment(resolveUri(baseId, baseUri)).resource;
|
|
449
|
+
pointer = "";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { node, baseUri, pointer };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Descend from a schema position into keyword/index children, maintaining
|
|
458
|
+
* canonical location and lexical base.
|
|
459
|
+
*/
|
|
460
|
+
child(ref: SchemaRef, segments: readonly (string | number)[]): SchemaRef {
|
|
461
|
+
const identifiers = this.dialectFor(ref.baseUri).identifiers;
|
|
462
|
+
let node: JsonValue = ref.node;
|
|
463
|
+
let { baseUri, pointer } = ref;
|
|
464
|
+
for (const seg of segments) {
|
|
465
|
+
node = (
|
|
466
|
+
Array.isArray(node)
|
|
467
|
+
? node[seg as number]
|
|
468
|
+
: (node as Record<string, JsonValue>)[seg as string]
|
|
469
|
+
) as JsonValue;
|
|
470
|
+
pointer += "/" + escapeSegment(String(seg));
|
|
471
|
+
if (isObject(node)) {
|
|
472
|
+
const baseId = identifiers(node).baseId;
|
|
473
|
+
if (baseId !== undefined) {
|
|
474
|
+
baseUri = splitFragment(resolveUri(baseId, baseUri)).resource;
|
|
475
|
+
pointer = "";
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return { node, baseUri, pointer };
|
|
480
|
+
}
|
|
481
|
+
}
|