@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/result.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Result assembly (ADR 0003): the option checks that admit or reject a
|
|
2
|
+
// combination of controls before any evaluation, and the assembly of a
|
|
3
|
+
// Result from the flat surface and the located tree. Both tiers use it —
|
|
4
|
+
// the interpreter after rendering its records, a compiled evaluator after
|
|
5
|
+
// recording its tree — so presence rules and key order have one owner.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type AnnotationSelection,
|
|
9
|
+
type AnnotationUnit,
|
|
10
|
+
type BasicOutputDocument,
|
|
11
|
+
type DetailedOutputUnit,
|
|
12
|
+
type ErrorUnit,
|
|
13
|
+
type IrrelevantRendering,
|
|
14
|
+
type ListOutputDocument,
|
|
15
|
+
type OutputUnit,
|
|
16
|
+
type RenderInput,
|
|
17
|
+
type RenderNode,
|
|
18
|
+
type TraceUnit,
|
|
19
|
+
renderBasic,
|
|
20
|
+
renderDetailed,
|
|
21
|
+
renderHierarchical,
|
|
22
|
+
renderList,
|
|
23
|
+
renderTrace,
|
|
24
|
+
renderVerbose,
|
|
25
|
+
} from "./output.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Output format names (ADR 0003), each fixing a document structure and field
|
|
29
|
+
* vocabulary: `flag`, `basic`, `detailed`, `verbose` from IETF draft-03 §13;
|
|
30
|
+
* `list`, `hierarchical` from the machines-oriented output proposal.
|
|
31
|
+
*/
|
|
32
|
+
export type OutputFormat =
|
|
33
|
+
"flag" | "basic" | "detailed" | "verbose" | "list" | "hierarchical";
|
|
34
|
+
|
|
35
|
+
const OUTPUT_FORMATS: ReadonlySet<string> = new Set<OutputFormat>([
|
|
36
|
+
"flag",
|
|
37
|
+
"basic",
|
|
38
|
+
"detailed",
|
|
39
|
+
"verbose",
|
|
40
|
+
"list",
|
|
41
|
+
"hierarchical",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** Thrown by `evaluate` for an unsupported combination of output options (ADR 0003: no silent no-ops). */
|
|
45
|
+
export class OutputOptionsError extends Error {}
|
|
46
|
+
|
|
47
|
+
/** Options for `Engine.evaluate`. */
|
|
48
|
+
export interface EvaluateOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Output format, by name; default `"flag"` (the minimal level: `valid`
|
|
51
|
+
* only). Every other name populates the flat `Result.errors`/
|
|
52
|
+
* `Result.annotations` surface and renders `Result.outputDocument` in the
|
|
53
|
+
* format's own structure ({@link OutputDocuments}).
|
|
54
|
+
*/
|
|
55
|
+
output?: OutputFormat;
|
|
56
|
+
/**
|
|
57
|
+
* Request the verbose level for `list` or `hierarchical`: every unit is
|
|
58
|
+
* kept and irrelevant records (draft-03 §12.2) render marked, as
|
|
59
|
+
* `droppedErrors`/`droppedAnnotations` in the document and as
|
|
60
|
+
* `Result.droppedErrors`/`Result.droppedAnnotations`. `basic` and
|
|
61
|
+
* `detailed` are relevant-level by definition and reject it; `verbose` is
|
|
62
|
+
* the verbose level by definition.
|
|
63
|
+
*/
|
|
64
|
+
verbose?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Which annotations reach output: `false` (default) none, `true` all, or
|
|
67
|
+
* an {@link AnnotationSelection}. Independent of format and level; never
|
|
68
|
+
* affects dependency data or validation.
|
|
69
|
+
*/
|
|
70
|
+
annotations?: boolean | AnnotationSelection;
|
|
71
|
+
/** Include `keyword`, `vocabulary`, and structured `params` on each flat error unit (D13). */
|
|
72
|
+
errorParams?: boolean;
|
|
73
|
+
/** Decorate flat units with schema-side source positions when available (D17). */
|
|
74
|
+
positions?: boolean;
|
|
75
|
+
/** Render the evaluation trace into `Result.trace`. */
|
|
76
|
+
trace?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The document type each non-minimal {@link OutputFormat} renders. */
|
|
80
|
+
export interface OutputDocuments {
|
|
81
|
+
basic: BasicOutputDocument;
|
|
82
|
+
detailed: DetailedOutputUnit;
|
|
83
|
+
verbose: DetailedOutputUnit;
|
|
84
|
+
list: ListOutputDocument;
|
|
85
|
+
hierarchical: OutputUnit;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The result of an evaluation. */
|
|
89
|
+
export interface Result {
|
|
90
|
+
valid: boolean;
|
|
91
|
+
/** relevant errors, native field names; present on an invalid result of any non-flag format */
|
|
92
|
+
errors?: ErrorUnit[];
|
|
93
|
+
/** relevant annotations per the selection; present on a valid result when annotations are selected */
|
|
94
|
+
annotations?: AnnotationUnit[];
|
|
95
|
+
/** irrelevant errors (draft-03 §12.2); present at the verbose level */
|
|
96
|
+
droppedErrors?: ErrorUnit[];
|
|
97
|
+
/** irrelevant annotations per the selection; present at the verbose level when annotations are selected */
|
|
98
|
+
droppedAnnotations?: AnnotationUnit[];
|
|
99
|
+
/** the output document, in the requested format's structure ({@link OutputDocuments}) */
|
|
100
|
+
outputDocument?: OutputDocuments[keyof OutputDocuments];
|
|
101
|
+
/** the evaluation trace, present with `trace: true`; `errorIndexes` reference `errors` on this result */
|
|
102
|
+
trace?: TraceUnit;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* {@link Result} narrowed by literal options: `outputDocument` takes the
|
|
107
|
+
* requested format's document type and `trace` is present when requested.
|
|
108
|
+
* A widened {@link EvaluateOptions} yields the plain `Result`.
|
|
109
|
+
*/
|
|
110
|
+
export type ResultFor<O extends EvaluateOptions> = Result &
|
|
111
|
+
([O["output"]] extends [infer F extends keyof OutputDocuments]
|
|
112
|
+
? { outputDocument: OutputDocuments[F] }
|
|
113
|
+
: unknown) &
|
|
114
|
+
([O["trace"]] extends [true] ? { trace: TraceUnit } : unknown);
|
|
115
|
+
|
|
116
|
+
/** What an evaluation must produce for a resolved set of output options. */
|
|
117
|
+
export interface OutputDemand {
|
|
118
|
+
format: OutputFormat;
|
|
119
|
+
/** the verbose level: irrelevant records are rendered, marked */
|
|
120
|
+
verbose: boolean;
|
|
121
|
+
/** the located tree is built: every format but flag and basic, or `trace: true` */
|
|
122
|
+
tracing: boolean;
|
|
123
|
+
annotations: boolean | AnnotationSelection;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Every combination of controls is supported or rejected here, before any
|
|
128
|
+
* evaluation (ADR 0003).
|
|
129
|
+
*/
|
|
130
|
+
export function resolveOutputDemand(options: EvaluateOptions): OutputDemand {
|
|
131
|
+
const format = options.output ?? "flag";
|
|
132
|
+
if (!OUTPUT_FORMATS.has(format)) {
|
|
133
|
+
throw new OutputOptionsError(`unknown output format '${format}'`);
|
|
134
|
+
}
|
|
135
|
+
const annotations = options.annotations ?? false;
|
|
136
|
+
if (format === "flag") {
|
|
137
|
+
const requested: [string, boolean][] = [
|
|
138
|
+
["verbose", options.verbose === true],
|
|
139
|
+
["annotations", annotations !== false],
|
|
140
|
+
["errorParams", options.errorParams === true],
|
|
141
|
+
["positions", options.positions === true],
|
|
142
|
+
["trace", options.trace === true],
|
|
143
|
+
];
|
|
144
|
+
for (const [name, on] of requested) {
|
|
145
|
+
if (on) {
|
|
146
|
+
throw new OutputOptionsError(
|
|
147
|
+
`'${name}' has no effect with output "flag", which carries no ` +
|
|
148
|
+
`records (the minimal level); choose "basic" or another format`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
options.verbose === true &&
|
|
155
|
+
(format === "basic" || format === "detailed")
|
|
156
|
+
) {
|
|
157
|
+
throw new OutputOptionsError(
|
|
158
|
+
`'verbose' does not apply to output "${format}", a relevant-level ` +
|
|
159
|
+
`format by definition (IETF draft-03 §13.4); use "verbose", or ` +
|
|
160
|
+
`"list"/"hierarchical" with verbose: true`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (options.verbose === false && format === "verbose") {
|
|
164
|
+
throw new OutputOptionsError(
|
|
165
|
+
`output "verbose" is the verbose level by definition; ` +
|
|
166
|
+
`'verbose: false' contradicts it`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
format,
|
|
171
|
+
verbose: format === "verbose" || options.verbose === true,
|
|
172
|
+
tracing:
|
|
173
|
+
format !== "flag" && (format !== "basic" || options.trace === true),
|
|
174
|
+
annotations,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The flat surface as one evaluation produced it. The arrays are placed on
|
|
180
|
+
* the {@link Result} as they are; `annotations` is empty on an invalid run
|
|
181
|
+
* or when none are selected, and the dropped pair is empty unless
|
|
182
|
+
* irrelevant records were retained (verbose demand).
|
|
183
|
+
*/
|
|
184
|
+
export interface ResultUnits {
|
|
185
|
+
errors: ErrorUnit[];
|
|
186
|
+
droppedErrors: ErrorUnit[];
|
|
187
|
+
annotations: AnnotationUnit[];
|
|
188
|
+
droppedAnnotations: AnnotationUnit[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Assembles a {@link Result} from the flat surface and, when the demand
|
|
193
|
+
* built one, the located tree. `root` is required whenever
|
|
194
|
+
* `demand.tracing`; `rootLocation` is the root schema's canonical location
|
|
195
|
+
* (the `basic` document's own).
|
|
196
|
+
*/
|
|
197
|
+
export function assembleResult(
|
|
198
|
+
demand: OutputDemand,
|
|
199
|
+
valid: boolean,
|
|
200
|
+
units: ResultUnits,
|
|
201
|
+
root: RenderNode | null,
|
|
202
|
+
rootLocation: string,
|
|
203
|
+
trace: boolean,
|
|
204
|
+
): Result {
|
|
205
|
+
const result: Result = { valid };
|
|
206
|
+
if (demand.format === "flag") return result;
|
|
207
|
+
if (!valid) result.errors = units.errors;
|
|
208
|
+
if (valid && demand.annotations !== false) {
|
|
209
|
+
result.annotations = units.annotations;
|
|
210
|
+
}
|
|
211
|
+
if (demand.verbose) {
|
|
212
|
+
result.droppedErrors = units.droppedErrors;
|
|
213
|
+
if (demand.annotations !== false) {
|
|
214
|
+
result.droppedAnnotations = units.droppedAnnotations;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const input: RenderInput | null = demand.tracing
|
|
218
|
+
? {
|
|
219
|
+
errors: units.errors,
|
|
220
|
+
droppedErrors: units.droppedErrors,
|
|
221
|
+
annotations: units.annotations,
|
|
222
|
+
droppedAnnotations: units.droppedAnnotations,
|
|
223
|
+
root: root!,
|
|
224
|
+
}
|
|
225
|
+
: null;
|
|
226
|
+
if (trace) result.trace = renderTrace(input!.root);
|
|
227
|
+
|
|
228
|
+
const irrelevant: IrrelevantRendering = demand.verbose ? "mark" : "omit";
|
|
229
|
+
switch (demand.format) {
|
|
230
|
+
case "basic":
|
|
231
|
+
result.outputDocument = renderBasic(
|
|
232
|
+
valid,
|
|
233
|
+
rootLocation,
|
|
234
|
+
units.errors,
|
|
235
|
+
units.annotations,
|
|
236
|
+
);
|
|
237
|
+
break;
|
|
238
|
+
case "list":
|
|
239
|
+
result.outputDocument = renderList(input!, irrelevant);
|
|
240
|
+
break;
|
|
241
|
+
case "hierarchical":
|
|
242
|
+
result.outputDocument = renderHierarchical(input!, irrelevant);
|
|
243
|
+
break;
|
|
244
|
+
case "detailed":
|
|
245
|
+
result.outputDocument = renderDetailed(input!);
|
|
246
|
+
break;
|
|
247
|
+
case "verbose":
|
|
248
|
+
result.outputDocument = renderVerbose(input!);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
package/src/uri.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// URI handling for schema identification and reference resolution.
|
|
2
|
+
|
|
3
|
+
/** Thrown when a reference cannot be resolved to an absolute URI. */
|
|
4
|
+
export class UnresolvableRefError extends Error {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a reference URI against a base URI to an absolute URI.
|
|
8
|
+
* @throws UnresolvableRefError if the pair does not form a valid URI.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveUri(ref: string, base: string): string {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(ref, base).href;
|
|
13
|
+
} catch {
|
|
14
|
+
throw new UnresolvableRefError(`cannot resolve '${ref}' against '${base}'`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A URI split into its resource part and decoded fragment. */
|
|
19
|
+
export interface SplitUri {
|
|
20
|
+
/** URI without fragment. */
|
|
21
|
+
resource: string;
|
|
22
|
+
/** Decoded fragment, `null` if absent. */
|
|
23
|
+
fragment: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Splits a URI into its resource and decoded fragment parts. */
|
|
27
|
+
export function splitFragment(uri: string): SplitUri {
|
|
28
|
+
const i = uri.indexOf("#");
|
|
29
|
+
return i === -1
|
|
30
|
+
? { resource: uri, fragment: null }
|
|
31
|
+
: {
|
|
32
|
+
resource: uri.slice(0, i),
|
|
33
|
+
fragment: decodeURIComponent(uri.slice(i + 1)),
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/walk.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Public schema-position walk (M8.6): the descent knowledge lives in each
|
|
2
|
+
// keyword's analyze() facts, so adapters and tools stop hardcoding their own
|
|
3
|
+
// applicator tables. Unlike the registry's registration walk (which owns
|
|
4
|
+
// base-URI rebasing, anchor indexing, and reference collection), this walk
|
|
5
|
+
// is deliberately tolerant: it runs on unvalidated input — e.g. a strict-
|
|
6
|
+
// mode check before registration — where a malformed keyword value must be
|
|
7
|
+
// skipped, not thrown. Registration keeps its InvalidSchemaError job.
|
|
8
|
+
|
|
9
|
+
import { JsonValue, isObject, escapeSegment } from "./json.js";
|
|
10
|
+
import { Dialect } from "./dialect.js";
|
|
11
|
+
import { DEFAULT_MAX_DEPTH, MaxDepthExceededError } from "./registry.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One schema position visited by {@link walkSchema}.
|
|
15
|
+
*/
|
|
16
|
+
export interface SchemaWalkVisit {
|
|
17
|
+
/** The value in schema position: an object or a boolean. */
|
|
18
|
+
readonly node: JsonValue;
|
|
19
|
+
/** JSON Pointer from the walk root. */
|
|
20
|
+
readonly pointer: string;
|
|
21
|
+
/** Applying keyword in the parent schema object; `null` at the root. */
|
|
22
|
+
readonly keyword: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Walks every schema position of `schema` reachable through the dialect's
|
|
27
|
+
* keyword facts (`analyze().subschemas`), visiting parents before children.
|
|
28
|
+
*
|
|
29
|
+
* Tolerant by design: values in schema position that are neither objects
|
|
30
|
+
* nor booleans are skipped, as are positions under keyword values whose
|
|
31
|
+
* `analyze()` rejects them — unknown keywords' values are never descended.
|
|
32
|
+
* Nesting beyond `maxDepth` still throws {@link MaxDepthExceededError}: the
|
|
33
|
+
* depth bound is a security posture, not a validity judgment.
|
|
34
|
+
*/
|
|
35
|
+
export function walkSchema(
|
|
36
|
+
schema: JsonValue,
|
|
37
|
+
dialect: Pick<Dialect, "keywords">,
|
|
38
|
+
visit: (v: SchemaWalkVisit) => void,
|
|
39
|
+
maxDepth: number = DEFAULT_MAX_DEPTH,
|
|
40
|
+
): void {
|
|
41
|
+
const step = (
|
|
42
|
+
node: JsonValue,
|
|
43
|
+
pointer: string,
|
|
44
|
+
keyword: string | null,
|
|
45
|
+
depth: number,
|
|
46
|
+
): void => {
|
|
47
|
+
if (depth > maxDepth) {
|
|
48
|
+
throw new MaxDepthExceededError(
|
|
49
|
+
`schema nesting exceeds maxDepth (${maxDepth}) at '#${pointer}'`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (typeof node === "boolean") {
|
|
53
|
+
visit({ node, pointer, keyword });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!isObject(node)) return;
|
|
57
|
+
visit({ node, pointer, keyword });
|
|
58
|
+
|
|
59
|
+
for (const [name, value] of Object.entries(node)) {
|
|
60
|
+
const behavior = dialect.keywords.get(name)?.behavior;
|
|
61
|
+
if (!behavior?.analyze) continue;
|
|
62
|
+
let positions;
|
|
63
|
+
try {
|
|
64
|
+
positions = behavior.analyze(value, { schema: node }).subschemas;
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!positions) continue;
|
|
69
|
+
for (const relPath of positions) {
|
|
70
|
+
let child: JsonValue | undefined = value;
|
|
71
|
+
let suffix = "/" + escapeSegment(name);
|
|
72
|
+
for (const seg of relPath) {
|
|
73
|
+
child =
|
|
74
|
+
child === null || typeof child !== "object"
|
|
75
|
+
? undefined
|
|
76
|
+
: Array.isArray(child)
|
|
77
|
+
? child[seg as number]
|
|
78
|
+
: (child as Record<string, JsonValue>)[seg as string];
|
|
79
|
+
suffix += "/" + escapeSegment(String(seg));
|
|
80
|
+
}
|
|
81
|
+
if (child !== undefined) {
|
|
82
|
+
step(child, pointer + suffix, name, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
step(schema, "", null, 0);
|
|
88
|
+
}
|