@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/output.ts
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// Output rendering (DESIGN.md D6; ADR 0003): the flat surface — units with
|
|
2
|
+
// the engine's native field names (evaluationPath, schemaLocation,
|
|
3
|
+
// inputLocation) — plus a located tree of schema applications, rendered by
|
|
4
|
+
// format name into each documented structure: IETF draft-03 §13 for
|
|
5
|
+
// basic/detailed/verbose, the machines-oriented output proposal for
|
|
6
|
+
// list/hierarchical. Everything here consumes strings and indexes, never
|
|
7
|
+
// engine records, so any producer of a RenderInput — the interpreter's
|
|
8
|
+
// trace (records.ts) or a compiled artifact — renders through one code path.
|
|
9
|
+
|
|
10
|
+
import { escapeSegment, unescapeSegment } from "./json.js";
|
|
11
|
+
import { ErrorParams } from "./dialect.js";
|
|
12
|
+
import type { KeywordTrace, RecordPredicate } from "./engine.js";
|
|
13
|
+
import { SourceLocation } from "./loader.js";
|
|
14
|
+
|
|
15
|
+
/** One rendered assertion failure, native field names. */
|
|
16
|
+
export interface ErrorUnit {
|
|
17
|
+
evaluationPath: string;
|
|
18
|
+
schemaLocation: string;
|
|
19
|
+
inputLocation: string;
|
|
20
|
+
error: string;
|
|
21
|
+
/**
|
|
22
|
+
* Failing keyword name, present with the `errorParams` option; absent when
|
|
23
|
+
* the schema itself was boolean `false`.
|
|
24
|
+
*/
|
|
25
|
+
keyword?: string;
|
|
26
|
+
/** The keyword's vocabulary URI, present with `errorParams` when known. */
|
|
27
|
+
vocabulary?: string;
|
|
28
|
+
/** Structured failure data (D13), present with `errorParams`. */
|
|
29
|
+
params?: ErrorParams;
|
|
30
|
+
/** Schema-side source position, present with the `positions` option (D17). */
|
|
31
|
+
source?: SourceLocation;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One rendered annotation, native field names. */
|
|
35
|
+
export interface AnnotationUnit {
|
|
36
|
+
keyword: string;
|
|
37
|
+
/** The keyword's vocabulary URI, when known. */
|
|
38
|
+
vocabulary?: string;
|
|
39
|
+
evaluationPath: string;
|
|
40
|
+
schemaLocation: string;
|
|
41
|
+
inputLocation: string;
|
|
42
|
+
annotation: unknown;
|
|
43
|
+
/** Schema-side source position, present with the `positions` option (D17). */
|
|
44
|
+
source?: SourceLocation;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Which annotations reach output (ADR 0003): a control independent of the
|
|
49
|
+
* output format and level. The allow-lists are OR-ed, the deny-lists
|
|
50
|
+
* subtract after them, and `keep` runs last on the rendered native unit —
|
|
51
|
+
* the very unit the flat surface then carries.
|
|
52
|
+
*/
|
|
53
|
+
export interface AnnotationSelection {
|
|
54
|
+
/** allow-list of keyword names; an empty array selects nothing */
|
|
55
|
+
keywords?: readonly string[];
|
|
56
|
+
/** allow-list of vocabulary URIs (OR-ed with `keywords`) */
|
|
57
|
+
vocabularies?: readonly string[];
|
|
58
|
+
/** deny-list of keyword names, subtracted after the allow-lists */
|
|
59
|
+
excludeKeywords?: readonly string[];
|
|
60
|
+
/** deny-list of vocabulary URIs, subtracted after the allow-lists */
|
|
61
|
+
excludeVocabularies?: readonly string[];
|
|
62
|
+
/** arbitrary predicate over the rendered unit, AND-ed after the lists */
|
|
63
|
+
keep?: (unit: AnnotationUnit) => boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Selection applies to annotation records only (§4 rule 5); dependency
|
|
67
|
+
// records live in a separate store, so no selection can hide one from
|
|
68
|
+
// ctx.visible().
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Annotation recording decision (D5): record iff the selection might render
|
|
72
|
+
* the annotation — `false` records nothing, `true` everything, and a
|
|
73
|
+
* selection object's allow/deny lists rule keywords out. The `keep`
|
|
74
|
+
* predicate runs only at render: recording a superset of what it keeps is
|
|
75
|
+
* correct, eliding on its behalf would not be. This is also the list stage
|
|
76
|
+
* of the interpreter's render-time selection (records.ts), so the two can
|
|
77
|
+
* never disagree. It applies at every output level: a deselected keyword's
|
|
78
|
+
* annotation is never rendered, relevant or not.
|
|
79
|
+
*/
|
|
80
|
+
export function makeRecordPredicate(
|
|
81
|
+
selection: boolean | AnnotationSelection,
|
|
82
|
+
): RecordPredicate {
|
|
83
|
+
if (selection === false) return () => false;
|
|
84
|
+
if (selection === true) return () => true;
|
|
85
|
+
const allow =
|
|
86
|
+
selection.keywords !== undefined || selection.vocabularies !== undefined
|
|
87
|
+
? {
|
|
88
|
+
names: new Set(selection.keywords ?? []),
|
|
89
|
+
vocabs: new Set(selection.vocabularies ?? []),
|
|
90
|
+
}
|
|
91
|
+
: null;
|
|
92
|
+
const denyNames = new Set(selection.excludeKeywords ?? []);
|
|
93
|
+
const denyVocabs = new Set(selection.excludeVocabularies ?? []);
|
|
94
|
+
return (_behaviorId, keywordName, vocabularyUri) => {
|
|
95
|
+
if (
|
|
96
|
+
allow !== null &&
|
|
97
|
+
!allow.names.has(keywordName) &&
|
|
98
|
+
!(vocabularyUri !== null && allow.vocabs.has(vocabularyUri))
|
|
99
|
+
) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return (
|
|
103
|
+
!denyNames.has(keywordName) &&
|
|
104
|
+
!(vocabularyUri !== null && denyVocabs.has(vocabularyUri))
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One schema application in a located evaluation tree: what the document
|
|
111
|
+
* renderers need from any producer, the interpreter's trace or a compiled
|
|
112
|
+
* artifact. Locations are absolute strings; records are indexes into the
|
|
113
|
+
* owning {@link RenderInput}'s flat arrays.
|
|
114
|
+
*
|
|
115
|
+
* Producers keep three invariants: a child's `evaluationPath` equals or
|
|
116
|
+
* extends its parent's; a unit indexed at a node has the node's
|
|
117
|
+
* `evaluationPath` (a boolean `false` schema) or extends it by exactly one
|
|
118
|
+
* escaped segment, the keyword; `keywords` lists the node's non-structural
|
|
119
|
+
* keyword evaluations in evaluation order.
|
|
120
|
+
*/
|
|
121
|
+
export interface RenderNode {
|
|
122
|
+
/** Escaped JSON Pointer of the evaluation path; `""` at the root. */
|
|
123
|
+
readonly evaluationPath: string;
|
|
124
|
+
/** Canonical schema location of the applied subschema: `baseUri#pointer`. */
|
|
125
|
+
readonly schemaLocation: string;
|
|
126
|
+
/** JSON Pointer of the input position this application evaluated. */
|
|
127
|
+
readonly inputLocation: string;
|
|
128
|
+
readonly valid: boolean;
|
|
129
|
+
readonly keywords: readonly KeywordTrace[];
|
|
130
|
+
/** Indexes into {@link RenderInput.errors} of the errors raised here. */
|
|
131
|
+
readonly errors: readonly number[];
|
|
132
|
+
/** Indexes into {@link RenderInput.droppedErrors}. */
|
|
133
|
+
readonly droppedErrors: readonly number[];
|
|
134
|
+
/** Indexes into {@link RenderInput.annotations}. */
|
|
135
|
+
readonly annotations: readonly number[];
|
|
136
|
+
/** Indexes into {@link RenderInput.droppedAnnotations}. */
|
|
137
|
+
readonly droppedAnnotations: readonly number[];
|
|
138
|
+
/** Nested applications, in evaluation order. */
|
|
139
|
+
readonly children: readonly RenderNode[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The flat surface plus its located tree: the input of every document
|
|
144
|
+
* renderer. `droppedErrors` and `droppedAnnotations` are empty unless the
|
|
145
|
+
* producer retained irrelevant records (verbose demand); relevant-level
|
|
146
|
+
* renderings never read them.
|
|
147
|
+
*/
|
|
148
|
+
export interface RenderInput {
|
|
149
|
+
/** relevant errors, encounter order (`Result.errors`) */
|
|
150
|
+
readonly errors: readonly ErrorUnit[];
|
|
151
|
+
/** errors of rejecting sub-evaluations under an accepting keyword */
|
|
152
|
+
readonly droppedErrors: readonly ErrorUnit[];
|
|
153
|
+
/** relevant annotations, already selected (`Result.annotations`) */
|
|
154
|
+
readonly annotations: readonly AnnotationUnit[];
|
|
155
|
+
/** irrelevant annotations, already selected */
|
|
156
|
+
readonly droppedAnnotations: readonly AnnotationUnit[];
|
|
157
|
+
readonly root: RenderNode;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The index list of a node without records, shared rather than allocated. */
|
|
161
|
+
export const NO_INDEXES: readonly number[] = [];
|
|
162
|
+
|
|
163
|
+
// A unit at a node carries the node's path plus at most one keyword segment
|
|
164
|
+
// (RenderNode's invariant); the keyword name is that segment, decoded, and a
|
|
165
|
+
// boolean-`false` schema's error, located at the node itself, keys as "".
|
|
166
|
+
const keywordOf = (unit: ErrorUnit, node: RenderNode): string => {
|
|
167
|
+
const rest = unit.evaluationPath.slice(node.evaluationPath.length);
|
|
168
|
+
return rest === "" ? "" : unescapeSegment(rest.slice(1));
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// The evaluation-path segments of `child` below `parentPath`, decoded.
|
|
172
|
+
const segmentsBelow = (child: RenderNode, parentPath: string): string[] => {
|
|
173
|
+
const rest = child.evaluationPath.slice(parentPath.length);
|
|
174
|
+
return rest === "" ? [] : rest.slice(1).split("/").map(unescapeSegment);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// The first of those segments, the applying keyword, without building the
|
|
178
|
+
// list: the draft-03 walk asks this once per child application.
|
|
179
|
+
const firstSegmentBelow = (
|
|
180
|
+
child: RenderNode,
|
|
181
|
+
parentPath: string,
|
|
182
|
+
): string | null => {
|
|
183
|
+
const path = child.evaluationPath;
|
|
184
|
+
if (path.length === parentPath.length) return null;
|
|
185
|
+
const end = path.indexOf("/", parentPath.length + 1);
|
|
186
|
+
return unescapeSegment(
|
|
187
|
+
path.slice(parentPath.length + 1, end === -1 ? undefined : end),
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Documents project unit fields into fresh objects and never embed a unit:
|
|
192
|
+
// the flat units are decorated (`positions`) after every document is built,
|
|
193
|
+
// and their `errorParams` fields belong to the flat surface only. Most
|
|
194
|
+
// nodes carry no records, so an empty pick allocates nothing.
|
|
195
|
+
const NO_UNITS: readonly never[] = [];
|
|
196
|
+
const pick = <T>(
|
|
197
|
+
indexes: readonly number[],
|
|
198
|
+
units: readonly T[],
|
|
199
|
+
): readonly T[] =>
|
|
200
|
+
indexes.length === 0 ? NO_UNITS : indexes.map((i) => units[i]!);
|
|
201
|
+
|
|
202
|
+
// Relevant records followed by dropped ones; a node's records are uniformly
|
|
203
|
+
// one or the other, so the concatenation is reached only by a custom
|
|
204
|
+
// keyword sharing its parent's node.
|
|
205
|
+
const withDropped = <T>(
|
|
206
|
+
relevant: readonly T[],
|
|
207
|
+
dropped: readonly T[],
|
|
208
|
+
): readonly T[] =>
|
|
209
|
+
relevant.length === 0
|
|
210
|
+
? dropped
|
|
211
|
+
: dropped.length === 0
|
|
212
|
+
? relevant
|
|
213
|
+
: [...relevant, ...dropped];
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Output unit of the machines-oriented proposal (`list`, `hierarchical`):
|
|
217
|
+
* errors and annotations keyed by keyword name, nested results under
|
|
218
|
+
* `details`. At the verbose level `droppedErrors`/`droppedAnnotations` mark
|
|
219
|
+
* irrelevant records (draft-03 §12.2): the proposal defines
|
|
220
|
+
* `droppedAnnotations` for a failed unit's own annotations, and the verbose
|
|
221
|
+
* level extends the marker to every irrelevant record.
|
|
222
|
+
*/
|
|
223
|
+
export interface OutputUnit {
|
|
224
|
+
valid: boolean;
|
|
225
|
+
evaluationPath: string;
|
|
226
|
+
schemaLocation: string;
|
|
227
|
+
instanceLocation: string;
|
|
228
|
+
errors?: Record<string, string>;
|
|
229
|
+
annotations?: Record<string, unknown>;
|
|
230
|
+
droppedErrors?: Record<string, string>;
|
|
231
|
+
droppedAnnotations?: Record<string, unknown>;
|
|
232
|
+
details?: OutputUnit[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The `list` document: a root unit carrying `valid` and the flat `details` list. */
|
|
236
|
+
export interface ListOutputDocument {
|
|
237
|
+
valid: boolean;
|
|
238
|
+
details: OutputUnit[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* How a `list`/`hierarchical` renderer treats irrelevant records: "omit"
|
|
243
|
+
* drops them and prunes the units left empty (the relevant level); "mark"
|
|
244
|
+
* keeps every unit and renders them under `droppedErrors`/
|
|
245
|
+
* `droppedAnnotations` (the verbose level).
|
|
246
|
+
*/
|
|
247
|
+
export type IrrelevantRendering = "omit" | "mark";
|
|
248
|
+
|
|
249
|
+
// A keyword may report several errors (required's missing names); a
|
|
250
|
+
// one-message-per-keyword field joins them.
|
|
251
|
+
const joinMessages = (errs: readonly ErrorUnit[]): string =>
|
|
252
|
+
errs.map((e) => e.error).join("; ");
|
|
253
|
+
|
|
254
|
+
// The by-keyword records are keyed by names the schema chooses, and the
|
|
255
|
+
// documents stay ordinary objects (consumers compare them as plain JSON),
|
|
256
|
+
// so the one name an ordinary object already answers to — "__proto__",
|
|
257
|
+
// an accessor on Object.prototype — has to be defined as an own property:
|
|
258
|
+
// assigned, a primitive would be discarded and an object would replace the
|
|
259
|
+
// record's prototype with schema-derived data.
|
|
260
|
+
function setKeyed<T>(record: Record<string, T>, key: string, value: T): void {
|
|
261
|
+
if (key === "__proto__") {
|
|
262
|
+
Object.defineProperty(record, key, {
|
|
263
|
+
value,
|
|
264
|
+
enumerable: true,
|
|
265
|
+
writable: true,
|
|
266
|
+
configurable: true,
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
record[key] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function errorsByKeyword(
|
|
274
|
+
errs: readonly ErrorUnit[],
|
|
275
|
+
node: RenderNode,
|
|
276
|
+
): Record<string, string> {
|
|
277
|
+
const byKeyword: Record<string, string> = {};
|
|
278
|
+
for (const e of errs) {
|
|
279
|
+
const key = keywordOf(e, node);
|
|
280
|
+
const prior = Object.hasOwn(byKeyword, key) ? byKeyword[key] : undefined;
|
|
281
|
+
setKeyed(
|
|
282
|
+
byKeyword,
|
|
283
|
+
key,
|
|
284
|
+
prior === undefined ? e.error : `${prior}; ${e.error}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return byKeyword;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function annotationsByKeyword(
|
|
291
|
+
anns: readonly AnnotationUnit[],
|
|
292
|
+
): Record<string, unknown> {
|
|
293
|
+
const byKeyword: Record<string, unknown> = {};
|
|
294
|
+
for (const a of anns) setKeyed(byKeyword, a.keyword, a.annotation);
|
|
295
|
+
return byKeyword;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* HIERARCHICAL structure over the located tree. Units carry errors and
|
|
300
|
+
* annotations keyed by keyword name. Irrelevant records render per
|
|
301
|
+
* {@link IrrelevantRendering}; the root unit always remains.
|
|
302
|
+
*/
|
|
303
|
+
export function renderHierarchical(
|
|
304
|
+
input: RenderInput,
|
|
305
|
+
irrelevant: IrrelevantRendering,
|
|
306
|
+
): OutputUnit {
|
|
307
|
+
const unitOf = (node: RenderNode): OutputUnit => ({
|
|
308
|
+
valid: node.valid,
|
|
309
|
+
evaluationPath: node.evaluationPath,
|
|
310
|
+
schemaLocation: node.schemaLocation,
|
|
311
|
+
instanceLocation: node.inputLocation,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const toUnit = (node: RenderNode): OutputUnit | undefined => {
|
|
315
|
+
const details = node.children
|
|
316
|
+
.map(toUnit)
|
|
317
|
+
.filter((u): u is OutputUnit => u !== undefined);
|
|
318
|
+
|
|
319
|
+
const unit = unitOf(node);
|
|
320
|
+
|
|
321
|
+
const errs = pick(node.errors, input.errors);
|
|
322
|
+
if (errs.length > 0) unit.errors = errorsByKeyword(errs, node);
|
|
323
|
+
if (irrelevant === "mark") {
|
|
324
|
+
const dropped = pick(node.droppedErrors, input.droppedErrors);
|
|
325
|
+
if (dropped.length > 0)
|
|
326
|
+
unit.droppedErrors = errorsByKeyword(dropped, node);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const anns = pick(node.annotations, input.annotations);
|
|
330
|
+
if (anns.length > 0) unit.annotations = annotationsByKeyword(anns);
|
|
331
|
+
if (irrelevant === "mark") {
|
|
332
|
+
const dropped = pick(node.droppedAnnotations, input.droppedAnnotations);
|
|
333
|
+
if (dropped.length > 0) {
|
|
334
|
+
unit.droppedAnnotations = annotationsByKeyword(dropped);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (details.length > 0) unit.details = details;
|
|
339
|
+
|
|
340
|
+
// Relevant level (§13.4): a unit carrying nothing relevant is omitted.
|
|
341
|
+
if (
|
|
342
|
+
irrelevant === "omit" &&
|
|
343
|
+
unit.errors === undefined &&
|
|
344
|
+
unit.annotations === undefined &&
|
|
345
|
+
unit.details === undefined
|
|
346
|
+
) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
return unit;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
return toUnit(input.root) ?? unitOf(input.root);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* LIST structure over the located tree: the same per-application units as
|
|
357
|
+
* HIERARCHICAL, flattened under a root unit that carries only `valid` and
|
|
358
|
+
* `details`. At the relevant level only units that report an error or an
|
|
359
|
+
* annotation appear (the proposal's SHOULD); the verbose level includes
|
|
360
|
+
* every unit.
|
|
361
|
+
*/
|
|
362
|
+
export function renderList(
|
|
363
|
+
input: RenderInput,
|
|
364
|
+
irrelevant: IrrelevantRendering,
|
|
365
|
+
): ListOutputDocument {
|
|
366
|
+
const nested = renderHierarchical(input, irrelevant);
|
|
367
|
+
const details: OutputUnit[] = [];
|
|
368
|
+
const collect = (unit: OutputUnit): void => {
|
|
369
|
+
const { details: children, ...rest } = unit;
|
|
370
|
+
if (
|
|
371
|
+
irrelevant === "mark" ||
|
|
372
|
+
rest.errors !== undefined ||
|
|
373
|
+
rest.annotations !== undefined
|
|
374
|
+
) {
|
|
375
|
+
details.push(rest);
|
|
376
|
+
}
|
|
377
|
+
children?.forEach(collect);
|
|
378
|
+
};
|
|
379
|
+
collect(nested);
|
|
380
|
+
return { valid: input.root.valid, details };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Output unit of IETF draft-03 §13.3 for the `detailed` and `verbose`
|
|
385
|
+
* structures: one node per keyword evaluation or schema application, with a
|
|
386
|
+
* local `error`/`annotation` and nested results under `errors` (failed
|
|
387
|
+
* node) or `annotations` (successful node).
|
|
388
|
+
*/
|
|
389
|
+
export interface DetailedOutputUnit {
|
|
390
|
+
valid: boolean;
|
|
391
|
+
keywordLocation: string;
|
|
392
|
+
absoluteKeywordLocation: string;
|
|
393
|
+
instanceLocation: string;
|
|
394
|
+
error?: string;
|
|
395
|
+
annotation?: unknown;
|
|
396
|
+
errors?: DetailedOutputUnit[];
|
|
397
|
+
annotations?: DetailedOutputUnit[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function attachNested(
|
|
401
|
+
unit: DetailedOutputUnit,
|
|
402
|
+
nested: readonly DetailedOutputUnit[],
|
|
403
|
+
): void {
|
|
404
|
+
if (nested.length === 0) return;
|
|
405
|
+
// §13.3.5: nested results key on the node's own result.
|
|
406
|
+
if (unit.valid) unit.annotations = [...nested];
|
|
407
|
+
else unit.errors = [...nested];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const nestedOf = (unit: DetailedOutputUnit): DetailedOutputUnit[] =>
|
|
411
|
+
unit.errors ?? unit.annotations ?? [];
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The draft-03 keyword-level tree (§13.4.3–13.4.4). Every schema application
|
|
415
|
+
* becomes a node whose children are one node per keyword evaluation, in
|
|
416
|
+
* evaluation order; each keyword node carries the keyword's own error or
|
|
417
|
+
* annotation and the applications it performed. At the relevant level only
|
|
418
|
+
* relevant records appear; the verbose level includes every record and
|
|
419
|
+
* relies on `valid` per node as the relevance marker.
|
|
420
|
+
*/
|
|
421
|
+
function buildDraft03Tree(
|
|
422
|
+
input: RenderInput,
|
|
423
|
+
level: "relevant" | "verbose",
|
|
424
|
+
): DetailedOutputUnit {
|
|
425
|
+
const build = (node: RenderNode): DetailedOutputUnit => {
|
|
426
|
+
const {
|
|
427
|
+
evaluationPath: keywordLocation,
|
|
428
|
+
schemaLocation: absoluteKeywordLocation,
|
|
429
|
+
inputLocation: instanceLocation,
|
|
430
|
+
} = node;
|
|
431
|
+
const unit: DetailedOutputUnit = {
|
|
432
|
+
valid: node.valid,
|
|
433
|
+
keywordLocation,
|
|
434
|
+
absoluteKeywordLocation,
|
|
435
|
+
instanceLocation,
|
|
436
|
+
};
|
|
437
|
+
const errs =
|
|
438
|
+
level === "verbose"
|
|
439
|
+
? withDropped(
|
|
440
|
+
pick(node.errors, input.errors),
|
|
441
|
+
pick(node.droppedErrors, input.droppedErrors),
|
|
442
|
+
)
|
|
443
|
+
: pick(node.errors, input.errors);
|
|
444
|
+
const anns =
|
|
445
|
+
level === "verbose"
|
|
446
|
+
? withDropped(
|
|
447
|
+
pick(node.annotations, input.annotations),
|
|
448
|
+
pick(node.droppedAnnotations, input.droppedAnnotations),
|
|
449
|
+
)
|
|
450
|
+
: pick(node.annotations, input.annotations);
|
|
451
|
+
// A boolean `false` schema's error belongs to the application itself.
|
|
452
|
+
const own = errs.filter((e) => e.evaluationPath === keywordLocation);
|
|
453
|
+
if (own.length > 0) unit.error = joinMessages(own);
|
|
454
|
+
|
|
455
|
+
// The first evaluation-path segment of a child application below its
|
|
456
|
+
// parent names the applying keyword.
|
|
457
|
+
const childrenOf = new Map<string | null, RenderNode[]>();
|
|
458
|
+
for (const child of node.children) {
|
|
459
|
+
const key = firstSegmentBelow(child, keywordLocation);
|
|
460
|
+
const list = childrenOf.get(key);
|
|
461
|
+
if (list) list.push(child);
|
|
462
|
+
else childrenOf.set(key, [child]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const nested: DetailedOutputUnit[] = [];
|
|
466
|
+
for (const k of node.keywords) {
|
|
467
|
+
const suffix = "/" + escapeSegment(k.name);
|
|
468
|
+
const kwLocation = keywordLocation + suffix;
|
|
469
|
+
const kwUnit: DetailedOutputUnit = {
|
|
470
|
+
valid: k.valid,
|
|
471
|
+
keywordLocation: kwLocation,
|
|
472
|
+
absoluteKeywordLocation: absoluteKeywordLocation + suffix,
|
|
473
|
+
instanceLocation,
|
|
474
|
+
};
|
|
475
|
+
const kwErrs = errs.filter((e) => e.evaluationPath === kwLocation);
|
|
476
|
+
if (kwErrs.length > 0) kwUnit.error = joinMessages(kwErrs);
|
|
477
|
+
const kwAnn = anns.find((a) => a.keyword === k.name);
|
|
478
|
+
if (kwAnn !== undefined) kwUnit.annotation = kwAnn.annotation;
|
|
479
|
+
const applied = childrenOf.get(k.name);
|
|
480
|
+
if (applied !== undefined) {
|
|
481
|
+
childrenOf.delete(k.name);
|
|
482
|
+
attachNested(kwUnit, applied.map(build));
|
|
483
|
+
}
|
|
484
|
+
nested.push(kwUnit);
|
|
485
|
+
}
|
|
486
|
+
// Applications not attributable to a keyword entry (a custom keyword
|
|
487
|
+
// applying with no segment of its own) stay under the application node,
|
|
488
|
+
// appended one by one: a stray list holds one entry per application, and
|
|
489
|
+
// an argument spread over it has a native-stack ceiling.
|
|
490
|
+
for (const stray of childrenOf.values()) {
|
|
491
|
+
for (const child of stray) nested.push(build(child));
|
|
492
|
+
}
|
|
493
|
+
attachNested(unit, nested);
|
|
494
|
+
return unit;
|
|
495
|
+
};
|
|
496
|
+
return build(input.root);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Detailed condensation (§13.4.3): nodes with no children are removed, nodes
|
|
500
|
+
// with a single child are replaced by the child; the root always remains.
|
|
501
|
+
function condense(
|
|
502
|
+
unit: DetailedOutputUnit,
|
|
503
|
+
isRoot: boolean,
|
|
504
|
+
): DetailedOutputUnit | undefined {
|
|
505
|
+
const nested = nestedOf(unit)
|
|
506
|
+
.map((n) => condense(n, false))
|
|
507
|
+
.filter((n): n is DetailedOutputUnit => n !== undefined);
|
|
508
|
+
const local = unit.error !== undefined || unit.annotation !== undefined;
|
|
509
|
+
if (!local && !isRoot) {
|
|
510
|
+
if (nested.length === 0) return undefined;
|
|
511
|
+
if (nested.length === 1) return nested[0];
|
|
512
|
+
}
|
|
513
|
+
const out: DetailedOutputUnit = {
|
|
514
|
+
valid: unit.valid,
|
|
515
|
+
keywordLocation: unit.keywordLocation,
|
|
516
|
+
absoluteKeywordLocation: unit.absoluteKeywordLocation,
|
|
517
|
+
instanceLocation: unit.instanceLocation,
|
|
518
|
+
};
|
|
519
|
+
if (unit.error !== undefined) out.error = unit.error;
|
|
520
|
+
if (unit.annotation !== undefined) out.annotation = unit.annotation;
|
|
521
|
+
attachNested(out, nested);
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Detailed output document (IETF draft-03 §13.4.3): the condensed keyword-level tree of relevant results. */
|
|
526
|
+
export function renderDetailed(input: RenderInput): DetailedOutputUnit {
|
|
527
|
+
return condense(buildDraft03Tree(input, "relevant"), true)!;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Verbose output document (IETF draft-03 §13.4.4): the full keyword-level tree, irrelevant results included. */
|
|
531
|
+
export function renderVerbose(input: RenderInput): DetailedOutputUnit {
|
|
532
|
+
return buildDraft03Tree(input, "verbose");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* One schema application from a traced evaluation.
|
|
537
|
+
*
|
|
538
|
+
* The tree mirrors the evaluation exactly, including applications inside
|
|
539
|
+
* subtrees that ultimately passed — adapters need those to reconstruct
|
|
540
|
+
* application context (e.g. which `anyOf` branches an error competed
|
|
541
|
+
* against) without parsing location strings.
|
|
542
|
+
*/
|
|
543
|
+
export interface TraceUnit {
|
|
544
|
+
/**
|
|
545
|
+
* Evaluation-path segments from the parent application, decoded (no JSON
|
|
546
|
+
* Pointer escaping). The first segment is the applying keyword
|
|
547
|
+
* (`"anyOf"`, `"properties"`, `"$ref"`, ...); any following segments are
|
|
548
|
+
* branch indexes or property/definition names. Empty at the root.
|
|
549
|
+
*/
|
|
550
|
+
readonly segments: readonly string[];
|
|
551
|
+
/** Canonical schema location of the applied subschema: `baseUri#pointer`. */
|
|
552
|
+
readonly schemaLocation: string;
|
|
553
|
+
/** JSON Pointer of the input position this application evaluated. */
|
|
554
|
+
readonly inputLocation: string;
|
|
555
|
+
readonly valid: boolean;
|
|
556
|
+
/**
|
|
557
|
+
* Indices into `Result.errors` (same run) of the relevant errors raised
|
|
558
|
+
* directly at this application; a rejecting application under an
|
|
559
|
+
* accepting keyword has none (draft-03 §12.2). Populated only when the
|
|
560
|
+
* evaluation failed — `Result.errors` does not exist for a valid result.
|
|
561
|
+
*/
|
|
562
|
+
readonly errorIndexes: readonly number[];
|
|
563
|
+
/** Nested applications, in evaluation order. */
|
|
564
|
+
readonly children: readonly TraceUnit[];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Renders the located tree into the public trace. Error correlation is
|
|
569
|
+
* positional: a node's `errors` indexes are its `errorIndexes`, and the
|
|
570
|
+
* public tree carries decoded segments so consumers never touch pointer
|
|
571
|
+
* escaping.
|
|
572
|
+
*/
|
|
573
|
+
export function renderTrace(root: RenderNode): TraceUnit {
|
|
574
|
+
const toUnit = (node: RenderNode, parentPath: string): TraceUnit => ({
|
|
575
|
+
segments: segmentsBelow(node, parentPath),
|
|
576
|
+
schemaLocation: node.schemaLocation,
|
|
577
|
+
inputLocation: node.inputLocation,
|
|
578
|
+
valid: node.valid,
|
|
579
|
+
errorIndexes: node.errors,
|
|
580
|
+
children: node.children.map((c) => toUnit(c, node.evaluationPath)),
|
|
581
|
+
});
|
|
582
|
+
return toUnit(root, "");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** One error of the Basic document (IETF draft-03 §13.4.2). */
|
|
586
|
+
export interface BasicErrorUnit {
|
|
587
|
+
keywordLocation: string;
|
|
588
|
+
absoluteKeywordLocation: string;
|
|
589
|
+
instanceLocation: string;
|
|
590
|
+
error: string;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** One annotation of the Basic document (IETF draft-03 §13.4.2). */
|
|
594
|
+
export interface BasicAnnotationUnit {
|
|
595
|
+
keywordLocation: string;
|
|
596
|
+
absoluteKeywordLocation: string;
|
|
597
|
+
instanceLocation: string;
|
|
598
|
+
annotation: unknown;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Basic output document (IETF draft-03 §13.4.2): a root unit with a flat
|
|
603
|
+
* `errors` array on failure or `annotations` array on success, one unit per
|
|
604
|
+
* record, each located at its keyword.
|
|
605
|
+
*/
|
|
606
|
+
export interface BasicOutputDocument {
|
|
607
|
+
valid: boolean;
|
|
608
|
+
keywordLocation: string;
|
|
609
|
+
absoluteKeywordLocation: string;
|
|
610
|
+
instanceLocation: string;
|
|
611
|
+
errors?: BasicErrorUnit[];
|
|
612
|
+
annotations?: BasicAnnotationUnit[];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Basic output document from the flat surface: `rootLocation` is the root
|
|
617
|
+
* schema's canonical location, `errors` and `annotations` the relevant,
|
|
618
|
+
* already selected units. Per the suite's output-tests basic fixtures,
|
|
619
|
+
* `errors` is absent on success; `annotations` appears only when non-empty.
|
|
620
|
+
*/
|
|
621
|
+
export function renderBasic(
|
|
622
|
+
valid: boolean,
|
|
623
|
+
rootLocation: string,
|
|
624
|
+
errors: readonly ErrorUnit[],
|
|
625
|
+
annotations: readonly AnnotationUnit[],
|
|
626
|
+
): BasicOutputDocument {
|
|
627
|
+
const doc: BasicOutputDocument = {
|
|
628
|
+
valid,
|
|
629
|
+
keywordLocation: "",
|
|
630
|
+
absoluteKeywordLocation: rootLocation,
|
|
631
|
+
instanceLocation: "",
|
|
632
|
+
};
|
|
633
|
+
if (valid) {
|
|
634
|
+
if (annotations.length > 0) {
|
|
635
|
+
doc.annotations = annotations.map((a): BasicAnnotationUnit => ({
|
|
636
|
+
keywordLocation: a.evaluationPath,
|
|
637
|
+
absoluteKeywordLocation: a.schemaLocation,
|
|
638
|
+
instanceLocation: a.inputLocation,
|
|
639
|
+
annotation: a.annotation,
|
|
640
|
+
}));
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
doc.errors = errors.map((e): BasicErrorUnit => ({
|
|
644
|
+
keywordLocation: e.evaluationPath,
|
|
645
|
+
absoluteKeywordLocation: e.schemaLocation,
|
|
646
|
+
instanceLocation: e.inputLocation,
|
|
647
|
+
error: e.error,
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
return doc;
|
|
651
|
+
}
|