toolcraft-schema 0.0.115 → 0.0.117
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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/json-schema/compiler.d.ts +11 -0
- package/dist/json-schema/compiler.js +390 -0
- package/dist/json-schema/evaluate.d.ts +3 -0
- package/dist/json-schema/evaluate.js +442 -0
- package/dist/json-schema/index.d.ts +5 -0
- package/dist/json-schema/index.js +19 -0
- package/dist/json-schema/types.d.ts +33 -0
- package/dist/json-schema/types.js +1 -0
- package/dist/json-schema/utils.d.ts +19 -0
- package/dist/json-schema/utils.js +171 -0
- package/dist/validate.d.ts +1 -0
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -178,5 +178,7 @@ export declare const S: {
|
|
|
178
178
|
export declare function toJsonSchema(schema: AnySchema): JsonSchema;
|
|
179
179
|
export declare function toJsonSchemaDocument(schema: AnySchema, options?: JsonSchemaDocumentOptions): JsonSchemaDocument;
|
|
180
180
|
export { Json, OneOf, RecordBuilder as Record, Union, validate };
|
|
181
|
+
export { compileJsonSchema, formatIssues } from "./json-schema/index.js";
|
|
182
|
+
export type { CompileJsonSchemaOptions, CompiledJsonSchema } from "./json-schema/index.js";
|
|
181
183
|
export type { JsonSchemaDocument, JsonSchemaDocumentOptions } from "./json-schema-document.js";
|
|
182
184
|
export type { JsonValue, JsonValueSchema, OneOfSchema, RecordSchema, UnionSchema, ValidationIssue, ValidationResult };
|
package/dist/index.js
CHANGED
|
@@ -292,3 +292,4 @@ export function toJsonSchemaDocument(schema, options = {}) {
|
|
|
292
292
|
return createJsonSchemaDocument(toJsonSchema(schema), options);
|
|
293
293
|
}
|
|
294
294
|
export { Json, OneOf, RecordBuilder as Record, Union, validate };
|
|
295
|
+
export { compileJsonSchema, formatIssues } from "./json-schema/index.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CompileJsonSchemaOptions, SchemaNode } from "./types.js";
|
|
2
|
+
export interface CompiledGraph {
|
|
3
|
+
root: SchemaNode;
|
|
4
|
+
locations: Map<string, SchemaNode>;
|
|
5
|
+
resources: Map<string, SchemaNode>;
|
|
6
|
+
anchors: Map<string, SchemaNode>;
|
|
7
|
+
dynamicAnchors: Map<string, SchemaNode>;
|
|
8
|
+
resolve(node: SchemaNode, reference: string): SchemaNode;
|
|
9
|
+
dynamicAnchor(scope: SchemaNode, name: string): SchemaNode | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function compileGraph(schema: unknown, options?: CompileJsonSchemaOptions): CompiledGraph;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { dialectFor, escapePointer, fragmentOf, isObject, isSchema, resolveUri, withoutFragment } from "./utils.js";
|
|
2
|
+
const schemaMapKeywords = new Set([
|
|
3
|
+
"$defs",
|
|
4
|
+
"definitions",
|
|
5
|
+
"properties",
|
|
6
|
+
"patternProperties",
|
|
7
|
+
"dependentSchemas",
|
|
8
|
+
"dependencies"
|
|
9
|
+
]);
|
|
10
|
+
const schemaArrayKeywords = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
|
|
11
|
+
const schemaKeywords = new Set([
|
|
12
|
+
"additionalItems",
|
|
13
|
+
"additionalProperties",
|
|
14
|
+
"contains",
|
|
15
|
+
"else",
|
|
16
|
+
"if",
|
|
17
|
+
"items",
|
|
18
|
+
"not",
|
|
19
|
+
"propertyNames",
|
|
20
|
+
"then",
|
|
21
|
+
"unevaluatedItems",
|
|
22
|
+
"unevaluatedProperties"
|
|
23
|
+
]);
|
|
24
|
+
const draftTypes = ["null", "boolean", "object", "array", "number", "integer", "string"];
|
|
25
|
+
const draftTypeSet = new Set(draftTypes);
|
|
26
|
+
const metaSchemaKeywordProperties = {
|
|
27
|
+
type: {
|
|
28
|
+
anyOf: [
|
|
29
|
+
{ enum: draftTypes },
|
|
30
|
+
{ type: "array", items: { enum: draftTypes }, minItems: 1, uniqueItems: true }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
minLength: { type: "integer", minimum: 0 },
|
|
34
|
+
maxLength: { type: "integer", minimum: 0 },
|
|
35
|
+
minItems: { type: "integer", minimum: 0 },
|
|
36
|
+
maxItems: { type: "integer", minimum: 0 },
|
|
37
|
+
minProperties: { type: "integer", minimum: 0 },
|
|
38
|
+
maxProperties: { type: "integer", minimum: 0 }
|
|
39
|
+
};
|
|
40
|
+
const builtInRegistry = {
|
|
41
|
+
"https://json-schema.org/draft/2020-12/schema": {
|
|
42
|
+
$id: "https://json-schema.org/draft/2020-12/schema",
|
|
43
|
+
type: ["object", "boolean"],
|
|
44
|
+
properties: {
|
|
45
|
+
...metaSchemaKeywordProperties,
|
|
46
|
+
$defs: { type: "object", additionalProperties: { $ref: "#" } }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"http://json-schema.org/draft-07/schema": {
|
|
50
|
+
$id: "http://json-schema.org/draft-07/schema#",
|
|
51
|
+
type: ["object", "boolean"],
|
|
52
|
+
properties: {
|
|
53
|
+
...metaSchemaKeywordProperties,
|
|
54
|
+
definitions: { type: "object", additionalProperties: { $ref: "#" } }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function mapKey(dialect, uri) {
|
|
59
|
+
return `${dialect}\0${uri}`;
|
|
60
|
+
}
|
|
61
|
+
function assertSchemaArray(value, keyword) {
|
|
62
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every(isSchema)) {
|
|
63
|
+
throw new Error(`${keyword} must be a non-empty array of schemas.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function assertStringArray(value, keyword) {
|
|
67
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
68
|
+
throw new Error(`${keyword} must be an array of strings.`);
|
|
69
|
+
}
|
|
70
|
+
if (new Set(value).size !== value.length) {
|
|
71
|
+
throw new Error(`${keyword} must contain unique strings.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function assertSchemaMap(value, keyword) {
|
|
75
|
+
if (!isObject(value) || !Object.values(value).every(isSchema)) {
|
|
76
|
+
throw new Error(`${keyword} must be an object containing schemas.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function assertNonNegativeInteger(value, keyword) {
|
|
80
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
81
|
+
throw new Error(`${keyword} must be a non-negative integer.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function validateSchemaObject(schema, dialect) {
|
|
85
|
+
if (schema.type !== undefined) {
|
|
86
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
87
|
+
if (types.length === 0 ||
|
|
88
|
+
!types.every((type) => typeof type === "string" && draftTypeSet.has(type)) ||
|
|
89
|
+
new Set(types).size !== types.length) {
|
|
90
|
+
throw new Error("type must be a JSON Schema type or a unique array of JSON Schema types.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const keyword of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
|
|
94
|
+
if (schema[keyword] !== undefined && typeof schema[keyword] !== "number") {
|
|
95
|
+
throw new Error(`${keyword} must be a number.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (schema.multipleOf !== undefined &&
|
|
99
|
+
(typeof schema.multipleOf !== "number" || schema.multipleOf <= 0)) {
|
|
100
|
+
throw new Error("multipleOf must be a number greater than zero.");
|
|
101
|
+
}
|
|
102
|
+
for (const keyword of [
|
|
103
|
+
"minLength",
|
|
104
|
+
"maxLength",
|
|
105
|
+
"minItems",
|
|
106
|
+
"maxItems",
|
|
107
|
+
"minContains",
|
|
108
|
+
"maxContains",
|
|
109
|
+
"minProperties",
|
|
110
|
+
"maxProperties"
|
|
111
|
+
]) {
|
|
112
|
+
if (schema[keyword] !== undefined) {
|
|
113
|
+
assertNonNegativeInteger(schema[keyword], keyword);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (schema.pattern !== undefined && typeof schema.pattern !== "string") {
|
|
117
|
+
throw new Error("pattern must be a string.");
|
|
118
|
+
}
|
|
119
|
+
for (const keyword of ["uniqueItems", "$recursiveAnchor"]) {
|
|
120
|
+
if (schema[keyword] !== undefined && typeof schema[keyword] !== "boolean") {
|
|
121
|
+
throw new Error(`${keyword} must be a boolean.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (schema.required !== undefined) {
|
|
125
|
+
assertStringArray(schema.required, "required");
|
|
126
|
+
}
|
|
127
|
+
if (schema.dependentRequired !== undefined) {
|
|
128
|
+
if (!isObject(schema.dependentRequired)) {
|
|
129
|
+
throw new Error("dependentRequired must be an object containing string arrays.");
|
|
130
|
+
}
|
|
131
|
+
for (const dependencies of Object.values(schema.dependentRequired)) {
|
|
132
|
+
assertStringArray(dependencies, "dependentRequired");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const keyword of schemaMapKeywords) {
|
|
136
|
+
const value = schema[keyword];
|
|
137
|
+
if (value === undefined || keyword === "dependencies")
|
|
138
|
+
continue;
|
|
139
|
+
assertSchemaMap(value, keyword);
|
|
140
|
+
}
|
|
141
|
+
if (schema.dependencies !== undefined) {
|
|
142
|
+
if (!isObject(schema.dependencies)) {
|
|
143
|
+
throw new Error("dependencies must be an object containing schemas or string arrays.");
|
|
144
|
+
}
|
|
145
|
+
for (const dependency of Object.values(schema.dependencies)) {
|
|
146
|
+
if (!isSchema(dependency))
|
|
147
|
+
assertStringArray(dependency, "dependencies");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
151
|
+
if (schema[keyword] !== undefined)
|
|
152
|
+
assertSchemaArray(schema[keyword], keyword);
|
|
153
|
+
}
|
|
154
|
+
if (schema.prefixItems !== undefined) {
|
|
155
|
+
if (!Array.isArray(schema.prefixItems) || !schema.prefixItems.every(isSchema)) {
|
|
156
|
+
throw new Error("prefixItems must be an array of schemas.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const keyword of schemaKeywords) {
|
|
160
|
+
const value = schema[keyword];
|
|
161
|
+
if (value === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
if (keyword === "items" && dialect === "draft7" && Array.isArray(value)) {
|
|
164
|
+
if (!value.every(isSchema))
|
|
165
|
+
throw new Error("items must contain only schemas.");
|
|
166
|
+
}
|
|
167
|
+
else if (!isSchema(value)) {
|
|
168
|
+
throw new Error(`${keyword} must be a schema.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (schema.enum !== undefined && !Array.isArray(schema.enum)) {
|
|
172
|
+
throw new Error("enum must be an array.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function compileGraph(schema, options = {}) {
|
|
176
|
+
if (!isSchema(schema)) {
|
|
177
|
+
throw new Error("JSON Schema must be a boolean or object.");
|
|
178
|
+
}
|
|
179
|
+
const locations = new Map();
|
|
180
|
+
const resources = new Map();
|
|
181
|
+
const anchors = new Map();
|
|
182
|
+
const dynamicAnchors = new Map();
|
|
183
|
+
const nodesBySchema = new WeakMap();
|
|
184
|
+
const pendingReferences = [];
|
|
185
|
+
const registry = { ...builtInRegistry, ...options.registry };
|
|
186
|
+
const rootBase = "https://toolcraft.invalid/root";
|
|
187
|
+
function validationVocabularyFor(schemaObject) {
|
|
188
|
+
const metaSchema = schemaObject.$schema;
|
|
189
|
+
if (typeof metaSchema !== "string") {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
const registered = registry[metaSchema];
|
|
193
|
+
if (!isObject(registered) || !isObject(registered.$vocabulary)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return Object.prototype.hasOwnProperty.call(registered.$vocabulary, "https://json-schema.org/draft/2020-12/vocab/validation");
|
|
197
|
+
}
|
|
198
|
+
function recordNode(schemaObject, node) {
|
|
199
|
+
if (schemaObject === undefined) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const nodes = nodesBySchema.get(schemaObject) ?? [];
|
|
203
|
+
nodes.push(node);
|
|
204
|
+
nodesBySchema.set(schemaObject, nodes);
|
|
205
|
+
}
|
|
206
|
+
function setLocation(dialect, uri, node) {
|
|
207
|
+
locations.set(mapKey(dialect, uri), node);
|
|
208
|
+
}
|
|
209
|
+
function scan(currentSchema, inheritedDialect, inheritedBase, resourceRoot, pointer, validationVocabulary, documentId) {
|
|
210
|
+
const schemaObject = isObject(currentSchema) ? currentSchema : undefined;
|
|
211
|
+
const dialect = schemaObject === undefined ? inheritedDialect : dialectFor(schemaObject, inheritedDialect);
|
|
212
|
+
if (schemaObject !== undefined) {
|
|
213
|
+
validateSchemaObject(schemaObject, dialect);
|
|
214
|
+
}
|
|
215
|
+
const declaredId = dialect === "draft7" ? (schemaObject?.$id ?? schemaObject?.id) : schemaObject?.$id;
|
|
216
|
+
const effectiveId = dialect === "draft7" && typeof schemaObject?.$ref === "string" ? undefined : declaredId;
|
|
217
|
+
const baseUri = typeof effectiveId === "string" ? resolveUri(effectiveId, inheritedBase) : inheritedBase;
|
|
218
|
+
const node = {
|
|
219
|
+
schema: currentSchema,
|
|
220
|
+
documentId,
|
|
221
|
+
dialect,
|
|
222
|
+
baseUri,
|
|
223
|
+
resourceUri: resourceRoot?.resourceUri ?? withoutFragment(baseUri),
|
|
224
|
+
resourceRoot: undefined,
|
|
225
|
+
pointer,
|
|
226
|
+
validationVocabulary,
|
|
227
|
+
children: new Map()
|
|
228
|
+
};
|
|
229
|
+
recordNode(schemaObject, node);
|
|
230
|
+
if (resourceRoot !== undefined) {
|
|
231
|
+
setLocation(resourceRoot.dialect, resolveUri(`#${encodeURI(pointer)}`, resourceRoot.resourceUri), node);
|
|
232
|
+
}
|
|
233
|
+
const startsResource = resourceRoot === undefined || withoutFragment(baseUri) !== resourceRoot.resourceUri;
|
|
234
|
+
node.resourceRoot = startsResource ? node : resourceRoot;
|
|
235
|
+
node.resourceUri = startsResource ? withoutFragment(baseUri) : resourceRoot.resourceUri;
|
|
236
|
+
const resourcePointer = startsResource ? "" : pointer;
|
|
237
|
+
node.pointer = resourcePointer;
|
|
238
|
+
if (startsResource) {
|
|
239
|
+
resources.set(mapKey(node.dialect, node.resourceUri), node);
|
|
240
|
+
setLocation(node.dialect, node.resourceUri, node);
|
|
241
|
+
}
|
|
242
|
+
setLocation(node.dialect, resolveUri(`#${encodeURI(resourcePointer)}`, node.resourceUri), node);
|
|
243
|
+
if (schemaObject === undefined) {
|
|
244
|
+
return node;
|
|
245
|
+
}
|
|
246
|
+
if (typeof schemaObject.$anchor === "string") {
|
|
247
|
+
anchors.set(mapKey(node.dialect, `${node.resourceUri}#${schemaObject.$anchor}`), node);
|
|
248
|
+
}
|
|
249
|
+
if (typeof schemaObject.$dynamicAnchor === "string") {
|
|
250
|
+
const anchorUri = `${node.resourceUri}#${schemaObject.$dynamicAnchor}`;
|
|
251
|
+
anchors.set(mapKey(node.dialect, anchorUri), node);
|
|
252
|
+
dynamicAnchors.set(mapKey(node.dialect, anchorUri), node);
|
|
253
|
+
}
|
|
254
|
+
if (dialect === "draft7" && typeof effectiveId === "string" && fragmentOf(baseUri) !== "") {
|
|
255
|
+
anchors.set(mapKey(dialect, baseUri), node);
|
|
256
|
+
}
|
|
257
|
+
for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"]) {
|
|
258
|
+
const reference = schemaObject[keyword];
|
|
259
|
+
if (reference !== undefined) {
|
|
260
|
+
if (typeof reference !== "string") {
|
|
261
|
+
throw new Error(`${keyword} must be a string.`);
|
|
262
|
+
}
|
|
263
|
+
pendingReferences.push({ node, reference });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (typeof schemaObject.pattern === "string") {
|
|
267
|
+
new RegExp(schemaObject.pattern, "u");
|
|
268
|
+
}
|
|
269
|
+
if (isObject(schemaObject.patternProperties)) {
|
|
270
|
+
for (const pattern of Object.keys(schemaObject.patternProperties)) {
|
|
271
|
+
new RegExp(pattern, "u");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const nextValidationVocabulary = resourceRoot === undefined ? validationVocabularyFor(schemaObject) : validationVocabulary;
|
|
275
|
+
for (const [keyword, value] of Object.entries(schemaObject)) {
|
|
276
|
+
if (schemaMapKeywords.has(keyword) && isObject(value)) {
|
|
277
|
+
for (const [key, childSchema] of Object.entries(value)) {
|
|
278
|
+
if (isSchema(childSchema)) {
|
|
279
|
+
node.children.set(`${keyword}/${key}`, scan(childSchema, dialect, baseUri, node.resourceRoot, `${resourcePointer}/${escapePointer(keyword)}/${escapePointer(key)}`, nextValidationVocabulary, documentId));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else if (schemaArrayKeywords.has(keyword) && Array.isArray(value)) {
|
|
284
|
+
value.forEach((childSchema, index) => {
|
|
285
|
+
if (isSchema(childSchema)) {
|
|
286
|
+
node.children.set(`${keyword}/${index}`, scan(childSchema, dialect, baseUri, node.resourceRoot, `${resourcePointer}/${escapePointer(keyword)}/${index}`, nextValidationVocabulary, documentId));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (schemaKeywords.has(keyword)) {
|
|
291
|
+
if (keyword === "items" && Array.isArray(value)) {
|
|
292
|
+
value.forEach((childSchema, index) => {
|
|
293
|
+
if (isSchema(childSchema)) {
|
|
294
|
+
node.children.set(`items/${index}`, scan(childSchema, dialect, baseUri, node.resourceRoot, `${resourcePointer}/items/${index}`, nextValidationVocabulary, documentId));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else if (isSchema(value)) {
|
|
299
|
+
node.children.set(keyword, scan(value, dialect, baseUri, node.resourceRoot, `${resourcePointer}/${escapePointer(keyword)}`, nextValidationVocabulary, documentId));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return node;
|
|
304
|
+
}
|
|
305
|
+
const root = scan(schema, "draft2020-12", rootBase, undefined, "", true, "root");
|
|
306
|
+
resources.set(mapKey(root.dialect, rootBase), root);
|
|
307
|
+
setLocation(root.dialect, rootBase, root);
|
|
308
|
+
setLocation(root.dialect, `${rootBase}#`, root);
|
|
309
|
+
for (const [uri, registeredSchema] of Object.entries(registry)) {
|
|
310
|
+
if (!isSchema(registeredSchema)) {
|
|
311
|
+
throw new Error(`Registered JSON Schema ${uri} must be a boolean or object.`);
|
|
312
|
+
}
|
|
313
|
+
for (const inheritedDialect of ["draft2020-12", "draft7"]) {
|
|
314
|
+
const remote = scan(registeredSchema, inheritedDialect, uri, undefined, "", true, `${inheritedDialect}:${uri}`);
|
|
315
|
+
resources.set(mapKey(remote.dialect, withoutFragment(uri)), remote);
|
|
316
|
+
setLocation(remote.dialect, withoutFragment(uri), remote);
|
|
317
|
+
setLocation(remote.dialect, `${withoutFragment(uri)}#`, remote);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function findByDialect(map, dialect, uri) {
|
|
321
|
+
const alternate = dialect === "draft7" ? "draft2020-12" : "draft7";
|
|
322
|
+
return map.get(mapKey(dialect, uri)) ?? map.get(mapKey(alternate, uri));
|
|
323
|
+
}
|
|
324
|
+
function resolve(node, reference) {
|
|
325
|
+
const absolute = resolveUri(reference, node.baseUri);
|
|
326
|
+
const direct = findByDialect(locations, node.dialect, absolute) ??
|
|
327
|
+
findByDialect(resources, node.dialect, absolute) ??
|
|
328
|
+
findByDialect(anchors, node.dialect, absolute);
|
|
329
|
+
if (direct !== undefined) {
|
|
330
|
+
return direct;
|
|
331
|
+
}
|
|
332
|
+
const resource = findByDialect(resources, node.dialect, withoutFragment(absolute));
|
|
333
|
+
const fragment = fragmentOf(absolute);
|
|
334
|
+
if (resource !== undefined && fragment === "") {
|
|
335
|
+
return resource;
|
|
336
|
+
}
|
|
337
|
+
if (fragment.startsWith("/")) {
|
|
338
|
+
const pointerTarget = findByDialect(locations, node.dialect, `${withoutFragment(absolute)}#${fragment}`);
|
|
339
|
+
if (pointerTarget !== undefined) {
|
|
340
|
+
return pointerTarget;
|
|
341
|
+
}
|
|
342
|
+
if (resource !== undefined && isObject(resource.schema)) {
|
|
343
|
+
let target = resource.schema;
|
|
344
|
+
for (const segment of fragment.slice(1).split("/")) {
|
|
345
|
+
if (!isObject(target) && !Array.isArray(target)) {
|
|
346
|
+
target = undefined;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
const key = decodeURIComponent(segment).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
350
|
+
target = Array.isArray(target) ? target[Number(key)] : target[key];
|
|
351
|
+
}
|
|
352
|
+
if (isObject(target)) {
|
|
353
|
+
const targetNode = nodesBySchema
|
|
354
|
+
.get(target)
|
|
355
|
+
?.find((candidate) => candidate.dialect === node.dialect);
|
|
356
|
+
if (targetNode !== undefined) {
|
|
357
|
+
return targetNode;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
throw new Error(`Unresolvable $ref: ${reference}`);
|
|
363
|
+
}
|
|
364
|
+
const reachableDocuments = new Set([root.documentId]);
|
|
365
|
+
let discoveredDocument = true;
|
|
366
|
+
while (discoveredDocument) {
|
|
367
|
+
discoveredDocument = false;
|
|
368
|
+
for (const pending of pendingReferences) {
|
|
369
|
+
if (!reachableDocuments.has(pending.node.documentId)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const target = resolve(pending.node, pending.reference);
|
|
373
|
+
if (!reachableDocuments.has(target.documentId)) {
|
|
374
|
+
reachableDocuments.add(target.documentId);
|
|
375
|
+
discoveredDocument = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
root,
|
|
381
|
+
locations,
|
|
382
|
+
resources,
|
|
383
|
+
anchors,
|
|
384
|
+
dynamicAnchors,
|
|
385
|
+
resolve,
|
|
386
|
+
dynamicAnchor(scope, name) {
|
|
387
|
+
return dynamicAnchors.get(mapKey(scope.dialect, `${scope.resourceUri}#${name}`));
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { deepEqual, invalidResult, isMultipleOf, isObject, issue, mergeResults, typeMatches, unicodeLength, validResult } from "./utils.js";
|
|
2
|
+
export function evaluateSchema(graph, value) {
|
|
3
|
+
return evaluateNode(graph, graph.root, value, {
|
|
4
|
+
instancePath: [],
|
|
5
|
+
dynamicScope: [],
|
|
6
|
+
activePairs: new Map()
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function evaluateNode(graph, node, value, context) {
|
|
10
|
+
if (node.schema === true) {
|
|
11
|
+
return validResult();
|
|
12
|
+
}
|
|
13
|
+
if (node.schema === false) {
|
|
14
|
+
return invalidResult(issue(context.instancePath, "valid schema", value, "must NOT be valid"));
|
|
15
|
+
}
|
|
16
|
+
const activeValues = context.activePairs.get(node);
|
|
17
|
+
if (activeValues?.has(value) === true) {
|
|
18
|
+
return validResult();
|
|
19
|
+
}
|
|
20
|
+
const nextActivePairs = new Map(context.activePairs);
|
|
21
|
+
const nextActiveValues = new Set(activeValues ?? []);
|
|
22
|
+
nextActiveValues.add(value);
|
|
23
|
+
nextActivePairs.set(node, nextActiveValues);
|
|
24
|
+
const lastScope = context.dynamicScope.at(-1);
|
|
25
|
+
const dynamicScope = lastScope?.resourceUri === node.resourceRoot.resourceUri
|
|
26
|
+
? context.dynamicScope
|
|
27
|
+
: [...context.dynamicScope, node.resourceRoot];
|
|
28
|
+
const nextContext = { ...context, dynamicScope, activePairs: nextActivePairs };
|
|
29
|
+
const schema = node.schema;
|
|
30
|
+
const referenceResult = evaluateReferences(graph, node, schema, value, nextContext);
|
|
31
|
+
if (node.dialect === "draft7" && typeof schema.$ref === "string") {
|
|
32
|
+
return referenceResult ?? validResult();
|
|
33
|
+
}
|
|
34
|
+
const results = [];
|
|
35
|
+
if (referenceResult !== undefined) {
|
|
36
|
+
results.push(referenceResult);
|
|
37
|
+
}
|
|
38
|
+
results.push(...evaluateApplicators(graph, node, schema, value, nextContext));
|
|
39
|
+
if (node.validationVocabulary) {
|
|
40
|
+
results.push(...evaluateValidationKeywords(node, schema, value, nextContext.instancePath));
|
|
41
|
+
}
|
|
42
|
+
const merged = mergeResults(results);
|
|
43
|
+
if (isObject(value)) {
|
|
44
|
+
evaluateUnevaluatedProperties(graph, node, schema, value, nextContext, merged);
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
evaluateUnevaluatedItems(graph, node, schema, value, nextContext, merged);
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
function evaluateReferences(graph, node, schema, value, context) {
|
|
52
|
+
const results = [];
|
|
53
|
+
if (typeof schema.$ref === "string") {
|
|
54
|
+
results.push(evaluateNode(graph, graph.resolve(node, schema.$ref), value, context));
|
|
55
|
+
}
|
|
56
|
+
if (typeof schema.$dynamicRef === "string") {
|
|
57
|
+
const staticTarget = graph.resolve(node, schema.$dynamicRef);
|
|
58
|
+
const fragment = new URL(schema.$dynamicRef, node.baseUri).hash.slice(1);
|
|
59
|
+
let target = staticTarget;
|
|
60
|
+
if (fragment !== "" &&
|
|
61
|
+
!fragment.startsWith("/") &&
|
|
62
|
+
isObject(staticTarget.schema) &&
|
|
63
|
+
staticTarget.schema.$dynamicAnchor === fragment) {
|
|
64
|
+
for (const scope of context.dynamicScope) {
|
|
65
|
+
const candidate = graph.dynamicAnchor(scope, fragment);
|
|
66
|
+
if (candidate !== undefined) {
|
|
67
|
+
target = candidate;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
results.push(evaluateNode(graph, target, value, context));
|
|
73
|
+
}
|
|
74
|
+
if (typeof schema.$recursiveRef === "string") {
|
|
75
|
+
let target = graph.resolve(node, schema.$recursiveRef);
|
|
76
|
+
if (schema.$recursiveRef === "#") {
|
|
77
|
+
for (const scope of context.dynamicScope) {
|
|
78
|
+
if (isObject(scope.schema) && scope.schema.$recursiveAnchor === true) {
|
|
79
|
+
target = scope;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
results.push(evaluateNode(graph, target, value, context));
|
|
85
|
+
}
|
|
86
|
+
return results.length === 0 ? undefined : mergeResults(results);
|
|
87
|
+
}
|
|
88
|
+
function evaluateApplicators(graph, node, schema, value, context) {
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const keyword of ["allOf", "anyOf", "oneOf"]) {
|
|
91
|
+
const schemas = schema[keyword];
|
|
92
|
+
if (!Array.isArray(schemas)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const branchResults = schemas.map((_, index) => evaluateChild(graph, node, `${keyword}/${index}`, value, context));
|
|
96
|
+
if (keyword === "allOf") {
|
|
97
|
+
results.push(mergeResults(branchResults));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const successful = branchResults.filter((result) => result.valid);
|
|
101
|
+
const valid = keyword === "anyOf" ? successful.length > 0 : successful.length === 1;
|
|
102
|
+
if (valid) {
|
|
103
|
+
results.push(mergeResults(successful));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
results.push(invalidResult(issue(context.instancePath, keyword, value, keyword === "anyOf"
|
|
107
|
+
? "must match a schema in anyOf"
|
|
108
|
+
: "must match exactly one schema in oneOf")));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (schema.not !== undefined) {
|
|
113
|
+
const result = evaluateChild(graph, node, "not", value, context);
|
|
114
|
+
if (result.valid) {
|
|
115
|
+
results.push(invalidResult(issue(context.instancePath, "not", value, "must NOT be valid")));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (schema.if !== undefined) {
|
|
119
|
+
const condition = evaluateChild(graph, node, "if", value, context);
|
|
120
|
+
const selected = condition.valid ? "then" : "else";
|
|
121
|
+
if (condition.valid) {
|
|
122
|
+
results.push(condition);
|
|
123
|
+
}
|
|
124
|
+
if (schema[selected] !== undefined) {
|
|
125
|
+
results.push(evaluateChild(graph, node, selected, value, context));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (isObject(value)) {
|
|
129
|
+
results.push(...evaluateObjectApplicators(graph, node, schema, value, context));
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(value)) {
|
|
132
|
+
results.push(...evaluateArrayApplicators(graph, node, schema, value, context));
|
|
133
|
+
}
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
function evaluateObjectApplicators(graph, node, schema, value, context) {
|
|
137
|
+
const results = [];
|
|
138
|
+
const evaluated = new Set();
|
|
139
|
+
const properties = isObject(schema.properties) ? schema.properties : {};
|
|
140
|
+
for (const key of Object.keys(properties)) {
|
|
141
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
142
|
+
const result = evaluateChild(graph, node, `properties/${key}`, value[key], childContext(context, key));
|
|
143
|
+
evaluated.add(key);
|
|
144
|
+
results.push(markProperty(atChildLocation(result), key));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const patternProperties = isObject(schema.patternProperties) ? schema.patternProperties : {};
|
|
148
|
+
for (const [pattern] of Object.entries(patternProperties)) {
|
|
149
|
+
const expression = new RegExp(pattern, "u");
|
|
150
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
151
|
+
if (expression.test(key)) {
|
|
152
|
+
const result = evaluateChild(graph, node, `patternProperties/${pattern}`, propertyValue, childContext(context, key));
|
|
153
|
+
evaluated.add(key);
|
|
154
|
+
results.push(markProperty(atChildLocation(result), key));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (schema.additionalProperties !== undefined) {
|
|
159
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
160
|
+
if (!evaluated.has(key)) {
|
|
161
|
+
const result = evaluateChild(graph, node, "additionalProperties", propertyValue, childContext(context, key));
|
|
162
|
+
results.push(markProperty(atChildLocation(result), key));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (schema.propertyNames !== undefined) {
|
|
167
|
+
for (const key of Object.keys(value)) {
|
|
168
|
+
results.push(atChildLocation(evaluateChild(graph, node, "propertyNames", key, childContext(context, key))));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const dependentSchemas = isObject(schema.dependentSchemas) ? schema.dependentSchemas : {};
|
|
172
|
+
for (const key of Object.keys(dependentSchemas)) {
|
|
173
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
174
|
+
results.push(evaluateChild(graph, node, `dependentSchemas/${key}`, value, context));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (node.dialect === "draft7" && isObject(schema.dependencies)) {
|
|
178
|
+
for (const [key, dependency] of Object.entries(schema.dependencies)) {
|
|
179
|
+
if (Object.prototype.hasOwnProperty.call(value, key) && !Array.isArray(dependency)) {
|
|
180
|
+
results.push(evaluateInline(graph, node, dependency, value, context, `dependencies/${key}`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
function evaluateArrayApplicators(graph, node, schema, value, context) {
|
|
187
|
+
const results = [];
|
|
188
|
+
if (node.dialect === "draft2020-12") {
|
|
189
|
+
const prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems : [];
|
|
190
|
+
prefixItems.forEach((_, index) => {
|
|
191
|
+
if (index < value.length) {
|
|
192
|
+
const result = evaluateChild(graph, node, `prefixItems/${index}`, value[index], childContext(context, String(index)));
|
|
193
|
+
results.push(markItem(atChildLocation(result), index));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (schema.items !== undefined && !Array.isArray(schema.items)) {
|
|
197
|
+
for (let index = prefixItems.length; index < value.length; index += 1) {
|
|
198
|
+
const result = evaluateChild(graph, node, "items", value[index], childContext(context, String(index)));
|
|
199
|
+
results.push(markItem(atChildLocation(result), index));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (Array.isArray(schema.items)) {
|
|
204
|
+
schema.items.forEach((_, index) => {
|
|
205
|
+
if (index < value.length) {
|
|
206
|
+
const result = evaluateChild(graph, node, `items/${index}`, value[index], childContext(context, String(index)));
|
|
207
|
+
results.push(markItem(atChildLocation(result), index));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (schema.additionalItems !== undefined) {
|
|
211
|
+
for (let index = schema.items.length; index < value.length; index += 1) {
|
|
212
|
+
const result = evaluateChild(graph, node, "additionalItems", value[index], childContext(context, String(index)));
|
|
213
|
+
results.push(markItem(atChildLocation(result), index));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (schema.items !== undefined) {
|
|
218
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
219
|
+
const result = evaluateChild(graph, node, "items", value[index], childContext(context, String(index)));
|
|
220
|
+
results.push(markItem(atChildLocation(result), index));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (schema.contains !== undefined) {
|
|
224
|
+
const containsResults = value.map((item, index) => evaluateChild(graph, node, "contains", item, childContext(context, String(index))));
|
|
225
|
+
const matching = containsResults
|
|
226
|
+
.map((result, index) => ({ result, index }))
|
|
227
|
+
.filter(({ result }) => result.valid);
|
|
228
|
+
const minimum = node.dialect === "draft2020-12" && typeof schema.minContains === "number"
|
|
229
|
+
? schema.minContains
|
|
230
|
+
: 1;
|
|
231
|
+
const maximum = node.dialect === "draft2020-12" && typeof schema.maxContains === "number"
|
|
232
|
+
? schema.maxContains
|
|
233
|
+
: Number.POSITIVE_INFINITY;
|
|
234
|
+
if (matching.length < minimum || matching.length > maximum) {
|
|
235
|
+
results.push(invalidResult(issue(context.instancePath, "contains", value, "must contain required matching items")));
|
|
236
|
+
}
|
|
237
|
+
else if (node.dialect === "draft2020-12") {
|
|
238
|
+
const result = validResult();
|
|
239
|
+
for (const match of matching) {
|
|
240
|
+
result.evaluatedItems.add(match.index);
|
|
241
|
+
}
|
|
242
|
+
results.push(result);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
function evaluateValidationKeywords(node, schema, value, path) {
|
|
248
|
+
const results = [];
|
|
249
|
+
if (schema.nullable === true && value === null) {
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
252
|
+
const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
|
|
253
|
+
if (types.length > 0 &&
|
|
254
|
+
!types.some((type) => typeof type === "string" && typeMatches(type, value))) {
|
|
255
|
+
results.push(invalidResult(issue(path, types.join(","), value, `must be ${types.join(",")}`)));
|
|
256
|
+
return results;
|
|
257
|
+
}
|
|
258
|
+
if (schema.const !== undefined && !deepEqual(schema.const, value)) {
|
|
259
|
+
results.push(invalidResult(issue(path, "const", value, "must be equal to constant")));
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((entry) => deepEqual(entry, value))) {
|
|
262
|
+
results.push(invalidResult(issue(path, "enum", value, "must be equal to one of the allowed values")));
|
|
263
|
+
}
|
|
264
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
265
|
+
evaluateNumberKeywords(schema, value, path, results);
|
|
266
|
+
}
|
|
267
|
+
if (typeof value === "string") {
|
|
268
|
+
evaluateStringKeywords(schema, value, path, results);
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
evaluateArrayKeywords(schema, value, path, results);
|
|
272
|
+
}
|
|
273
|
+
if (isObject(value)) {
|
|
274
|
+
evaluateObjectKeywords(node, schema, value, path, results);
|
|
275
|
+
}
|
|
276
|
+
return results;
|
|
277
|
+
}
|
|
278
|
+
function evaluateNumberKeywords(schema, value, path, results) {
|
|
279
|
+
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
280
|
+
results.push(invalidResult(issue(path, `multiple of ${schema.multipleOf}`, value, `must be multiple of ${schema.multipleOf}`)));
|
|
281
|
+
}
|
|
282
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) {
|
|
283
|
+
results.push(invalidResult(issue(path, `<= ${schema.maximum}`, value, `must be <= ${schema.maximum}`)));
|
|
284
|
+
}
|
|
285
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) {
|
|
286
|
+
results.push(invalidResult(issue(path, `>= ${schema.minimum}`, value, `must be >= ${schema.minimum}`)));
|
|
287
|
+
}
|
|
288
|
+
if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum) {
|
|
289
|
+
results.push(invalidResult(issue(path, `< ${schema.exclusiveMaximum}`, value, `must be < ${schema.exclusiveMaximum}`)));
|
|
290
|
+
}
|
|
291
|
+
if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum) {
|
|
292
|
+
results.push(invalidResult(issue(path, `> ${schema.exclusiveMinimum}`, value, `must be > ${schema.exclusiveMinimum}`)));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function evaluateStringKeywords(schema, value, path, results) {
|
|
296
|
+
const length = unicodeLength(value);
|
|
297
|
+
if (typeof schema.maxLength === "number" && length > schema.maxLength) {
|
|
298
|
+
results.push(invalidResult(issue(path, `length <= ${schema.maxLength}`, value, `must NOT have more than ${schema.maxLength} characters`)));
|
|
299
|
+
}
|
|
300
|
+
if (typeof schema.minLength === "number" && length < schema.minLength) {
|
|
301
|
+
results.push(invalidResult(issue(path, `length >= ${schema.minLength}`, value, `must NOT have fewer than ${schema.minLength} characters`)));
|
|
302
|
+
}
|
|
303
|
+
if (typeof schema.pattern === "string" && !new RegExp(schema.pattern, "u").test(value)) {
|
|
304
|
+
results.push(invalidResult(issue(path, `pattern ${schema.pattern}`, value, `must match pattern ${schema.pattern}`)));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function evaluateArrayKeywords(schema, value, path, results) {
|
|
308
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
309
|
+
results.push(invalidResult(issue(path, `items <= ${schema.maxItems}`, value, `must NOT have more than ${schema.maxItems} items`)));
|
|
310
|
+
}
|
|
311
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
312
|
+
results.push(invalidResult(issue(path, `items >= ${schema.minItems}`, value, `must NOT have fewer than ${schema.minItems} items`)));
|
|
313
|
+
}
|
|
314
|
+
if (schema.uniqueItems === true) {
|
|
315
|
+
for (let left = 0; left < value.length; left += 1) {
|
|
316
|
+
for (let right = left + 1; right < value.length; right += 1) {
|
|
317
|
+
if (deepEqual(value[left], value[right])) {
|
|
318
|
+
results.push(invalidResult(issue(path, "unique items", value, "must NOT have duplicate items")));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function evaluateObjectKeywords(node, schema, value, path, results) {
|
|
326
|
+
const keys = Object.keys(value);
|
|
327
|
+
if (typeof schema.maxProperties === "number" && keys.length > schema.maxProperties) {
|
|
328
|
+
results.push(invalidResult(issue(path, `properties <= ${schema.maxProperties}`, value, `must NOT have more than ${schema.maxProperties} properties`)));
|
|
329
|
+
}
|
|
330
|
+
if (typeof schema.minProperties === "number" && keys.length < schema.minProperties) {
|
|
331
|
+
results.push(invalidResult(issue(path, `properties >= ${schema.minProperties}`, value, `must NOT have fewer than ${schema.minProperties} properties`)));
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(schema.required)) {
|
|
334
|
+
for (const key of schema.required) {
|
|
335
|
+
if (typeof key === "string" && !Object.prototype.hasOwnProperty.call(value, key)) {
|
|
336
|
+
results.push(invalidResult(issue([...path, key], "required", undefined, `must have required property '${key}'`)));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const dependentRequired = isObject(schema.dependentRequired) ? schema.dependentRequired : {};
|
|
341
|
+
for (const [key, dependencies] of Object.entries(dependentRequired)) {
|
|
342
|
+
if (Object.prototype.hasOwnProperty.call(value, key) && Array.isArray(dependencies)) {
|
|
343
|
+
addMissingDependencies(value, dependencies, path, results);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (node.dialect === "draft7" && isObject(schema.dependencies)) {
|
|
347
|
+
for (const [key, dependencies] of Object.entries(schema.dependencies)) {
|
|
348
|
+
if (Object.prototype.hasOwnProperty.call(value, key) && Array.isArray(dependencies)) {
|
|
349
|
+
addMissingDependencies(value, dependencies, path, results);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function addMissingDependencies(value, dependencies, path, results) {
|
|
355
|
+
for (const dependency of dependencies) {
|
|
356
|
+
if (typeof dependency === "string" &&
|
|
357
|
+
!Object.prototype.hasOwnProperty.call(value, dependency)) {
|
|
358
|
+
results.push(invalidResult(issue([...path, dependency], "dependency", undefined, `must have property '${dependency}'`)));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function evaluateUnevaluatedProperties(graph, node, schema, value, context, result) {
|
|
363
|
+
if (node.dialect !== "draft2020-12" || schema.unevaluatedProperties === undefined) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
367
|
+
if (!result.evaluatedProperties.has(key)) {
|
|
368
|
+
const propertyResult = evaluateChild(graph, node, "unevaluatedProperties", propertyValue, childContext(context, key));
|
|
369
|
+
propertyResult.evaluatedProperties.add(key);
|
|
370
|
+
mergeInto(result, propertyResult);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function evaluateUnevaluatedItems(graph, node, schema, value, context, result) {
|
|
375
|
+
if (node.dialect !== "draft2020-12" || schema.unevaluatedItems === undefined) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
379
|
+
if (!result.evaluatedItems.has(index)) {
|
|
380
|
+
const itemResult = evaluateChild(graph, node, "unevaluatedItems", value[index], childContext(context, String(index)));
|
|
381
|
+
itemResult.evaluatedItems.add(index);
|
|
382
|
+
mergeInto(result, itemResult);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function evaluateChild(graph, node, key, value, context) {
|
|
387
|
+
const child = node.children.get(key);
|
|
388
|
+
if (child === undefined) {
|
|
389
|
+
return validResult();
|
|
390
|
+
}
|
|
391
|
+
return evaluateNode(graph, child, value, context);
|
|
392
|
+
}
|
|
393
|
+
function evaluateInline(graph, node, schema, value, context, key) {
|
|
394
|
+
const child = node.children.get(key);
|
|
395
|
+
if (child !== undefined) {
|
|
396
|
+
return evaluateNode(graph, child, value, context);
|
|
397
|
+
}
|
|
398
|
+
if (typeof schema !== "boolean" && !isObject(schema)) {
|
|
399
|
+
return validResult();
|
|
400
|
+
}
|
|
401
|
+
const inlineNode = {
|
|
402
|
+
schema: schema,
|
|
403
|
+
documentId: node.documentId,
|
|
404
|
+
dialect: node.dialect,
|
|
405
|
+
baseUri: node.baseUri,
|
|
406
|
+
resourceUri: node.resourceUri,
|
|
407
|
+
resourceRoot: node.resourceRoot,
|
|
408
|
+
pointer: node.pointer,
|
|
409
|
+
validationVocabulary: node.validationVocabulary,
|
|
410
|
+
children: new Map()
|
|
411
|
+
};
|
|
412
|
+
return evaluateNode(graph, inlineNode, value, context);
|
|
413
|
+
}
|
|
414
|
+
function childContext(context, segment) {
|
|
415
|
+
return { ...context, instancePath: [...context.instancePath, segment] };
|
|
416
|
+
}
|
|
417
|
+
function mergeInto(target, source) {
|
|
418
|
+
target.valid &&= source.valid;
|
|
419
|
+
target.issues.push(...source.issues);
|
|
420
|
+
for (const key of source.evaluatedProperties) {
|
|
421
|
+
target.evaluatedProperties.add(key);
|
|
422
|
+
}
|
|
423
|
+
for (const index of source.evaluatedItems) {
|
|
424
|
+
target.evaluatedItems.add(index);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function atChildLocation(result) {
|
|
428
|
+
return {
|
|
429
|
+
valid: result.valid,
|
|
430
|
+
issues: result.issues,
|
|
431
|
+
evaluatedProperties: new Set(),
|
|
432
|
+
evaluatedItems: new Set()
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function markProperty(result, key) {
|
|
436
|
+
result.evaluatedProperties.add(key);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
function markItem(result, index) {
|
|
440
|
+
result.evaluatedItems.add(index);
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ValidationIssue } from "../validate.js";
|
|
2
|
+
import type { CompileJsonSchemaOptions, CompiledJsonSchema } from "./types.js";
|
|
3
|
+
export type { CompileJsonSchemaOptions, CompiledJsonSchema } from "./types.js";
|
|
4
|
+
export declare function compileJsonSchema(schema: unknown, options?: CompileJsonSchemaOptions): CompiledJsonSchema;
|
|
5
|
+
export declare function formatIssues(issues: readonly ValidationIssue[]): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { compileGraph } from "./compiler.js";
|
|
2
|
+
import { evaluateSchema } from "./evaluate.js";
|
|
3
|
+
export function compileJsonSchema(schema, options = {}) {
|
|
4
|
+
const graph = compileGraph(schema, options);
|
|
5
|
+
return {
|
|
6
|
+
validate(value) {
|
|
7
|
+
const result = evaluateSchema(graph, value);
|
|
8
|
+
return result.valid ? { ok: true, value } : { ok: false, issues: result.issues };
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function formatIssues(issues) {
|
|
13
|
+
return issues
|
|
14
|
+
.map((current) => {
|
|
15
|
+
const path = current.path.length === 0 ? "data" : `data/${current.path.join("/")}`;
|
|
16
|
+
return `${path} ${current.message}`;
|
|
17
|
+
})
|
|
18
|
+
.join(", ");
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ValidationIssue, ValidationResult } from "../validate.js";
|
|
2
|
+
export type JsonSchemaRegistry = Record<string, unknown>;
|
|
3
|
+
export interface CompileJsonSchemaOptions {
|
|
4
|
+
registry?: JsonSchemaRegistry;
|
|
5
|
+
}
|
|
6
|
+
export interface CompiledJsonSchema {
|
|
7
|
+
validate(value: unknown): ValidationResult<unknown>;
|
|
8
|
+
}
|
|
9
|
+
export type Dialect = "draft7" | "draft2020-12";
|
|
10
|
+
export type SchemaObject = Record<string, unknown>;
|
|
11
|
+
export type JsonSchema = boolean | SchemaObject;
|
|
12
|
+
export interface SchemaNode {
|
|
13
|
+
schema: JsonSchema;
|
|
14
|
+
documentId: string;
|
|
15
|
+
dialect: Dialect;
|
|
16
|
+
baseUri: string;
|
|
17
|
+
resourceUri: string;
|
|
18
|
+
resourceRoot: SchemaNode;
|
|
19
|
+
pointer: string;
|
|
20
|
+
validationVocabulary: boolean;
|
|
21
|
+
children: Map<string, SchemaNode>;
|
|
22
|
+
}
|
|
23
|
+
export interface EvaluationResult {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
issues: ValidationIssue[];
|
|
26
|
+
evaluatedProperties: Set<string>;
|
|
27
|
+
evaluatedItems: Set<number>;
|
|
28
|
+
}
|
|
29
|
+
export interface EvaluationContext {
|
|
30
|
+
instancePath: readonly string[];
|
|
31
|
+
dynamicScope: readonly SchemaNode[];
|
|
32
|
+
activePairs: Map<SchemaNode, Set<unknown>>;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ValidationIssue } from "../validate.js";
|
|
2
|
+
import type { Dialect, EvaluationResult, JsonSchema, SchemaObject } from "./types.js";
|
|
3
|
+
export declare function isSchema(value: unknown): value is JsonSchema;
|
|
4
|
+
export declare function isObject(value: unknown): value is SchemaObject;
|
|
5
|
+
export declare function dialectFor(schema: SchemaObject, inherited: Dialect): Dialect;
|
|
6
|
+
export declare function resolveUri(reference: string, baseUri: string): string;
|
|
7
|
+
export declare function withoutFragment(uri: string): string;
|
|
8
|
+
export declare function fragmentOf(uri: string): string;
|
|
9
|
+
export declare function escapePointer(value: string): string;
|
|
10
|
+
export declare function decodePointer(value: string): string;
|
|
11
|
+
export declare function deepEqual(left: unknown, right: unknown): boolean;
|
|
12
|
+
export declare function receivedType(value: unknown): string;
|
|
13
|
+
export declare function issue(path: readonly string[], expected: string, value: unknown, message: string, keyword?: string): ValidationIssue;
|
|
14
|
+
export declare function validResult(): EvaluationResult;
|
|
15
|
+
export declare function invalidResult(problem: ValidationIssue): EvaluationResult;
|
|
16
|
+
export declare function mergeResults(results: readonly EvaluationResult[]): EvaluationResult;
|
|
17
|
+
export declare function typeMatches(type: string, value: unknown): boolean;
|
|
18
|
+
export declare function unicodeLength(value: string): number;
|
|
19
|
+
export declare function isMultipleOf(value: number, divisor: number): boolean;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export function isSchema(value) {
|
|
2
|
+
return typeof value === "boolean" || isObject(value);
|
|
3
|
+
}
|
|
4
|
+
export function isObject(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export function dialectFor(schema, inherited) {
|
|
8
|
+
const dialect = schema.$schema;
|
|
9
|
+
if (typeof dialect !== "string") {
|
|
10
|
+
return inherited;
|
|
11
|
+
}
|
|
12
|
+
if (dialect.includes("draft-07")) {
|
|
13
|
+
return "draft7";
|
|
14
|
+
}
|
|
15
|
+
if (dialect.includes("2020-12")) {
|
|
16
|
+
return "draft2020-12";
|
|
17
|
+
}
|
|
18
|
+
return inherited;
|
|
19
|
+
}
|
|
20
|
+
export function resolveUri(reference, baseUri) {
|
|
21
|
+
try {
|
|
22
|
+
return new URL(reference, baseUri).href;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error(`Invalid schema URI: ${reference}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function withoutFragment(uri) {
|
|
29
|
+
const index = uri.indexOf("#");
|
|
30
|
+
return index === -1 ? uri : uri.slice(0, index);
|
|
31
|
+
}
|
|
32
|
+
export function fragmentOf(uri) {
|
|
33
|
+
const index = uri.indexOf("#");
|
|
34
|
+
return index === -1 ? "" : uri.slice(index + 1);
|
|
35
|
+
}
|
|
36
|
+
export function escapePointer(value) {
|
|
37
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
38
|
+
}
|
|
39
|
+
export function decodePointer(value) {
|
|
40
|
+
return decodeURIComponent(value).replaceAll("~1", "/").replaceAll("~0", "~");
|
|
41
|
+
}
|
|
42
|
+
export function deepEqual(left, right) {
|
|
43
|
+
if (Object.is(left, right)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (typeof left !== typeof right || left === null || right === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(left)) {
|
|
50
|
+
return (Array.isArray(right) &&
|
|
51
|
+
left.length === right.length &&
|
|
52
|
+
left.every((value, index) => deepEqual(value, right[index])));
|
|
53
|
+
}
|
|
54
|
+
if (isObject(left) && isObject(right)) {
|
|
55
|
+
const leftKeys = Object.keys(left);
|
|
56
|
+
const rightKeys = Object.keys(right);
|
|
57
|
+
return (leftKeys.length === rightKeys.length &&
|
|
58
|
+
leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && deepEqual(left[key], right[key])));
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
export function receivedType(value) {
|
|
63
|
+
if (value === null) {
|
|
64
|
+
return "null";
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return "array";
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
70
|
+
return "integer";
|
|
71
|
+
}
|
|
72
|
+
return typeof value;
|
|
73
|
+
}
|
|
74
|
+
export function issue(path, expected, value, message, keyword = keywordFor(expected)) {
|
|
75
|
+
return { path, expected, received: receivedType(value), message, keyword };
|
|
76
|
+
}
|
|
77
|
+
function keywordFor(expected) {
|
|
78
|
+
if (["null", "boolean", "object", "array", "number", "integer", "string"].includes(expected)) {
|
|
79
|
+
return "type";
|
|
80
|
+
}
|
|
81
|
+
if (expected.startsWith("multiple of"))
|
|
82
|
+
return "multipleOf";
|
|
83
|
+
if (expected.startsWith("length <="))
|
|
84
|
+
return "maxLength";
|
|
85
|
+
if (expected.startsWith("length >="))
|
|
86
|
+
return "minLength";
|
|
87
|
+
if (expected.startsWith("pattern "))
|
|
88
|
+
return "pattern";
|
|
89
|
+
if (expected.startsWith("items <="))
|
|
90
|
+
return "maxItems";
|
|
91
|
+
if (expected.startsWith("items >="))
|
|
92
|
+
return "minItems";
|
|
93
|
+
if (expected === "unique items")
|
|
94
|
+
return "uniqueItems";
|
|
95
|
+
if (expected.startsWith("properties <="))
|
|
96
|
+
return "maxProperties";
|
|
97
|
+
if (expected.startsWith("properties >="))
|
|
98
|
+
return "minProperties";
|
|
99
|
+
if (expected.startsWith("<= "))
|
|
100
|
+
return "maximum";
|
|
101
|
+
if (expected.startsWith(">= "))
|
|
102
|
+
return "minimum";
|
|
103
|
+
if (expected.startsWith("< "))
|
|
104
|
+
return "exclusiveMaximum";
|
|
105
|
+
if (expected.startsWith("> "))
|
|
106
|
+
return "exclusiveMinimum";
|
|
107
|
+
if (expected === "valid schema")
|
|
108
|
+
return "false schema";
|
|
109
|
+
return expected;
|
|
110
|
+
}
|
|
111
|
+
export function validResult() {
|
|
112
|
+
return {
|
|
113
|
+
valid: true,
|
|
114
|
+
issues: [],
|
|
115
|
+
evaluatedProperties: new Set(),
|
|
116
|
+
evaluatedItems: new Set()
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function invalidResult(problem) {
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
issues: [problem],
|
|
123
|
+
evaluatedProperties: new Set(),
|
|
124
|
+
evaluatedItems: new Set()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export function mergeResults(results) {
|
|
128
|
+
const merged = validResult();
|
|
129
|
+
for (const result of results) {
|
|
130
|
+
merged.valid &&= result.valid;
|
|
131
|
+
merged.issues.push(...result.issues);
|
|
132
|
+
for (const key of result.evaluatedProperties) {
|
|
133
|
+
merged.evaluatedProperties.add(key);
|
|
134
|
+
}
|
|
135
|
+
for (const index of result.evaluatedItems) {
|
|
136
|
+
merged.evaluatedItems.add(index);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return merged;
|
|
140
|
+
}
|
|
141
|
+
export function typeMatches(type, value) {
|
|
142
|
+
switch (type) {
|
|
143
|
+
case "null":
|
|
144
|
+
return value === null;
|
|
145
|
+
case "boolean":
|
|
146
|
+
return typeof value === "boolean";
|
|
147
|
+
case "object":
|
|
148
|
+
return isObject(value);
|
|
149
|
+
case "array":
|
|
150
|
+
return Array.isArray(value);
|
|
151
|
+
case "number":
|
|
152
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
153
|
+
case "integer":
|
|
154
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
155
|
+
case "string":
|
|
156
|
+
return typeof value === "string";
|
|
157
|
+
default:
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function unicodeLength(value) {
|
|
162
|
+
return [...value].length;
|
|
163
|
+
}
|
|
164
|
+
export function isMultipleOf(value, divisor) {
|
|
165
|
+
if (divisor === 0) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const quotient = value / divisor;
|
|
169
|
+
return (Math.abs(quotient - Math.round(quotient)) <=
|
|
170
|
+
Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4);
|
|
171
|
+
}
|
package/dist/validate.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-schema",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.117",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "rm -rf dist && tsc",
|
|
16
|
-
"test": "cd ../.. && vitest run packages/toolcraft-schema/src
|
|
17
|
-
"test:unit": "cd ../.. && vitest run packages/toolcraft-schema/src
|
|
16
|
+
"test": "cd ../.. && vitest run packages/toolcraft-schema/src",
|
|
17
|
+
"test:unit": "cd ../.. && vitest run packages/toolcraft-schema/src",
|
|
18
18
|
"lint": "cd ../.. && eslint packages/toolcraft-schema/src --ext ts && tsc -p packages/toolcraft-schema/tsconfig.json --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|