@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/index.ts
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
// @json-schema-engine/core public API: sync evaluation over registered schemas (D7); the
|
|
2
|
+
// 2020-12 dialect preloaded; custom vocabularies/dialects via the same
|
|
3
|
+
// registry the built-ins use. Resource I/O is the one async boundary:
|
|
4
|
+
// load/loadSchema pull in referenced resources through caller-supplied
|
|
5
|
+
// loaders and assemble dialects from metaschema $vocabulary declarations.
|
|
6
|
+
|
|
7
|
+
import { JsonValue, isObject } from "./json.js";
|
|
8
|
+
import { resolveUri, splitFragment, UnresolvableRefError } from "./uri.js";
|
|
9
|
+
import {
|
|
10
|
+
DialectRegistry,
|
|
11
|
+
DialectOptions,
|
|
12
|
+
KeywordBehavior,
|
|
13
|
+
UnknownDialectError,
|
|
14
|
+
UnknownVocabularyError,
|
|
15
|
+
} from "./dialect.js";
|
|
16
|
+
import { DEFAULT_MAX_DEPTH, SchemaRegistry } from "./registry.js";
|
|
17
|
+
import { runEvaluation } from "./engine.js";
|
|
18
|
+
import {
|
|
19
|
+
RegexCache,
|
|
20
|
+
RegexEngine,
|
|
21
|
+
UnsafeRegexError,
|
|
22
|
+
detectUnsafeRegex,
|
|
23
|
+
} from "./regex.js";
|
|
24
|
+
import {
|
|
25
|
+
LoadedDocument,
|
|
26
|
+
SchemaLoader,
|
|
27
|
+
SourceLocation,
|
|
28
|
+
SourceRange,
|
|
29
|
+
} from "./loader.js";
|
|
30
|
+
import { AnnotationUnit, ErrorUnit, makeRecordPredicate } from "./output.js";
|
|
31
|
+
import {
|
|
32
|
+
type EvaluateOptions,
|
|
33
|
+
type Result,
|
|
34
|
+
type ResultFor,
|
|
35
|
+
assembleResult,
|
|
36
|
+
resolveOutputDemand,
|
|
37
|
+
} from "./result.js";
|
|
38
|
+
import {
|
|
39
|
+
type RecordSets,
|
|
40
|
+
renderError,
|
|
41
|
+
renderSelected,
|
|
42
|
+
schemaLocationOf,
|
|
43
|
+
toRenderNode,
|
|
44
|
+
} from "./records.js";
|
|
45
|
+
import {
|
|
46
|
+
DIALECT_2020_12,
|
|
47
|
+
registerStandardDialects,
|
|
48
|
+
} from "./keywords/vocab2020.js";
|
|
49
|
+
import {
|
|
50
|
+
VOCAB_FORMAT_ASSERTION,
|
|
51
|
+
assertingFormat,
|
|
52
|
+
type FormatTable,
|
|
53
|
+
} from "./keywords/format.js";
|
|
54
|
+
import { METASCHEMAS_2020_12 } from "./keywords/metaschemas2020.js";
|
|
55
|
+
import { METASCHEMAS_2019_09 } from "./keywords/metaschemas2019.js";
|
|
56
|
+
import { METASCHEMAS_DRAFT_07 } from "./keywords/metaschemas7.js";
|
|
57
|
+
import { METASCHEMAS_DRAFT_06 } from "./keywords/metaschemas6.js";
|
|
58
|
+
import { VOCAB_CORE_2019 } from "./keywords/core.js";
|
|
59
|
+
import { identifiers2019, identifiers2020 } from "./dialect.js";
|
|
60
|
+
|
|
61
|
+
export type { JsonValue, JsonType } from "./json.js";
|
|
62
|
+
export type { Cursor } from "./cursor.js";
|
|
63
|
+
export { rootCursor, childCursor, instancePointer } from "./cursor.js";
|
|
64
|
+
export type { SchemaRef } from "./ref.js";
|
|
65
|
+
export type {
|
|
66
|
+
Dialect,
|
|
67
|
+
DialectKeyword,
|
|
68
|
+
KeywordBehavior,
|
|
69
|
+
KeywordContext,
|
|
70
|
+
StaticFacts,
|
|
71
|
+
DependencyView,
|
|
72
|
+
DialectOptions,
|
|
73
|
+
IdentifierFacts,
|
|
74
|
+
IdentifierExtractor,
|
|
75
|
+
} from "./dialect.js";
|
|
76
|
+
export {
|
|
77
|
+
DialectRegistry,
|
|
78
|
+
ReadOnlyRegistryError,
|
|
79
|
+
UnknownDialectError,
|
|
80
|
+
UnknownVocabularyError,
|
|
81
|
+
identifiers2020,
|
|
82
|
+
identifiers2019,
|
|
83
|
+
identifiersLegacy,
|
|
84
|
+
} from "./dialect.js";
|
|
85
|
+
export {
|
|
86
|
+
DEFAULT_MAX_DEPTH,
|
|
87
|
+
InvalidSchemaError,
|
|
88
|
+
MaxDepthExceededError,
|
|
89
|
+
SchemaRegistry,
|
|
90
|
+
} from "./registry.js";
|
|
91
|
+
export type { DocumentLocation } from "./registry.js";
|
|
92
|
+
export { UnsafeRegexError, detectUnsafeRegex } from "./regex.js";
|
|
93
|
+
export type { RegexEngine, CompiledRegex } from "./regex.js";
|
|
94
|
+
export { UnresolvableRefError } from "./uri.js";
|
|
95
|
+
export {
|
|
96
|
+
InfiniteLoopError,
|
|
97
|
+
KeywordContractError,
|
|
98
|
+
UndeclaredConsumptionError,
|
|
99
|
+
UndeclaredProductionError,
|
|
100
|
+
UnknownKeywordError,
|
|
101
|
+
evaluateFragment,
|
|
102
|
+
materializePath,
|
|
103
|
+
runEvaluation,
|
|
104
|
+
} from "./engine.js";
|
|
105
|
+
export type {
|
|
106
|
+
AnnotationRecord,
|
|
107
|
+
DependencyRecord,
|
|
108
|
+
ErrorRecord,
|
|
109
|
+
EvalState,
|
|
110
|
+
Frame,
|
|
111
|
+
FragmentOptions,
|
|
112
|
+
KeywordTrace,
|
|
113
|
+
PathNode,
|
|
114
|
+
RecordPredicate,
|
|
115
|
+
TraceNode,
|
|
116
|
+
} from "./engine.js";
|
|
117
|
+
export {
|
|
118
|
+
canonicalKey,
|
|
119
|
+
codePointLength,
|
|
120
|
+
escapeSegment,
|
|
121
|
+
firstDuplicatePair,
|
|
122
|
+
hasDuplicateItems,
|
|
123
|
+
jsonEqual,
|
|
124
|
+
unescapeSegment,
|
|
125
|
+
} from "./json.js";
|
|
126
|
+
export { isMultipleOf } from "./keywords/validation.js";
|
|
127
|
+
export { walkSchema } from "./walk.js";
|
|
128
|
+
export type { SchemaWalkVisit } from "./walk.js";
|
|
129
|
+
export type {
|
|
130
|
+
AnalyzeContext,
|
|
131
|
+
ErrorParams,
|
|
132
|
+
IndexCoverage,
|
|
133
|
+
NameCoverage,
|
|
134
|
+
SubschemaApplication,
|
|
135
|
+
} from "./dialect.js";
|
|
136
|
+
export { lowerIR } from "./lowering.js";
|
|
137
|
+
export {
|
|
138
|
+
makeRecordPredicate,
|
|
139
|
+
renderBasic,
|
|
140
|
+
renderDetailed,
|
|
141
|
+
renderHierarchical,
|
|
142
|
+
renderList,
|
|
143
|
+
renderTrace,
|
|
144
|
+
renderVerbose,
|
|
145
|
+
} from "./output.js";
|
|
146
|
+
export {
|
|
147
|
+
renderAnnotation,
|
|
148
|
+
renderError,
|
|
149
|
+
traceToRenderNodes,
|
|
150
|
+
} from "./records.js";
|
|
151
|
+
export type { MutableRenderNode } from "./records.js";
|
|
152
|
+
export {
|
|
153
|
+
OutputOptionsError,
|
|
154
|
+
assembleResult,
|
|
155
|
+
resolveOutputDemand,
|
|
156
|
+
} from "./result.js";
|
|
157
|
+
export type {
|
|
158
|
+
EvaluateOptions,
|
|
159
|
+
OutputDemand,
|
|
160
|
+
OutputDocuments,
|
|
161
|
+
OutputFormat,
|
|
162
|
+
Result,
|
|
163
|
+
ResultFor,
|
|
164
|
+
ResultUnits,
|
|
165
|
+
} from "./result.js";
|
|
166
|
+
export type {
|
|
167
|
+
LowerApply,
|
|
168
|
+
LowerCursor,
|
|
169
|
+
LowerExpr,
|
|
170
|
+
LowerHelper,
|
|
171
|
+
LowerMessage,
|
|
172
|
+
LowerParams,
|
|
173
|
+
LowerProduceValue,
|
|
174
|
+
LowerStmt,
|
|
175
|
+
LoweringContext,
|
|
176
|
+
} from "./lowering.js";
|
|
177
|
+
export {
|
|
178
|
+
foldNameCoverage,
|
|
179
|
+
foldIndexCoverage,
|
|
180
|
+
harvestCoverage,
|
|
181
|
+
} from "./coverage.js";
|
|
182
|
+
export { RegexCache, defaultRegexEngine } from "./regex.js";
|
|
183
|
+
export type {
|
|
184
|
+
AnnotationSelection,
|
|
185
|
+
AnnotationUnit,
|
|
186
|
+
BasicAnnotationUnit,
|
|
187
|
+
BasicErrorUnit,
|
|
188
|
+
BasicOutputDocument,
|
|
189
|
+
DetailedOutputUnit,
|
|
190
|
+
ErrorUnit,
|
|
191
|
+
IrrelevantRendering,
|
|
192
|
+
ListOutputDocument,
|
|
193
|
+
OutputUnit,
|
|
194
|
+
RenderInput,
|
|
195
|
+
RenderNode,
|
|
196
|
+
TraceUnit,
|
|
197
|
+
} from "./output.js";
|
|
198
|
+
export type {
|
|
199
|
+
LoadedDocument,
|
|
200
|
+
SchemaLoader,
|
|
201
|
+
SourceLocation,
|
|
202
|
+
SourcePosition,
|
|
203
|
+
SourceRange,
|
|
204
|
+
SourceSpan,
|
|
205
|
+
} from "./loader.js";
|
|
206
|
+
export { DIALECT_2020_12 } from "./keywords/vocab2020.js";
|
|
207
|
+
export {
|
|
208
|
+
UnknownFormatError,
|
|
209
|
+
VOCAB_FORMAT_ASSERTION,
|
|
210
|
+
assertingFormat,
|
|
211
|
+
} from "./keywords/format.js";
|
|
212
|
+
export type { FormatDefinition, FormatTable } from "./keywords/format.js";
|
|
213
|
+
export { DIALECT_2019_09 } from "./keywords/vocab2019.js";
|
|
214
|
+
export { DIALECT_DRAFT_07, DIALECT_DRAFT_06 } from "./keywords/vocab7.js";
|
|
215
|
+
|
|
216
|
+
/** Thrown by {@link EngineOptions.validateSchemas} when a registered schema fails its metaschema. */
|
|
217
|
+
export class SchemaValidationError extends Error {
|
|
218
|
+
constructor(
|
|
219
|
+
message: string,
|
|
220
|
+
readonly errors: readonly ErrorUnit[],
|
|
221
|
+
) {
|
|
222
|
+
super(message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Options for {@link Engine}'s constructor. */
|
|
227
|
+
export interface EngineOptions {
|
|
228
|
+
/** dialect for documents without $schema; default 2020-12 */
|
|
229
|
+
defaultDialect?: string;
|
|
230
|
+
/** resource loaders, tried in order (D7) */
|
|
231
|
+
loaders?: readonly SchemaLoader[];
|
|
232
|
+
/**
|
|
233
|
+
* Validate each load/register target against its metaschema when that
|
|
234
|
+
* metaschema is registered as a schema resource. The standard metaschemas
|
|
235
|
+
* for the built-in dialects are bundled; for custom dialects, an
|
|
236
|
+
* unavailable metaschema means "cannot check", not failure — supply a
|
|
237
|
+
* loader for it to get the check.
|
|
238
|
+
*/
|
|
239
|
+
validateSchemas?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Compile `pattern`/`patternProperties` through this engine instead of the
|
|
242
|
+
* native `RegExp`. Supply a linear-time engine (e.g. RE2) to evaluate
|
|
243
|
+
* untrusted schemas without exposure to catastrophic backtracking (ReDoS).
|
|
244
|
+
* See docs/guide/security.md.
|
|
245
|
+
*/
|
|
246
|
+
regexEngine?: RegexEngine;
|
|
247
|
+
/**
|
|
248
|
+
* Reject a schema at registration when a `pattern`/`patternProperties`
|
|
249
|
+
* regex looks exponential-time ({@link detectUnsafeRegex}), throwing
|
|
250
|
+
* {@link UnsafeRegexError}. A conservative static screen, off by default;
|
|
251
|
+
* a linear-time `regexEngine` remains the only hard guarantee.
|
|
252
|
+
*/
|
|
253
|
+
rejectUnsafeRegex?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Maximum schema-nesting (registration) and schema-application
|
|
256
|
+
* (evaluation) depth before {@link MaxDepthExceededError}. Bounds otherwise
|
|
257
|
+
* unbounded recursion on adversarial input; default
|
|
258
|
+
* {@link DEFAULT_MAX_DEPTH}.
|
|
259
|
+
*/
|
|
260
|
+
maxDepth?: number;
|
|
261
|
+
/**
|
|
262
|
+
* Format implementations (M7; \@json-schema-engine/formats supplies standard tables).
|
|
263
|
+
* Enables the format-assertion vocabulary: a `$vocabulary` dialect
|
|
264
|
+
* declaring it gets an asserting `format` that REFUSES unsupported
|
|
265
|
+
* formats at registration ({@link UnknownFormatError}).
|
|
266
|
+
*/
|
|
267
|
+
formats?: FormatTable;
|
|
268
|
+
/**
|
|
269
|
+
* Assert recognized formats in the standard dialects (all drafts) —
|
|
270
|
+
* the spec's opt-in assertion configuration. Best effort: formats the
|
|
271
|
+
* table lacks fall back to annotation-only. Requires `formats`.
|
|
272
|
+
*/
|
|
273
|
+
assertFormats?: boolean;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* \@json-schema-engine/core public API: synchronous evaluation over registered schemas,
|
|
278
|
+
* with the 2020-12 dialect preloaded and async resource loading for
|
|
279
|
+
* `$ref` closures and `$vocabulary`-assembled dialects.
|
|
280
|
+
*/
|
|
281
|
+
export class Engine {
|
|
282
|
+
readonly dialects = new DialectRegistry();
|
|
283
|
+
private schemas: SchemaRegistry;
|
|
284
|
+
private defaultDialect: string;
|
|
285
|
+
private loaders: SchemaLoader[];
|
|
286
|
+
private validateSchemas: boolean;
|
|
287
|
+
private regexCache: RegexCache;
|
|
288
|
+
private maxDepth: number;
|
|
289
|
+
private formatTable: FormatTable | undefined;
|
|
290
|
+
// Dialect URIs whose assembly is in progress, to fail metaschema cycles.
|
|
291
|
+
private assembling = new Set<string>();
|
|
292
|
+
|
|
293
|
+
constructor(options: EngineOptions = {}) {
|
|
294
|
+
if (options.assertFormats && !options.formats) {
|
|
295
|
+
throw new TypeError("assertFormats requires a formats table");
|
|
296
|
+
}
|
|
297
|
+
registerStandardDialects(
|
|
298
|
+
this.dialects,
|
|
299
|
+
options.assertFormats
|
|
300
|
+
? {
|
|
301
|
+
format: (id) => assertingFormat(id, options.formats!, false),
|
|
302
|
+
}
|
|
303
|
+
: {},
|
|
304
|
+
);
|
|
305
|
+
// The format-assertion vocabulary exists whenever a table is supplied;
|
|
306
|
+
// without one, a dialect requiring it fails with UnknownVocabularyError,
|
|
307
|
+
// which is the correct "cannot honor the assertion promise" answer.
|
|
308
|
+
this.formatTable = options.formats;
|
|
309
|
+
if (options.formats) {
|
|
310
|
+
const table = options.formats;
|
|
311
|
+
this.dialects.registerVocabulary(VOCAB_FORMAT_ASSERTION, {
|
|
312
|
+
format: assertingFormat(
|
|
313
|
+
`${VOCAB_FORMAT_ASSERTION}#format`,
|
|
314
|
+
table,
|
|
315
|
+
true,
|
|
316
|
+
),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
this.defaultDialect = splitFragment(
|
|
320
|
+
options.defaultDialect ?? DIALECT_2020_12,
|
|
321
|
+
).resource;
|
|
322
|
+
this.regexCache = new RegexCache(options.regexEngine);
|
|
323
|
+
this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
324
|
+
this.schemas = new SchemaRegistry(
|
|
325
|
+
this.dialects,
|
|
326
|
+
this.defaultDialect,
|
|
327
|
+
this.maxDepth,
|
|
328
|
+
);
|
|
329
|
+
this.loaders = [...(options.loaders ?? [])];
|
|
330
|
+
this.validateSchemas = options.validateSchemas ?? false;
|
|
331
|
+
// Standard metaschemas are registered as ordinary schema resources so
|
|
332
|
+
// that $refs to them resolve without loaders and validateSchemas can
|
|
333
|
+
// check standard-dialect documents. Registered directly (not through
|
|
334
|
+
// registerSchema) so the policy never self-validates them here.
|
|
335
|
+
for (const [uri, doc] of METASCHEMAS_2020_12) {
|
|
336
|
+
this.schemas.register(doc, uri);
|
|
337
|
+
}
|
|
338
|
+
for (const [uri, doc] of METASCHEMAS_2019_09) {
|
|
339
|
+
this.schemas.register(doc, uri);
|
|
340
|
+
}
|
|
341
|
+
for (const [uri, doc] of METASCHEMAS_DRAFT_07) {
|
|
342
|
+
this.schemas.register(doc, uri);
|
|
343
|
+
}
|
|
344
|
+
for (const [uri, doc] of METASCHEMAS_DRAFT_06) {
|
|
345
|
+
this.schemas.register(doc, uri);
|
|
346
|
+
}
|
|
347
|
+
// Installed after the trusted metaschemas register, so the screen applies
|
|
348
|
+
// only to caller schemas (D20).
|
|
349
|
+
if (options.rejectUnsafeRegex) {
|
|
350
|
+
this.schemas.onRegex = (pattern, location) => {
|
|
351
|
+
const verdict = detectUnsafeRegex(pattern);
|
|
352
|
+
if (!verdict.safe) {
|
|
353
|
+
throw new UnsafeRegexError(
|
|
354
|
+
`unsafe regex at '${location}': ${verdict.reason}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Registers an additional resource loader, tried after existing ones (D7). */
|
|
362
|
+
addLoader(loader: SchemaLoader): void {
|
|
363
|
+
this.loaders.push(loader);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The engine's schema registry — the compiler tier's read surface (M6):
|
|
368
|
+
* resolved refs, per-resource dialects, identifier indexes. Mutations go
|
|
369
|
+
* through the Engine methods, not the registry; `snapshot()` yields the
|
|
370
|
+
* read-only view a compiled artifact binds to.
|
|
371
|
+
*/
|
|
372
|
+
get registry(): SchemaRegistry {
|
|
373
|
+
return this.schemas;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The engine's compiled-pattern cache, shared with compiled artifacts (M6). */
|
|
377
|
+
get patternCache(): RegexCache {
|
|
378
|
+
return this.regexCache;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The format table backing this engine's asserting `format`, or undefined
|
|
383
|
+
* when none was configured — the compiler tier's read surface for
|
|
384
|
+
* format-assertion lowering (M7). The asserting `format` behaviors close
|
|
385
|
+
* over this same table, so a compiled `formatTest` and the interpreter agree.
|
|
386
|
+
*/
|
|
387
|
+
get formats(): FormatTable | undefined {
|
|
388
|
+
return this.formatTable;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Register a local schema document synchronously; returns its canonical
|
|
393
|
+
* base URI. References to unregistered resources are not loaded — use
|
|
394
|
+
* loadSchema for that.
|
|
395
|
+
*/
|
|
396
|
+
registerSchema(
|
|
397
|
+
schema: JsonValue,
|
|
398
|
+
retrievalUri: string,
|
|
399
|
+
dialectUri?: string,
|
|
400
|
+
getRange?: (pointer: string) => SourceRange | undefined,
|
|
401
|
+
): string {
|
|
402
|
+
const baseUri = this.schemas.register(
|
|
403
|
+
schema,
|
|
404
|
+
retrievalUri,
|
|
405
|
+
dialectUri,
|
|
406
|
+
getRange,
|
|
407
|
+
);
|
|
408
|
+
this.maybeValidate(baseUri);
|
|
409
|
+
return baseUri;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Register a schema document and load everything it transitively
|
|
414
|
+
* references through the configured loaders. Loader misses for referenced
|
|
415
|
+
* resources are not errors here; evaluation reports them if the reference
|
|
416
|
+
* is actually followed.
|
|
417
|
+
*/
|
|
418
|
+
async loadSchema(
|
|
419
|
+
schema: JsonValue,
|
|
420
|
+
retrievalUri: string,
|
|
421
|
+
dialectUri?: string,
|
|
422
|
+
getRange?: (pointer: string) => SourceRange | undefined,
|
|
423
|
+
): Promise<string> {
|
|
424
|
+
await this.ensureDialectFor(schema, retrievalUri, dialectUri);
|
|
425
|
+
const baseUri = this.schemas.register(
|
|
426
|
+
schema,
|
|
427
|
+
retrievalUri,
|
|
428
|
+
dialectUri,
|
|
429
|
+
getRange,
|
|
430
|
+
);
|
|
431
|
+
await this.loadPending();
|
|
432
|
+
this.maybeValidate(baseUri);
|
|
433
|
+
return baseUri;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Fetch a resource by URI through the loaders and register its closure. */
|
|
437
|
+
async load(uri: string): Promise<string> {
|
|
438
|
+
const resource = splitFragment(uri).resource;
|
|
439
|
+
const baseUri = await this.loadResource(resource);
|
|
440
|
+
if (baseUri === undefined) {
|
|
441
|
+
throw new UnresolvableRefError(`no loader provided '${resource}'`);
|
|
442
|
+
}
|
|
443
|
+
await this.loadPending();
|
|
444
|
+
this.maybeValidate(baseUri);
|
|
445
|
+
return baseUri;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Registers a vocabulary's keyword behaviors under its URI. */
|
|
449
|
+
registerVocabulary(
|
|
450
|
+
uri: string,
|
|
451
|
+
keywords: Readonly<Record<string, KeywordBehavior>>,
|
|
452
|
+
): void {
|
|
453
|
+
this.dialects.registerVocabulary(uri, keywords);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Assembles a dialect from already-registered vocabularies.
|
|
458
|
+
* @throws UnknownDialectError if a listed vocabulary is not registered.
|
|
459
|
+
*/
|
|
460
|
+
registerDialect(
|
|
461
|
+
uri: string,
|
|
462
|
+
vocabularyUris: readonly string[],
|
|
463
|
+
options?: DialectOptions,
|
|
464
|
+
): void {
|
|
465
|
+
this.dialects.registerDialect(uri, vocabularyUris, options);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Where a schema resource lives within its registered document: the
|
|
470
|
+
* containing document plus the resource root's document-rooted pointer
|
|
471
|
+
* (D17 bridge; see loader.ts), for source-position lookup.
|
|
472
|
+
*/
|
|
473
|
+
documentLocation(resourceUri: string) {
|
|
474
|
+
return this.schemas.documentLocation(resourceUri);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Translates a canonical schema location (`resourceUri#/pointer`) to its
|
|
479
|
+
* document, document-rooted pointer, and — when the document's loader
|
|
480
|
+
* reported positions — source range (D17 bridge; see loader.ts).
|
|
481
|
+
*/
|
|
482
|
+
locate(schemaLocation: string): SourceLocation | undefined {
|
|
483
|
+
const { resource, fragment } = splitFragment(schemaLocation);
|
|
484
|
+
const loc = this.schemas.documentLocation(resource);
|
|
485
|
+
if (loc === undefined) return undefined;
|
|
486
|
+
const pointer = loc.pointer + (fragment ?? "");
|
|
487
|
+
const range = this.schemas.range(loc.documentUri, pointer);
|
|
488
|
+
return range === undefined
|
|
489
|
+
? { documentUri: loc.documentUri, pointer }
|
|
490
|
+
: { documentUri: loc.documentUri, pointer, range };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Evaluates an instance against a registered schema. Literal options
|
|
495
|
+
* narrow the result ({@link ResultFor}); an unsupported combination of
|
|
496
|
+
* options throws {@link OutputOptionsError} before evaluating.
|
|
497
|
+
*/
|
|
498
|
+
evaluate<O extends EvaluateOptions>(
|
|
499
|
+
schemaUri: string,
|
|
500
|
+
instance: JsonValue,
|
|
501
|
+
options?: O,
|
|
502
|
+
): ResultFor<O>;
|
|
503
|
+
evaluate(
|
|
504
|
+
schemaUri: string,
|
|
505
|
+
instance: JsonValue,
|
|
506
|
+
options: EvaluateOptions = {},
|
|
507
|
+
): Result {
|
|
508
|
+
const demand = resolveOutputDemand(options);
|
|
509
|
+
const { valid, state } = runEvaluation(
|
|
510
|
+
this.schemas,
|
|
511
|
+
schemaUri,
|
|
512
|
+
instance,
|
|
513
|
+
demand.tracing,
|
|
514
|
+
makeRecordPredicate(demand.annotations),
|
|
515
|
+
this.regexCache,
|
|
516
|
+
this.maxDepth,
|
|
517
|
+
);
|
|
518
|
+
if (demand.format === "flag") return { valid };
|
|
519
|
+
|
|
520
|
+
// The flat surface first; its record arrays stay paired with the unit
|
|
521
|
+
// arrays so the located tree can index the units.
|
|
522
|
+
const params = options.errorParams ?? false;
|
|
523
|
+
const records: RecordSets = {
|
|
524
|
+
errors: state.errors,
|
|
525
|
+
droppedErrors: [],
|
|
526
|
+
annotations: [],
|
|
527
|
+
droppedAnnotations: [],
|
|
528
|
+
};
|
|
529
|
+
const errors = state.errors.map((e) => renderError(e, params));
|
|
530
|
+
let annotations: AnnotationUnit[] = [];
|
|
531
|
+
if (valid && demand.annotations !== false) {
|
|
532
|
+
const selected = renderSelected(
|
|
533
|
+
state.rootAnnotations,
|
|
534
|
+
demand.annotations,
|
|
535
|
+
);
|
|
536
|
+
records.annotations = selected.records;
|
|
537
|
+
annotations = selected.units;
|
|
538
|
+
}
|
|
539
|
+
let droppedErrors: ErrorUnit[] = [];
|
|
540
|
+
let droppedAnnotations: AnnotationUnit[] = [];
|
|
541
|
+
if (demand.verbose) {
|
|
542
|
+
records.droppedErrors = state.droppedErrors ?? [];
|
|
543
|
+
droppedErrors = records.droppedErrors.map((e) => renderError(e, params));
|
|
544
|
+
if (demand.annotations !== false) {
|
|
545
|
+
// The relevant annotations are a valid run's root survivors; an
|
|
546
|
+
// invalid run has none (draft-03 §12.2).
|
|
547
|
+
const relevant = new Set(valid ? state.rootAnnotations : []);
|
|
548
|
+
const selected = renderSelected(
|
|
549
|
+
(state.allAnnotations ?? []).filter((a) => !relevant.has(a)),
|
|
550
|
+
demand.annotations,
|
|
551
|
+
);
|
|
552
|
+
records.droppedAnnotations = selected.records;
|
|
553
|
+
droppedAnnotations = selected.units;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const root = demand.tracing
|
|
557
|
+
? toRenderNode(state.traceRoot!, records)
|
|
558
|
+
: null;
|
|
559
|
+
const result = assembleResult(
|
|
560
|
+
demand,
|
|
561
|
+
valid,
|
|
562
|
+
{ errors, droppedErrors, annotations, droppedAnnotations },
|
|
563
|
+
root,
|
|
564
|
+
schemaLocationOf(this.schemas.rootRef(schemaUri), null),
|
|
565
|
+
options.trace === true,
|
|
566
|
+
);
|
|
567
|
+
if (options.positions) {
|
|
568
|
+
this.decorate(result.errors);
|
|
569
|
+
this.decorate(result.annotations);
|
|
570
|
+
this.decorate(result.droppedErrors);
|
|
571
|
+
this.decorate(result.droppedAnnotations);
|
|
572
|
+
}
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private decorate(
|
|
577
|
+
units: readonly ErrorUnit[] | readonly AnnotationUnit[] | undefined,
|
|
578
|
+
): void {
|
|
579
|
+
for (const unit of units ?? []) {
|
|
580
|
+
const source = this.locate(unit.schemaLocation);
|
|
581
|
+
if (source !== undefined) unit.source = source;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private async fetch(resource: string): Promise<LoadedDocument | undefined> {
|
|
586
|
+
for (const loader of this.loaders) {
|
|
587
|
+
const doc = await loader(resource);
|
|
588
|
+
if (doc !== undefined) return doc;
|
|
589
|
+
}
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Fetch + register one resource; undefined when no loader handles it. */
|
|
594
|
+
private async loadResource(resource: string): Promise<string | undefined> {
|
|
595
|
+
if (this.schemas.has(resource)) return resource;
|
|
596
|
+
const doc = await this.fetch(resource);
|
|
597
|
+
if (doc === undefined) return undefined;
|
|
598
|
+
await this.ensureDialectFor(doc.value, resource);
|
|
599
|
+
return this.schemas.register(doc.value, resource, undefined, doc.getRange);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Drain reference targets collected by registration walks until closure.
|
|
603
|
+
// Each round may register documents whose own walks add new targets.
|
|
604
|
+
private async loadPending(): Promise<void> {
|
|
605
|
+
for (;;) {
|
|
606
|
+
const missing = this.schemas.takeUnresolved();
|
|
607
|
+
if (missing.length === 0) return;
|
|
608
|
+
for (const resource of missing) {
|
|
609
|
+
await this.loadResource(resource);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Make the document's dialect exist before registration: known dialects
|
|
615
|
+
// pass through; otherwise the $schema target is loaded as a metaschema and
|
|
616
|
+
// a dialect is assembled from its $vocabulary (spec §8.1).
|
|
617
|
+
private async ensureDialectFor(
|
|
618
|
+
schema: JsonValue,
|
|
619
|
+
retrievalUri: string,
|
|
620
|
+
dialectUri?: string,
|
|
621
|
+
): Promise<void> {
|
|
622
|
+
let effective = splitFragment(dialectUri ?? this.defaultDialect).resource;
|
|
623
|
+
if (isObject(schema) && typeof schema.$schema === "string") {
|
|
624
|
+
effective = splitFragment(
|
|
625
|
+
resolveUri(schema.$schema, retrievalUri),
|
|
626
|
+
).resource;
|
|
627
|
+
}
|
|
628
|
+
if (this.dialects.hasDialect(effective)) return;
|
|
629
|
+
if (this.assembling.has(effective)) {
|
|
630
|
+
throw new UnknownDialectError(`metaschema cycle at '${effective}'`);
|
|
631
|
+
}
|
|
632
|
+
this.assembling.add(effective);
|
|
633
|
+
try {
|
|
634
|
+
const metaBase = await this.loadResource(effective);
|
|
635
|
+
const meta =
|
|
636
|
+
metaBase === undefined ? undefined : this.schemas.document(metaBase);
|
|
637
|
+
if (meta === undefined) {
|
|
638
|
+
throw new UnknownDialectError(
|
|
639
|
+
`dialect '${effective}' is not registered and no loader provides its metaschema`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
this.assembleDialect(effective, meta);
|
|
643
|
+
} finally {
|
|
644
|
+
this.assembling.delete(effective);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private assembleDialect(uri: string, meta: JsonValue): void {
|
|
649
|
+
const declared = isObject(meta) ? meta.$vocabulary : undefined;
|
|
650
|
+
if (!isObject(declared)) {
|
|
651
|
+
// The spec leaves $vocabulary-less metaschemas open; the least-surprise
|
|
652
|
+
// reading is the default dialect's vocabulary set and syntax options.
|
|
653
|
+
const base = this.dialects.getDialect(this.defaultDialect);
|
|
654
|
+
this.dialects.registerDialect(uri, base.vocabularyUris, {
|
|
655
|
+
allowUnknownKeywords: base.allowUnknownKeywords,
|
|
656
|
+
identifiers: base.identifiers,
|
|
657
|
+
refIgnoresSiblings: base.refIgnoresSiblings,
|
|
658
|
+
});
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const uris: string[] = [];
|
|
662
|
+
for (const [vocabUri, required] of Object.entries(declared)) {
|
|
663
|
+
if (this.dialects.hasVocabulary(vocabUri)) {
|
|
664
|
+
uris.push(vocabUri);
|
|
665
|
+
} else if (required === true) {
|
|
666
|
+
throw new UnknownVocabularyError(
|
|
667
|
+
`dialect '${uri}' requires unknown vocabulary '${vocabUri}'`,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
// Unknown optional vocabularies are skipped; their keywords fall to
|
|
671
|
+
// unknown-keyword annotation handling (spec MUST for false).
|
|
672
|
+
}
|
|
673
|
+
// Identifier syntax travels with the core vocabulary (D18): a dialect
|
|
674
|
+
// assembled around the 2019-09 core gets 2019-09 identifier handling.
|
|
675
|
+
this.dialects.registerDialect(uri, uris, {
|
|
676
|
+
identifiers: uris.includes(VOCAB_CORE_2019)
|
|
677
|
+
? identifiers2019
|
|
678
|
+
: identifiers2020,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private maybeValidate(baseUri: string): void {
|
|
683
|
+
if (!this.validateSchemas) return;
|
|
684
|
+
const dialectUri = this.schemas.dialectUriFor(baseUri);
|
|
685
|
+
if (!this.schemas.has(dialectUri)) return; // metaschema document unavailable
|
|
686
|
+
const doc = this.schemas.document(baseUri)!;
|
|
687
|
+
const result = this.evaluate(dialectUri, doc, { output: "list" });
|
|
688
|
+
if (!result.valid) {
|
|
689
|
+
throw new SchemaValidationError(
|
|
690
|
+
`schema '${baseUri}' fails its metaschema '${dialectUri}'`,
|
|
691
|
+
result.errors ?? [],
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Creates a new {@link Engine}. */
|
|
698
|
+
export const createEngine = (options?: EngineOptions): Engine =>
|
|
699
|
+
new Engine(options);
|