@jarenjs/validate 0.8.4 → 0.9.2
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/ARCHITECTURE.md +1067 -0
- package/LICENSE +21 -0
- package/README.md +339 -2
- package/dist/types/array.d.ts +2 -0
- package/dist/types/bigint.d.ts +1 -0
- package/dist/types/combine.d.ts +1 -0
- package/dist/types/condition.d.ts +1 -0
- package/dist/types/content.d.ts +3 -0
- package/dist/types/data.d.ts +7 -0
- package/dist/types/dollar-data.d.ts +20 -0
- package/dist/types/dynamic-ref.d.ts +44 -0
- package/dist/types/enum.d.ts +1 -0
- package/dist/types/format.d.ts +21 -0
- package/dist/types/index.d.ts +874 -0
- package/dist/types/number.d.ts +1 -0
- package/dist/types/object.d.ts +3 -0
- package/dist/types/query-keyword.d.ts +19 -0
- package/dist/types/query.d.ts +29 -0
- package/dist/types/schema.d.ts +1 -0
- package/dist/types/string.d.ts +1 -0
- package/dist/types/tools.d.ts +51 -0
- package/dist/types/traverse.d.ts +32 -0
- package/dist/types/unevaluated.d.ts +12 -0
- package/package.json +32 -7
- package/src/array.js +565 -0
- package/src/bigint.js +97 -0
- package/src/combine.js +226 -0
- package/src/condition.js +109 -0
- package/src/content.js +83 -0
- package/src/data.js +477 -0
- package/src/dollar-data.js +629 -0
- package/src/dynamic-ref.js +121 -0
- package/src/enum.js +148 -0
- package/src/format.js +66 -0
- package/src/index.js +1854 -0
- package/src/number.js +159 -0
- package/src/object.js +755 -0
- package/src/query-keyword.js +99 -0
- package/src/query.js +59 -0
- package/src/schema.js +645 -0
- package/src/string.js +152 -0
- package/src/tools.js +205 -0
- package/src/traverse.js +433 -0
- package/src/unevaluated.js +151 -0
- package/dist/index.js +0 -1998
- package/dist/index.js.map +0 -7
- package/dist/index.min.js +0 -2
- package/dist/index.min.js.map +0 -7
package/src/traverse.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isBoolOrObjectClass,
|
|
3
|
+
hasSchemaRef,
|
|
4
|
+
} from './tools.js';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
isBoolishType,
|
|
8
|
+
} from '@jarenjs/core/number';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
isObjectClass,
|
|
12
|
+
isStringType,
|
|
13
|
+
} from '@jarenjs/core';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
isStringWhiteSpace,
|
|
17
|
+
} from '@jarenjs/core/string';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
isValidHtmlIdentifier,
|
|
21
|
+
} from '@jarenjs/core/text';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function encodeJsonPointerKey(key) {
|
|
25
|
+
return encodeURIComponent(key.replace('~', '~0').replace('/', '~1'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function encodeJsonPointerPath(path, key, index) {
|
|
29
|
+
return index == null
|
|
30
|
+
? `${path}/${encodeJsonPointerKey(key)}`
|
|
31
|
+
: `${path}/${encodeJsonPointerKey(key)}/${encodeJsonPointerKey(String(index))}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeJsonPointerKey(key) {
|
|
35
|
+
return decodeURIComponent(key.replace('~0', '~').replace('~1', '/'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function decodeJsonPointerPath(path) {
|
|
39
|
+
// split and remove the leading empty string
|
|
40
|
+
return path.split('/').map(decodeJsonPointerKey).splice(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class JsonPointerOptions {
|
|
44
|
+
constructor(anchorsGlobal = false, anchorsAllowed = true, skipErrors = true) {
|
|
45
|
+
this.anchorsGlobal = anchorsGlobal;
|
|
46
|
+
this.anchorsAllowed = anchorsAllowed;
|
|
47
|
+
this.skipErrors = skipErrors;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class JsonPointer {
|
|
52
|
+
constructor(id, search, leftUri, fragment) {
|
|
53
|
+
this.id = id;
|
|
54
|
+
this.search = search;
|
|
55
|
+
this.leftUri = leftUri;
|
|
56
|
+
this.fragment = fragment;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createJsonPointer(refUri, baseUri, opts = new JsonPointerOptions()) {
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = !isStringType(refUri) || isStringWhiteSpace(refUri)
|
|
64
|
+
? new URL(baseUri)
|
|
65
|
+
: !isStringType(baseUri) || isStringWhiteSpace(baseUri)
|
|
66
|
+
? new URL(refUri)
|
|
67
|
+
: new URL(refUri, baseUri);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Handle case where baseUri is a relative reference (not a valid URL)
|
|
70
|
+
// Only apply manual resolution when baseUri is a plain identifier (no scheme, no /)
|
|
71
|
+
const isRelativeBase = isStringType(baseUri) && !/^[a-z][a-z0-9+.-]*:/i.test(baseUri);
|
|
72
|
+
|
|
73
|
+
if (isStringType(refUri) && isRelativeBase) {
|
|
74
|
+
// If refUri is absolute (has a scheme), use it as-is
|
|
75
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(refUri)) {
|
|
76
|
+
url = new URL(refUri);
|
|
77
|
+
} else if (refUri.startsWith('#')) {
|
|
78
|
+
// Fragment-only reference: combine with baseUri
|
|
79
|
+
const effectiveBase = baseUri;
|
|
80
|
+
url = new URL(refUri, 'http://example.com/' + effectiveBase);
|
|
81
|
+
// Restore the original baseUri in the result
|
|
82
|
+
const href = url.href.replace('http://example.com/', '');
|
|
83
|
+
const [uri, fragment] = href.split('#');
|
|
84
|
+
return new JsonPointer(
|
|
85
|
+
effectiveBase + refUri,
|
|
86
|
+
undefined,
|
|
87
|
+
effectiveBase + '#',
|
|
88
|
+
fragment || null
|
|
89
|
+
);
|
|
90
|
+
} else if (refUri.includes('#')) {
|
|
91
|
+
// refUri has a fragment: manual resolution
|
|
92
|
+
const [refBase, refFragment] = refUri.split('#');
|
|
93
|
+
const resolvedId = baseUri.endsWith('/')
|
|
94
|
+
? baseUri + refBase + '#' + refFragment
|
|
95
|
+
: baseUri + '/' + refBase + '#' + refFragment;
|
|
96
|
+
return new JsonPointer(
|
|
97
|
+
resolvedId,
|
|
98
|
+
undefined,
|
|
99
|
+
baseUri + '#',
|
|
100
|
+
refFragment
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
// No fragment: simple concatenation
|
|
104
|
+
const resolvedId = baseUri.endsWith('/')
|
|
105
|
+
? baseUri + refUri
|
|
106
|
+
: baseUri + '/' + refUri;
|
|
107
|
+
return new JsonPointer(resolvedId + '#', undefined, resolvedId + '#', null);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Re-throw the original error if we can't handle it
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const [uri, fragment] = url.href.split('#');
|
|
116
|
+
const [leftUri, search] = uri.split('?');
|
|
117
|
+
|
|
118
|
+
if (!isStringWhiteSpace(fragment)) {
|
|
119
|
+
// this is a sort of $anchor and we are karen about it and thus not use isStringAnchor().
|
|
120
|
+
if (opts.anchorsAllowed == true && isValidHtmlIdentifier(fragment)) {
|
|
121
|
+
return opts.anchorsGlobal == true
|
|
122
|
+
? new JsonPointer(`#${fragment}`, search, `${leftUri}#`, fragment)
|
|
123
|
+
: new JsonPointer(`${leftUri}#${fragment}`, search, `${leftUri}#`, fragment);
|
|
124
|
+
}
|
|
125
|
+
// or a json pointer
|
|
126
|
+
else if (fragment.startsWith('/')) {
|
|
127
|
+
return new JsonPointer(`${leftUri}#${fragment}`, search, `${leftUri}#`, fragment);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return new JsonPointer(`${leftUri}#`, search, `${leftUri}#`, null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const TRAVERSE_SCHEMA_OBJECTS = [
|
|
134
|
+
'items', 'prefixItems', 'additionalItems', 'contains', 'unevaluatedItems',
|
|
135
|
+
'additionalProperties', 'propertyNames', 'unevaluatedProperties',
|
|
136
|
+
'not', 'oneOf', 'anyOf', 'allOf', 'if', 'then', 'else',
|
|
137
|
+
];
|
|
138
|
+
const TRAVERSE_SCHEMA_MAPS = [
|
|
139
|
+
'properties', 'patternProperties',
|
|
140
|
+
'dependencies', 'dependentSchemas', 'dependentRequired',
|
|
141
|
+
'definitions', '$defs', 'components',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export function storeSchemaIdsInMap(schemas, baseUri, schema, opts = new JsonPointerOptions()) {
|
|
145
|
+
if (!isObjectClass(schema)) {
|
|
146
|
+
const { id } = createJsonPointer(baseUri, undefined, opts);
|
|
147
|
+
schemas.set(id, schema);
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const { id: rootUri } = createJsonPointer(schema.$id, baseUri, opts);
|
|
152
|
+
if (!isStringType(schema.$id) || isStringWhiteSpace(schema.$id)) {
|
|
153
|
+
if (schemas.has(rootUri))
|
|
154
|
+
throw new Error(`Schema '${rootUri}' already exists`);
|
|
155
|
+
|
|
156
|
+
schemas.set(rootUri, schema);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
baseUri = rootUri;
|
|
160
|
+
|
|
161
|
+
const queue = [{ obj: schema, base: rootUri, path: '#' }];
|
|
162
|
+
while (queue.length > 0) {
|
|
163
|
+
// @ts-ignore
|
|
164
|
+
const { obj, base, path } = queue.shift();
|
|
165
|
+
|
|
166
|
+
// Handle $id (draft 6+) or id (draft 4) for identifying subschemas
|
|
167
|
+
const idKeyword = isStringType(obj.$id) ? obj.$id : isStringType(obj.id) ? obj.id : undefined;
|
|
168
|
+
if (isStringType(idKeyword) && !isStringWhiteSpace(idKeyword)) {
|
|
169
|
+
const { id } = createJsonPointer(idKeyword, base, opts);
|
|
170
|
+
if (!schemas.has(id))
|
|
171
|
+
schemas.set(id, obj);
|
|
172
|
+
else if (schemas.get(id) == null)
|
|
173
|
+
schemas.set(id, obj);
|
|
174
|
+
else
|
|
175
|
+
throw new Error(`Schema '${id}' for path '${path}' in '${base}' already exists`);
|
|
176
|
+
|
|
177
|
+
// CHECK: Also store under alternate ID (with/without # suffix) for absolute URIs only
|
|
178
|
+
if (!id.startsWith('#')) {
|
|
179
|
+
const altId = id.endsWith('#') ? id.slice(0, -1) : id + '#';
|
|
180
|
+
if (!schemas.has(altId)) {
|
|
181
|
+
schemas.set(altId, obj);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// we reset the baseUri when the $id property is set.
|
|
186
|
+
baseUri = id;
|
|
187
|
+
}
|
|
188
|
+
else
|
|
189
|
+
baseUri = base;
|
|
190
|
+
|
|
191
|
+
if (isStringType(obj.$anchor) && !isStringWhiteSpace(obj.$anchor)) {
|
|
192
|
+
const { id } = createJsonPointer(`#${obj.$anchor}`, baseUri, opts);
|
|
193
|
+
if (!schemas.has(id))
|
|
194
|
+
schemas.set(id, obj);
|
|
195
|
+
else if (schemas.get(id) == null)
|
|
196
|
+
schemas.set(id, obj);
|
|
197
|
+
else
|
|
198
|
+
throw new Error(`Schema '${id}' for path '${path}' in '${base}' already exists`);
|
|
199
|
+
|
|
200
|
+
// we reset the baseUri when the $anchor property is set.
|
|
201
|
+
if (!id.startsWith('#')) // except when anchors are global
|
|
202
|
+
baseUri = id;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle $dynamicAnchor (draft 2020-12) - similar to $anchor but for $dynamicRef
|
|
206
|
+
if (isStringType(obj.$dynamicAnchor) && !isStringWhiteSpace(obj.$dynamicAnchor)) {
|
|
207
|
+
const { id } = createJsonPointer(`#${obj.$dynamicAnchor}`, baseUri, opts);
|
|
208
|
+
if (!schemas.has(id))
|
|
209
|
+
schemas.set(id, obj);
|
|
210
|
+
else if (schemas.get(id) == null)
|
|
211
|
+
schemas.set(id, obj);
|
|
212
|
+
else
|
|
213
|
+
throw new Error(`Schema '${id}' for path '${path}' in '${base}' already exists`);
|
|
214
|
+
|
|
215
|
+
// Note: $dynamicAnchor does NOT change the baseUri like $anchor does
|
|
216
|
+
// It's only used for $dynamicRef resolution
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (isStringType(obj.$ref) && !isStringWhiteSpace(obj.$ref)) {
|
|
220
|
+
const { id: ref } = createJsonPointer(obj.$ref, baseUri, opts);
|
|
221
|
+
if (!schemas.has(ref))
|
|
222
|
+
schemas.set(ref, null);
|
|
223
|
+
|
|
224
|
+
// Don't continue here - we need to process other schemas in the same
|
|
225
|
+
// parent object (like definitions) even if this one has a $ref.
|
|
226
|
+
// The $ref just means we don't traverse INTO this object's properties,
|
|
227
|
+
// but siblings should still be processed.
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// iterate through all properties
|
|
231
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
232
|
+
if (!isObjectClass(value) && !Array.isArray(value))
|
|
233
|
+
continue;
|
|
234
|
+
|
|
235
|
+
// is the property a schema object to traverse in?
|
|
236
|
+
if (TRAVERSE_SCHEMA_OBJECTS.includes(key)) {
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
const len = value.length;
|
|
239
|
+
for (let index = 0; index < len; index++) {
|
|
240
|
+
const item = value[index];
|
|
241
|
+
const nextpath = encodeJsonPointerPath(path, key, index);
|
|
242
|
+
|
|
243
|
+
if (isBoolishType(item))
|
|
244
|
+
continue;
|
|
245
|
+
if (!isObjectClass(item)) {
|
|
246
|
+
if (opts.skipErrors === true)
|
|
247
|
+
continue;
|
|
248
|
+
else
|
|
249
|
+
throw new Error(`${nextpath} is not a schema`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
queue.push({ obj: item, base: baseUri, path: nextpath });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const nextpath = encodeJsonPointerPath(path, key);
|
|
257
|
+
|
|
258
|
+
queue.push({ obj: value, base: baseUri, path: nextpath });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// or is the property a map of key and schema objects?
|
|
262
|
+
else if (TRAVERSE_SCHEMA_MAPS.includes(key)) {
|
|
263
|
+
if (Array.isArray(value))
|
|
264
|
+
continue;
|
|
265
|
+
|
|
266
|
+
for (const [index, item] of Object.entries(value)) {
|
|
267
|
+
const nextpath = encodeJsonPointerPath(path, key, index);
|
|
268
|
+
if (isBoolishType(item))
|
|
269
|
+
continue;
|
|
270
|
+
if (!isObjectClass(item)) {
|
|
271
|
+
if (opts.skipErrors === true)
|
|
272
|
+
continue;
|
|
273
|
+
else
|
|
274
|
+
throw new Error(`${nextpath} is not a schema`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
queue.push({ obj: item, base: baseUri, path: nextpath });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return rootUri;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function resolveRefSchemaShallow(schemas, refUri, baseUri, opts = new JsonPointerOptions()) {
|
|
287
|
+
let { id: base, leftUri, fragment } = createJsonPointer(refUri, baseUri, opts);
|
|
288
|
+
if (!schemas.has(leftUri))
|
|
289
|
+
throw new Error(`The root of reference: '$ref': '${base}', is not found in init-cache`);
|
|
290
|
+
|
|
291
|
+
// get the document from cache
|
|
292
|
+
let schema = schemas.get(leftUri);
|
|
293
|
+
|
|
294
|
+
// If there is no fragment, return the whole schema
|
|
295
|
+
if (isStringWhiteSpace(fragment)) {
|
|
296
|
+
return { id: base, schema };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If the fragment is a plain name anchor (not a JSON pointer starting with /),
|
|
300
|
+
// look it up directly in the schemas map as a location-independent identifier
|
|
301
|
+
if (!fragment.startsWith('/')) {
|
|
302
|
+
// The anchor could be stored as just the fragment (e.g., "#foo") or as a full URI
|
|
303
|
+
// Try the full id first (which includes the base URI)
|
|
304
|
+
if (schemas.has(base)) {
|
|
305
|
+
return { id: base, schema: schemas.get(base) };
|
|
306
|
+
}
|
|
307
|
+
// Try the scoped anchor format: baseUri + fragment (e.g., "https://example.com/schema#foo")
|
|
308
|
+
// This handles anchors stored with anchorsGlobal: false
|
|
309
|
+
const scopedAnchorId = `${leftUri}${fragment}`;
|
|
310
|
+
if (schemas.has(scopedAnchorId)) {
|
|
311
|
+
return { id: scopedAnchorId, schema: schemas.get(scopedAnchorId) };
|
|
312
|
+
}
|
|
313
|
+
// Try just the fragment with hash (global anchor format)
|
|
314
|
+
const fragmentWithHash = `#${fragment}`;
|
|
315
|
+
if (schemas.has(fragmentWithHash)) {
|
|
316
|
+
return { id: fragmentWithHash, schema: schemas.get(fragmentWithHash) };
|
|
317
|
+
}
|
|
318
|
+
// Fall through to JSON pointer traversal for backward compatibility
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Decode and resolve the JSON pointer
|
|
322
|
+
const fragments = decodeJsonPointerPath(fragment);
|
|
323
|
+
|
|
324
|
+
// Traverse the schema based on the JSON pointer
|
|
325
|
+
let current = '';
|
|
326
|
+
for (const part of fragments) {
|
|
327
|
+
current = current + '/' + part;
|
|
328
|
+
if (!isBoolOrObjectClass(schema[part]) && !Array.isArray(schema[part]))
|
|
329
|
+
throw new Error(`The '${current}' is not is not a valid schema in '${leftUri}'`);
|
|
330
|
+
|
|
331
|
+
schema = schema[part];
|
|
332
|
+
if (isObjectClass(schema) && isStringType(schema.$id) && !isStringWhiteSpace(schema.$id)) {
|
|
333
|
+
const { id } = createJsonPointer(schema.$id, base, opts);
|
|
334
|
+
base = id;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { id: base, schema };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function restoreSchemaRefsInMap(schemas, opts = new JsonPointerOptions()) {
|
|
342
|
+
for (const [id, item] of schemas.entries()) {
|
|
343
|
+
if (item != null)
|
|
344
|
+
continue;
|
|
345
|
+
|
|
346
|
+
// Try to use resolveRefSchemaDeep to flatten ref chains
|
|
347
|
+
// This resolves a→b→c into a→c, eliminating chain traversal at validation time
|
|
348
|
+
// If deep resolution fails (e.g., remote ref not loaded yet), fall back to shallow
|
|
349
|
+
try {
|
|
350
|
+
const { id: finalId, schema: finalSchema } = resolveRefSchemaDeep(
|
|
351
|
+
schemas,
|
|
352
|
+
id,
|
|
353
|
+
{ $ref: id },
|
|
354
|
+
opts
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (finalSchema == null)
|
|
358
|
+
throw new Error(`Can not resolve schema for '${id}'`);
|
|
359
|
+
|
|
360
|
+
// Store the final resolved schema (flattened ref chain)
|
|
361
|
+
schemas.set(id, finalSchema);
|
|
362
|
+
|
|
363
|
+
// Also store under the final ID for direct access if not already present
|
|
364
|
+
if (finalId !== id && !schemas.has(finalId)) {
|
|
365
|
+
schemas.set(finalId, finalSchema);
|
|
366
|
+
}
|
|
367
|
+
} catch (e) {
|
|
368
|
+
// If deep resolution fails (remote ref not loaded), fall back to shallow resolution
|
|
369
|
+
// This preserves the original behavior for unresolved refs
|
|
370
|
+
const { schema } = resolveRefSchemaShallow(schemas, id, null, opts);
|
|
371
|
+
if (schema == null)
|
|
372
|
+
throw new Error(`Can not resolve schema for '${id}'`);
|
|
373
|
+
|
|
374
|
+
schemas.set(id, schema);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export class TraverseOptions extends JsonPointerOptions {
|
|
380
|
+
constructor(
|
|
381
|
+
origin = 'https://github.com/jklarenbeek/jarenjs',
|
|
382
|
+
mergeSchemas = true,
|
|
383
|
+
anchorsGlobal = false,
|
|
384
|
+
anchorsAllowed = true,
|
|
385
|
+
skipErrors = true
|
|
386
|
+
) {
|
|
387
|
+
super(anchorsGlobal, anchorsAllowed, skipErrors);
|
|
388
|
+
this.origin = origin,
|
|
389
|
+
this.mergeSchemas = mergeSchemas;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function resolveRefSchemaDeep(schemas, baseUri, refschema, opts = new TraverseOptions()) {
|
|
394
|
+
if (!isObjectClass(refschema))
|
|
395
|
+
return { id: baseUri, schema: refschema };
|
|
396
|
+
|
|
397
|
+
if (!hasSchemaRef(refschema))
|
|
398
|
+
return { id: baseUri, schema: refschema };
|
|
399
|
+
|
|
400
|
+
const queue = [{ item: refschema, base: baseUri }];
|
|
401
|
+
const seen = new Set();
|
|
402
|
+
let result = {};
|
|
403
|
+
|
|
404
|
+
while (queue.length > 0) {
|
|
405
|
+
// @ts-ignore
|
|
406
|
+
const { item, base } = queue.shift();
|
|
407
|
+
if (!isObjectClass(item))
|
|
408
|
+
return { id: base, schema: item };
|
|
409
|
+
|
|
410
|
+
// In draft 7 and earlier, $ref completely replaces the schema
|
|
411
|
+
// and all sibling keywords must be ignored. We only keep the $ref
|
|
412
|
+
// to resolve it, discarding all other keywords from this item.
|
|
413
|
+
if (hasSchemaRef(item)) {
|
|
414
|
+
const ref = item.$ref;
|
|
415
|
+
const { id, schema } = resolveRefSchemaShallow(schemas, ref, base, opts);
|
|
416
|
+
|
|
417
|
+
if (seen.has(id))
|
|
418
|
+
return { id: base, schema: result };
|
|
419
|
+
|
|
420
|
+
seen.add(id);
|
|
421
|
+
|
|
422
|
+
queue.push({ item: schema, base: id });
|
|
423
|
+
} else {
|
|
424
|
+
// No $ref in this item, merge as normal
|
|
425
|
+
result = opts.mergeSchemas == true
|
|
426
|
+
? { ...item, ...result }
|
|
427
|
+
: { ...item };
|
|
428
|
+
return { id: base, schema: result };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
throw new Error(`The json schema '${baseUri}' can not be resolved!`);
|
|
433
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isObjectType,
|
|
5
|
+
isArrayClass,
|
|
6
|
+
} from '@jarenjs/core';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getBoolOrObjectClass,
|
|
10
|
+
} from './tools.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Compile the unevaluatedProperties keyword as a final-stage validator.
|
|
14
|
+
* Receives the evaluation-log mark taken before the containing schema
|
|
15
|
+
* (including a sibling $ref) started, and checks every property of the
|
|
16
|
+
* instance that was not recorded as evaluated since that mark.
|
|
17
|
+
* @param {import('./index.js').ValidationObject} schemaObj
|
|
18
|
+
* @param {object} jsonSchema
|
|
19
|
+
* @returns {function|undefined} validator(data, dataPath, dataRoot, mark)
|
|
20
|
+
*/
|
|
21
|
+
function compileUnevaluatedProperties(schemaObj, jsonSchema) {
|
|
22
|
+
const uneval = getBoolOrObjectClass(jsonSchema.unevaluatedProperties);
|
|
23
|
+
if (uneval == null) return undefined;
|
|
24
|
+
|
|
25
|
+
const root = schemaObj.root;
|
|
26
|
+
const addError = schemaObj.createErrorHandler(uneval, 'unevaluatedProperties');
|
|
27
|
+
|
|
28
|
+
// true: everything left over is valid, but counts as evaluated for
|
|
29
|
+
// any unevaluatedProperties in an outer schema.
|
|
30
|
+
if (uneval === true) {
|
|
31
|
+
return function validateUnevaluatedPropertiesTrue(data, dataPath, dataRoot, mark) {
|
|
32
|
+
if (!isObjectType(data)) return true;
|
|
33
|
+
const log = root.evalLog;
|
|
34
|
+
const keys = Object.keys(data);
|
|
35
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
36
|
+
log.add(data, keys[i]);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (uneval === false) {
|
|
43
|
+
return function validateUnevaluatedPropertiesFalse(data, dataPath, dataRoot, mark) {
|
|
44
|
+
if (!isObjectType(data)) return true;
|
|
45
|
+
const log = root.evalLog;
|
|
46
|
+
const keys = Object.keys(data);
|
|
47
|
+
let valid = true;
|
|
48
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
49
|
+
if (!log.hasKey(data, keys[i], mark))
|
|
50
|
+
valid = addError(keys[i], dataPath) && valid;
|
|
51
|
+
}
|
|
52
|
+
return valid;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const validator = schemaObj.createValidator(uneval, 'unevaluatedProperties');
|
|
57
|
+
return function validateUnevaluatedProperties(data, dataPath, dataRoot, mark) {
|
|
58
|
+
if (!isObjectType(data)) return true;
|
|
59
|
+
const log = root.evalLog;
|
|
60
|
+
const keys = Object.keys(data);
|
|
61
|
+
let valid = true;
|
|
62
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
63
|
+
const key = keys[i];
|
|
64
|
+
if (log.hasKey(data, key, mark)) continue;
|
|
65
|
+
if (validator(data[key], dataPath, dataRoot, key) === true)
|
|
66
|
+
log.add(data, key);
|
|
67
|
+
else
|
|
68
|
+
valid = addError(key, dataPath) && valid;
|
|
69
|
+
}
|
|
70
|
+
return valid;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compile the unevaluatedItems keyword as a final-stage validator.
|
|
76
|
+
* @param {import('./index.js').ValidationObject} schemaObj
|
|
77
|
+
* @param {object} jsonSchema
|
|
78
|
+
* @returns {function|undefined} validator(data, dataPath, dataRoot, mark)
|
|
79
|
+
*/
|
|
80
|
+
function compileUnevaluatedItems(schemaObj, jsonSchema) {
|
|
81
|
+
const uneval = getBoolOrObjectClass(jsonSchema.unevaluatedItems);
|
|
82
|
+
if (uneval == null) return undefined;
|
|
83
|
+
|
|
84
|
+
const root = schemaObj.root;
|
|
85
|
+
const addError = schemaObj.createErrorHandler(uneval, 'unevaluatedItems');
|
|
86
|
+
|
|
87
|
+
if (uneval === true) {
|
|
88
|
+
return function validateUnevaluatedItemsTrue(data, dataPath, dataRoot, mark) {
|
|
89
|
+
if (!isArrayClass(data)) return true;
|
|
90
|
+
root.evalLog.add(data, -1);
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (uneval === false) {
|
|
96
|
+
return function validateUnevaluatedItemsFalse(data, dataPath, dataRoot, mark) {
|
|
97
|
+
if (!isArrayClass(data)) return true;
|
|
98
|
+
const log = root.evalLog;
|
|
99
|
+
for (let i = 0; i < data.length; ++i) {
|
|
100
|
+
if (!log.hasItem(data, i, mark))
|
|
101
|
+
return addError(i, dataPath);
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const validator = schemaObj.createValidator(uneval, 'unevaluatedItems');
|
|
108
|
+
return function validateUnevaluatedItems(data, dataPath, dataRoot, mark) {
|
|
109
|
+
if (!isArrayClass(data)) return true;
|
|
110
|
+
const log = root.evalLog;
|
|
111
|
+
let valid = true;
|
|
112
|
+
for (let i = 0; i < data.length; ++i) {
|
|
113
|
+
if (log.hasItem(data, i, mark)) continue;
|
|
114
|
+
if (validator(data[i], dataPath, dataRoot, i) === true)
|
|
115
|
+
log.add(data, i);
|
|
116
|
+
else
|
|
117
|
+
valid = addError(i, dataPath) && valid;
|
|
118
|
+
}
|
|
119
|
+
return valid;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Wraps a compiled schema validator so unevaluatedProperties/unevaluatedItems
|
|
125
|
+
* run last, seeing every annotation produced on this instance by the schema's
|
|
126
|
+
* own keywords and its in-place applicators (allOf/anyOf/oneOf/if/$ref/...).
|
|
127
|
+
* Returns the validator unchanged when evaluation tracking is off or the
|
|
128
|
+
* schema has no unevaluated* keywords.
|
|
129
|
+
* @param {import('./index.js').ValidationObject} schemaObj
|
|
130
|
+
* @param {object} jsonSchema
|
|
131
|
+
* @param {function} validator - The compiled validator for all other keywords
|
|
132
|
+
* @returns {function} The wrapped (or original) validator
|
|
133
|
+
*/
|
|
134
|
+
export function wrapUnevaluated(schemaObj, jsonSchema, validator) {
|
|
135
|
+
const root = schemaObj.root;
|
|
136
|
+
if (!root.usesUnevaluated) return validator;
|
|
137
|
+
if (jsonSchema == null || typeof jsonSchema !== 'object') return validator;
|
|
138
|
+
|
|
139
|
+
const unevalProps = compileUnevaluatedProperties(schemaObj, jsonSchema);
|
|
140
|
+
const unevalItems = compileUnevaluatedItems(schemaObj, jsonSchema);
|
|
141
|
+
if (unevalProps == null && unevalItems == null) return validator;
|
|
142
|
+
|
|
143
|
+
return function validateUnevaluatedSchema(data, dataPath, dataRoot, dataKey) {
|
|
144
|
+
const log = root.evalLog;
|
|
145
|
+
const mark = log.mark();
|
|
146
|
+
if (validator(data, dataPath, dataRoot, dataKey) === false) return false;
|
|
147
|
+
if (unevalProps != null && unevalProps(data, dataPath, dataRoot, mark) === false) return false;
|
|
148
|
+
if (unevalItems != null && unevalItems(data, dataPath, dataRoot, mark) === false) return false;
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
}
|