z-schema 10.0.0 → 12.0.0
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/README.md +35 -17
- package/cjs/index.d.ts +345 -34
- package/cjs/index.js +4446 -1685
- package/dist/errors.js +5 -0
- package/dist/format-validators.js +131 -107
- package/dist/json-schema-versions.js +4 -1
- package/dist/json-schema.js +50 -16
- package/dist/json-validation.js +524 -669
- package/dist/report.js +37 -16
- package/dist/schema-cache.js +76 -18
- package/dist/schema-compiler.js +72 -47
- package/dist/schema-validator.js +117 -52
- package/dist/schemas/draft-07-schema.json +172 -0
- package/dist/schemas/draft-2019-09-meta-applicator.json +52 -0
- package/dist/schemas/draft-2019-09-meta-content.json +12 -0
- package/dist/schemas/draft-2019-09-meta-core.json +53 -0
- package/dist/schemas/draft-2019-09-meta-format.json +10 -0
- package/dist/schemas/draft-2019-09-meta-meta-data.json +32 -0
- package/dist/schemas/draft-2019-09-meta-validation.json +94 -0
- package/dist/schemas/draft-2019-09-schema.json +41 -0
- package/dist/schemas/draft-2020-12-meta-applicator.json +44 -0
- package/dist/schemas/draft-2020-12-meta-content.json +12 -0
- package/dist/schemas/draft-2020-12-meta-core.json +47 -0
- package/dist/schemas/draft-2020-12-meta-format-annotation.json +10 -0
- package/dist/schemas/draft-2020-12-meta-format-assertion.json +10 -0
- package/dist/schemas/draft-2020-12-meta-meta-data.json +32 -0
- package/dist/schemas/draft-2020-12-meta-unevaluated.json +11 -0
- package/dist/schemas/draft-2020-12-meta-validation.json +94 -0
- package/dist/schemas/draft-2020-12-schema.json +57 -0
- package/dist/types/errors.d.ts +4 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/json-schema-versions.d.ts +128 -9
- package/dist/types/json-schema.d.ts +28 -11
- package/dist/types/json-validation.d.ts +2 -3
- package/dist/types/report.d.ts +14 -4
- package/dist/types/schema-cache.d.ts +7 -0
- package/dist/types/schema-compiler.d.ts +5 -3
- package/dist/types/schema-validator.d.ts +2 -2
- package/dist/types/utils/array.d.ts +8 -1
- package/dist/types/utils/base64.d.ts +2 -0
- package/dist/types/utils/clone.d.ts +1 -1
- package/dist/types/utils/date.d.ts +1 -0
- package/dist/types/utils/hostname.d.ts +2 -0
- package/dist/types/utils/json.d.ts +2 -1
- package/dist/types/utils/properties.d.ts +0 -1
- package/dist/types/utils/time.d.ts +12 -0
- package/dist/types/utils/unicode.d.ts +3 -12
- package/dist/types/validation/array.d.ts +12 -0
- package/dist/types/validation/combinators.d.ts +10 -0
- package/dist/types/validation/numeric.d.ts +8 -0
- package/dist/types/validation/object.d.ts +13 -0
- package/dist/types/validation/ref.d.ts +11 -0
- package/dist/types/validation/shared.d.ts +26 -0
- package/dist/types/validation/string.d.ts +9 -0
- package/dist/types/validation/type.d.ts +6 -0
- package/dist/types/z-schema-base.d.ts +39 -1
- package/dist/types/z-schema-options.d.ts +3 -0
- package/dist/types/z-schema.d.ts +144 -8
- package/dist/utils/array.js +49 -7
- package/dist/utils/base64.js +29 -0
- package/dist/utils/clone.js +13 -12
- package/dist/utils/date.js +21 -0
- package/dist/utils/hostname.js +146 -0
- package/dist/utils/json.js +11 -6
- package/dist/utils/properties.js +1 -6
- package/dist/utils/time.js +50 -0
- package/dist/utils/unicode.js +8 -41
- package/dist/utils/uri.js +1 -1
- package/dist/validation/array.js +128 -0
- package/dist/validation/combinators.js +107 -0
- package/dist/validation/numeric.js +97 -0
- package/dist/validation/object.js +238 -0
- package/dist/validation/ref.js +70 -0
- package/dist/validation/shared.js +136 -0
- package/dist/validation/string.js +178 -0
- package/dist/validation/type.js +55 -0
- package/dist/z-schema-base.js +52 -32
- package/dist/z-schema-options.js +12 -8
- package/dist/z-schema-versions.js +92 -9
- package/dist/z-schema.js +135 -38
- package/package.json +22 -8
- package/src/errors.ts +8 -0
- package/src/format-validators.ts +146 -105
- package/src/index.ts +10 -1
- package/src/json-schema-versions.ts +181 -11
- package/src/json-schema.ts +102 -35
- package/src/json-validation.ts +653 -724
- package/src/report.ts +42 -20
- package/src/schema-cache.ts +94 -18
- package/src/schema-compiler.ts +94 -51
- package/src/schema-validator.ts +132 -56
- package/src/schemas/draft-07-schema.json +172 -0
- package/src/schemas/draft-2019-09-meta-applicator.json +53 -0
- package/src/schemas/draft-2019-09-meta-content.json +14 -0
- package/src/schemas/draft-2019-09-meta-core.json +54 -0
- package/src/schemas/draft-2019-09-meta-format.json +11 -0
- package/src/schemas/draft-2019-09-meta-meta-data.json +34 -0
- package/src/schemas/draft-2019-09-meta-validation.json +95 -0
- package/src/schemas/draft-2019-09-schema.json +42 -0
- package/src/schemas/draft-2020-12-meta-applicator.json +45 -0
- package/src/schemas/draft-2020-12-meta-content.json +14 -0
- package/src/schemas/draft-2020-12-meta-core.json +48 -0
- package/src/schemas/draft-2020-12-meta-format-annotation.json +11 -0
- package/src/schemas/draft-2020-12-meta-format-assertion.json +11 -0
- package/src/schemas/draft-2020-12-meta-meta-data.json +34 -0
- package/src/schemas/draft-2020-12-meta-unevaluated.json +12 -0
- package/src/schemas/draft-2020-12-meta-validation.json +95 -0
- package/src/schemas/draft-2020-12-schema.json +58 -0
- package/src/utils/array.ts +51 -7
- package/src/utils/base64.ts +32 -0
- package/src/utils/clone.ts +16 -12
- package/src/utils/date.ts +23 -0
- package/src/utils/hostname.ts +174 -0
- package/src/utils/json.ts +15 -6
- package/src/utils/properties.ts +1 -7
- package/src/utils/time.ts +73 -0
- package/src/utils/unicode.ts +8 -39
- package/src/utils/uri.ts +1 -1
- package/src/validation/array.ts +158 -0
- package/src/validation/combinators.ts +132 -0
- package/src/validation/numeric.ts +120 -0
- package/src/validation/object.ts +318 -0
- package/src/validation/ref.ts +85 -0
- package/src/validation/shared.ts +191 -0
- package/src/validation/string.ts +224 -0
- package/src/validation/type.ts +66 -0
- package/src/z-schema-base.ts +54 -36
- package/src/z-schema-options.ts +15 -8
- package/src/z-schema-versions.ts +107 -12
- package/src/z-schema.ts +158 -42
- package/umd/ZSchema.js +4446 -1685
- package/umd/ZSchema.min.js +1 -1
- package/dist/schemas/draft-04-hyper-schema.json +0 -135
- package/dist/schemas/draft-06-hyper-schema.json +0 -132
- package/dist/schemas/draft-06-links.json +0 -43
- package/src/schemas/draft-04-hyper-schema.json +0 -136
- package/src/schemas/draft-06-hyper-schema.json +0 -133
- package/src/schemas/draft-06-links.json +0 -43
package/dist/report.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Errors, getValidateError } from './errors.js';
|
|
2
|
+
import { shallowClone } from './utils/clone.js';
|
|
2
3
|
import { get } from './utils/json.js';
|
|
3
4
|
import { jsonSymbol, schemaSymbol } from './utils/symbols.js';
|
|
4
5
|
import { isAbsoluteUri } from './utils/uri.js';
|
|
@@ -6,6 +7,9 @@ import { isObject } from './utils/what-is.js';
|
|
|
6
7
|
export class Report {
|
|
7
8
|
asyncTasks = [];
|
|
8
9
|
commonErrorMessage;
|
|
10
|
+
__$recursiveAnchorStack = [];
|
|
11
|
+
__$dynamicScopeStack = [];
|
|
12
|
+
__validationResultCache = new Map();
|
|
9
13
|
errors = [];
|
|
10
14
|
json;
|
|
11
15
|
path = [];
|
|
@@ -22,11 +26,17 @@ export class Report {
|
|
|
22
26
|
// subreport
|
|
23
27
|
this.reportOptions = reportOptionsOrValidate || {};
|
|
24
28
|
this.validateOptions = validateOptions || parentOrOptions.validateOptions;
|
|
29
|
+
this.__$recursiveAnchorStack = [...parentOrOptions.__$recursiveAnchorStack];
|
|
30
|
+
this.__$dynamicScopeStack = [...parentOrOptions.__$dynamicScopeStack];
|
|
31
|
+
this.__validationResultCache = parentOrOptions.__validationResultCache;
|
|
25
32
|
}
|
|
26
33
|
else {
|
|
27
34
|
// primary
|
|
28
35
|
this.reportOptions = {};
|
|
29
36
|
this.validateOptions = reportOptionsOrValidate || {};
|
|
37
|
+
this.__$recursiveAnchorStack = [];
|
|
38
|
+
this.__$dynamicScopeStack = [];
|
|
39
|
+
this.__validationResultCache = new Map();
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
isValid() {
|
|
@@ -38,6 +48,25 @@ export class Report {
|
|
|
38
48
|
addAsyncTask(fn, args, asyncTaskResultProcessFn) {
|
|
39
49
|
this.asyncTasks.push([fn, args, asyncTaskResultProcessFn]);
|
|
40
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Like {@link addAsyncTask}, but automatically saves the current `path` and
|
|
53
|
+
* restores it around `processFn`. This eliminates the manual
|
|
54
|
+
* path-save/restore boilerplate that every async-aware validator would
|
|
55
|
+
* otherwise need.
|
|
56
|
+
*/
|
|
57
|
+
addAsyncTaskWithPath(fn, args, processFn) {
|
|
58
|
+
const pathBefore = shallowClone(this.path);
|
|
59
|
+
this.asyncTasks.push([
|
|
60
|
+
fn,
|
|
61
|
+
args,
|
|
62
|
+
(result) => {
|
|
63
|
+
const backup = this.path;
|
|
64
|
+
this.path = pathBefore;
|
|
65
|
+
processFn(result);
|
|
66
|
+
this.path = backup;
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
41
70
|
getAncestor(id) {
|
|
42
71
|
if (!this.parentReport) {
|
|
43
72
|
return undefined;
|
|
@@ -50,7 +79,6 @@ export class Report {
|
|
|
50
79
|
processAsyncTasks(timeout, callback) {
|
|
51
80
|
const validationTimeout = timeout || 2000;
|
|
52
81
|
let tasksCount = this.asyncTasks.length;
|
|
53
|
-
let idx = tasksCount;
|
|
54
82
|
let timedOut = false;
|
|
55
83
|
const finish = () => {
|
|
56
84
|
setTimeout(() => {
|
|
@@ -73,8 +101,8 @@ export class Report {
|
|
|
73
101
|
finish();
|
|
74
102
|
return;
|
|
75
103
|
}
|
|
76
|
-
|
|
77
|
-
const [fn, fnArgs, processFn] = this.asyncTasks[
|
|
104
|
+
for (let i = 0; i < this.asyncTasks.length; i++) {
|
|
105
|
+
const [fn, fnArgs, processFn] = this.asyncTasks[i];
|
|
78
106
|
const respondCallback = respond(processFn);
|
|
79
107
|
fn(...fnArgs, respondCallback);
|
|
80
108
|
}
|
|
@@ -137,14 +165,12 @@ export class Report {
|
|
|
137
165
|
return this.rootSchema.id;
|
|
138
166
|
}
|
|
139
167
|
hasError(errCode, errParams) {
|
|
140
|
-
let idx = this.errors.length;
|
|
141
|
-
while (idx--) {
|
|
168
|
+
for (let idx = 0; idx < this.errors.length; idx++) {
|
|
142
169
|
if (this.errors[idx].code === errCode) {
|
|
143
170
|
// assume match
|
|
144
171
|
let match = true;
|
|
145
172
|
// check the params too
|
|
146
|
-
let idx2 = this.errors[idx].params.length;
|
|
147
|
-
while (idx2--) {
|
|
173
|
+
for (let idx2 = 0; idx2 < this.errors[idx].params.length; idx2++) {
|
|
148
174
|
if (this.errors[idx].params[idx2] !== errParams[idx2]) {
|
|
149
175
|
match = false;
|
|
150
176
|
}
|
|
@@ -181,8 +207,7 @@ export class Report {
|
|
|
181
207
|
throw new Error('No errorMessage known for code ' + errorCode);
|
|
182
208
|
}
|
|
183
209
|
params = params || [];
|
|
184
|
-
let idx = params.length;
|
|
185
|
-
while (idx--) {
|
|
210
|
+
for (let idx = 0; idx < params.length; idx++) {
|
|
186
211
|
const param = params[idx] === null || isObject(params[idx]) ? JSON.stringify(params[idx]) : params[idx];
|
|
187
212
|
errorMessage = errorMessage.replace('{' + idx + '}', param.toString());
|
|
188
213
|
}
|
|
@@ -195,7 +220,6 @@ export class Report {
|
|
|
195
220
|
schemaId: this.getSchemaId(),
|
|
196
221
|
keyword: keyword,
|
|
197
222
|
};
|
|
198
|
-
// TODO v8: remove Symbol usage
|
|
199
223
|
err[schemaSymbol] = schema;
|
|
200
224
|
err[jsonSymbol] = this.getJson();
|
|
201
225
|
if (schema && typeof schema === 'string') {
|
|
@@ -214,12 +238,9 @@ export class Report {
|
|
|
214
238
|
subReports = [subReports];
|
|
215
239
|
}
|
|
216
240
|
err.inner = [];
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
let idx2 = subReport.errors.length;
|
|
221
|
-
while (idx2--) {
|
|
222
|
-
err.inner.push(subReport.errors[idx2]);
|
|
241
|
+
for (const subReport of subReports) {
|
|
242
|
+
for (const error of subReport.errors) {
|
|
243
|
+
err.inner.push(error);
|
|
223
244
|
}
|
|
224
245
|
}
|
|
225
246
|
if (err.inner.length === 0) {
|
package/dist/schema-cache.js
CHANGED
|
@@ -3,6 +3,35 @@ import { Report } from './report.js';
|
|
|
3
3
|
import { deepClone } from './utils/clone.js';
|
|
4
4
|
import { decodeJSONPointer } from './utils/json.js';
|
|
5
5
|
import { getQueryPath, getRemotePath, isAbsoluteUri } from './utils/uri.js';
|
|
6
|
+
import { normalizeOptions } from './z-schema-options.js';
|
|
7
|
+
const getEffectiveId = (schema) => {
|
|
8
|
+
let id = getId(schema);
|
|
9
|
+
if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
|
|
10
|
+
id = schema.id;
|
|
11
|
+
}
|
|
12
|
+
return id;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Shared logic for registering a remote reference schema.
|
|
16
|
+
* Used by both the static `ZSchema.setRemoteReference()` (global cache) and
|
|
17
|
+
* the instance `validator.setRemoteReference()` (instance cache).
|
|
18
|
+
*/
|
|
19
|
+
export function prepareRemoteSchema(schema, uri, validationOptions, maxCloneDepth) {
|
|
20
|
+
let _schema;
|
|
21
|
+
if (typeof schema === 'string') {
|
|
22
|
+
_schema = JSON.parse(schema);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
_schema = deepClone(schema, maxCloneDepth);
|
|
26
|
+
}
|
|
27
|
+
if (!_schema.id) {
|
|
28
|
+
_schema.id = uri;
|
|
29
|
+
}
|
|
30
|
+
if (validationOptions) {
|
|
31
|
+
_schema.__$validationOptions = normalizeOptions(validationOptions);
|
|
32
|
+
}
|
|
33
|
+
return _schema;
|
|
34
|
+
}
|
|
6
35
|
export class SchemaCache {
|
|
7
36
|
validator;
|
|
8
37
|
static global_cache = {};
|
|
@@ -41,31 +70,29 @@ export class SchemaCache {
|
|
|
41
70
|
return this.getSchemaByUri(report, refOrSchema);
|
|
42
71
|
}
|
|
43
72
|
// no caching done on this, but we need to return a clone so we can mutate it
|
|
44
|
-
return deepClone(refOrSchema);
|
|
73
|
+
return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
|
|
45
74
|
}
|
|
46
75
|
fromCache(path) {
|
|
47
76
|
let found = this.cache[path];
|
|
48
77
|
if (found) {
|
|
49
|
-
return
|
|
78
|
+
return found;
|
|
50
79
|
}
|
|
51
|
-
const asClone = (s) => {
|
|
52
|
-
if (!s.id || (!isAbsoluteUri(s.id) && isAbsoluteUri(path))) {
|
|
53
|
-
s.id = path;
|
|
54
|
-
}
|
|
55
|
-
return deepClone(s);
|
|
56
|
-
};
|
|
57
80
|
found = SchemaCache.global_cache[path];
|
|
58
81
|
if (found) {
|
|
59
|
-
|
|
82
|
+
// Clone once from global cache into instance cache so subsequent lookups
|
|
83
|
+
// never deep-clone again for the same path on this instance.
|
|
84
|
+
const clone = deepClone(found, this.validator.options.maxRecursionDepth);
|
|
85
|
+
if (!clone.id || (!isAbsoluteUri(clone.id) && isAbsoluteUri(path))) {
|
|
86
|
+
clone.id = path;
|
|
87
|
+
}
|
|
88
|
+
this.cache[path] = clone;
|
|
89
|
+
return clone;
|
|
60
90
|
}
|
|
61
91
|
return undefined;
|
|
62
92
|
}
|
|
63
93
|
getSchemaByUri(report, uri, root) {
|
|
64
94
|
if (root && !isAbsoluteUri(uri)) {
|
|
65
|
-
|
|
66
|
-
if ((!rootId || !isAbsoluteUri(rootId)) && typeof root.id === 'string' && isAbsoluteUri(root.id)) {
|
|
67
|
-
rootId = root.id;
|
|
68
|
-
}
|
|
95
|
+
const rootId = getEffectiveId(root);
|
|
69
96
|
if (rootId && isAbsoluteUri(rootId)) {
|
|
70
97
|
const hashIndex = rootId.indexOf('#');
|
|
71
98
|
const rootBase = hashIndex === -1 ? rootId : rootId.slice(0, hashIndex);
|
|
@@ -79,20 +106,40 @@ export class SchemaCache {
|
|
|
79
106
|
}
|
|
80
107
|
const remotePath = getRemotePath(uri);
|
|
81
108
|
const queryPath = getQueryPath(uri);
|
|
82
|
-
let result
|
|
109
|
+
let result;
|
|
110
|
+
let resolvedFromAncestor = false;
|
|
111
|
+
if (remotePath) {
|
|
112
|
+
const ancestorReport = report.getAncestor(remotePath);
|
|
113
|
+
if (ancestorReport?.rootSchema) {
|
|
114
|
+
result = ancestorReport.rootSchema;
|
|
115
|
+
resolvedFromAncestor = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!result && root && remotePath) {
|
|
119
|
+
const rootId = getEffectiveId(root);
|
|
120
|
+
const rootRemotePath = rootId ? getRemotePath(rootId) : undefined;
|
|
121
|
+
if (rootRemotePath && rootRemotePath === remotePath) {
|
|
122
|
+
result = root;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!result) {
|
|
126
|
+
result = remotePath ? this.fromCache(remotePath) : root;
|
|
127
|
+
}
|
|
83
128
|
if (result && remotePath && isAbsoluteUri(remotePath) && (!result.id || !isAbsoluteUri(result.id))) {
|
|
84
129
|
result.id = remotePath;
|
|
85
130
|
}
|
|
86
131
|
if (result && remotePath) {
|
|
87
132
|
// we need to avoid compiling schemas in a recursive loop
|
|
88
|
-
const compileRemote = result !== root;
|
|
133
|
+
const compileRemote = result !== root && !resolvedFromAncestor;
|
|
89
134
|
// now we need to compile and validate resolved schema (in case it's not already)
|
|
90
135
|
if (compileRemote) {
|
|
91
136
|
report.path.push(remotePath);
|
|
92
137
|
let remoteReport;
|
|
138
|
+
let usesAncestorReport = false;
|
|
93
139
|
const anscestorReport = result.id ? report.getAncestor(result.id) : undefined;
|
|
94
140
|
if (anscestorReport) {
|
|
95
141
|
remoteReport = anscestorReport;
|
|
142
|
+
usesAncestorReport = true;
|
|
96
143
|
}
|
|
97
144
|
else {
|
|
98
145
|
remoteReport = new Report(report);
|
|
@@ -103,14 +150,21 @@ export class SchemaCache {
|
|
|
103
150
|
// If custom validationOptions were provided to setRemoteReference(),
|
|
104
151
|
// use them instead of the default options
|
|
105
152
|
this.validator.options = result.__$validationOptions || this.validator.options;
|
|
106
|
-
|
|
153
|
+
const parentSchemaUri = typeof result.$schema === 'string' ? getRemotePath(result.$schema) : undefined;
|
|
154
|
+
const currentSchemaUri = report.getSchemaId();
|
|
155
|
+
const parentSchemaIsCompiling = !!parentSchemaUri &&
|
|
156
|
+
parentSchemaUri.length > 0 &&
|
|
157
|
+
(currentSchemaUri === parentSchemaUri || !!report.getAncestor(parentSchemaUri));
|
|
158
|
+
if (!parentSchemaIsCompiling) {
|
|
159
|
+
this.validator.sv.validateSchema(remoteReport, result);
|
|
160
|
+
}
|
|
107
161
|
}
|
|
108
162
|
finally {
|
|
109
163
|
this.validator.options = savedOptions;
|
|
110
164
|
}
|
|
111
165
|
}
|
|
112
166
|
}
|
|
113
|
-
const remoteReportIsValid = remoteReport.isValid();
|
|
167
|
+
const remoteReportIsValid = usesAncestorReport ? true : remoteReport.isValid();
|
|
114
168
|
if (!remoteReportIsValid) {
|
|
115
169
|
report.addError('REMOTE_NOT_VALID', [uri], remoteReport);
|
|
116
170
|
}
|
|
@@ -120,13 +174,14 @@ export class SchemaCache {
|
|
|
120
174
|
}
|
|
121
175
|
}
|
|
122
176
|
}
|
|
177
|
+
const resourceRoot = result;
|
|
123
178
|
if (result && queryPath) {
|
|
124
179
|
const parts = queryPath.split('/');
|
|
125
180
|
for (let idx = 0, lim = parts.length; result && idx < lim; idx++) {
|
|
126
181
|
const key = decodeJSONPointer(parts[idx]);
|
|
127
182
|
if (idx === 0) {
|
|
128
183
|
// it's an id
|
|
129
|
-
result = findId(result, key);
|
|
184
|
+
result = findId(result, key, remotePath, remotePath, this.validator.options.maxRecursionDepth);
|
|
130
185
|
}
|
|
131
186
|
else {
|
|
132
187
|
// it's a path behind id
|
|
@@ -134,6 +189,9 @@ export class SchemaCache {
|
|
|
134
189
|
}
|
|
135
190
|
}
|
|
136
191
|
}
|
|
192
|
+
if (result && typeof result === 'object' && resourceRoot && typeof resourceRoot === 'object') {
|
|
193
|
+
result.__$resourceRoot = resourceRoot;
|
|
194
|
+
}
|
|
137
195
|
return result;
|
|
138
196
|
}
|
|
139
197
|
}
|
package/dist/schema-compiler.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { getId } from './json-schema.js';
|
|
1
|
+
import { getId, isInternalKey, NON_SCHEMA_KEYWORDS } from './json-schema.js';
|
|
2
2
|
import { Report } from './report.js';
|
|
3
3
|
import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
|
|
4
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
|
|
4
5
|
import { getSchemaReader } from './z-schema-reader.js';
|
|
5
|
-
export const collectIds = (obj) => {
|
|
6
|
+
export const collectIds = (obj, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
|
|
6
7
|
const ids = [];
|
|
7
|
-
|
|
8
|
-
function walk(node, scope) {
|
|
8
|
+
function walk(node, scope, _depth = 0) {
|
|
9
9
|
if (typeof node !== 'object' || node == null)
|
|
10
10
|
return;
|
|
11
|
+
if (_depth >= maxDepth) {
|
|
12
|
+
throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in collectIds. ` +
|
|
13
|
+
'If your schema is deeply nested and valid, increase the maxRecursionDepth option.');
|
|
14
|
+
}
|
|
11
15
|
let addedScope = false;
|
|
12
16
|
const nodeId = getId(node);
|
|
13
17
|
if (typeof nodeId === 'string') {
|
|
@@ -24,7 +28,7 @@ export const collectIds = (obj) => {
|
|
|
24
28
|
id.absoluteUri = nodeId;
|
|
25
29
|
}
|
|
26
30
|
else if (type === 'root' && typeof node.id === 'string' && isAbsoluteUri(node.id) && node.id !== nodeId) {
|
|
27
|
-
id.absoluteUri =
|
|
31
|
+
id.absoluteUri = resolveSchemaScopeId(node.id, node, nodeId);
|
|
28
32
|
}
|
|
29
33
|
else if (type === 'relative') {
|
|
30
34
|
id.absoluteParent = scope
|
|
@@ -32,7 +36,7 @@ export const collectIds = (obj) => {
|
|
|
32
36
|
.slice(-1)[0];
|
|
33
37
|
if (id.absoluteParent) {
|
|
34
38
|
const parentUri = id.absoluteParent.absoluteUri || id.absoluteParent.id;
|
|
35
|
-
id.absoluteUri = parentUri
|
|
39
|
+
id.absoluteUri = resolveSchemaScopeId(parentUri, node, id.id);
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
ids.push(id);
|
|
@@ -41,14 +45,14 @@ export const collectIds = (obj) => {
|
|
|
41
45
|
}
|
|
42
46
|
if (Array.isArray(node)) {
|
|
43
47
|
for (const item of node) {
|
|
44
|
-
walk(item, scope);
|
|
48
|
+
walk(item, scope, _depth + 1);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
else {
|
|
48
52
|
for (const key of Object.keys(node)) {
|
|
49
|
-
if (key
|
|
53
|
+
if (isInternalKey(key) || NON_SCHEMA_KEYWORDS.includes(key))
|
|
50
54
|
continue;
|
|
51
|
-
walk(node[key], scope);
|
|
55
|
+
walk(node[key], scope, _depth + 1);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
if (addedScope) {
|
|
@@ -58,14 +62,18 @@ export const collectIds = (obj) => {
|
|
|
58
62
|
walk(obj, []);
|
|
59
63
|
return ids;
|
|
60
64
|
};
|
|
61
|
-
const
|
|
62
|
-
export const collectReferences = (obj, results, scope, path) => {
|
|
65
|
+
export const collectReferences = (obj, results, scope, path, options, maxDepth = DEFAULT_MAX_RECURSION_DEPTH, _depth = 0) => {
|
|
63
66
|
results = results || [];
|
|
64
67
|
scope = scope || [];
|
|
65
68
|
path = path || [];
|
|
69
|
+
options = options || {};
|
|
66
70
|
if (typeof obj !== 'object' || obj === null) {
|
|
67
71
|
return results;
|
|
68
72
|
}
|
|
73
|
+
if (_depth >= maxDepth) {
|
|
74
|
+
throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in collectReferences. ` +
|
|
75
|
+
'If your schema is deeply nested and valid, increase the maxRecursionDepth option.');
|
|
76
|
+
}
|
|
69
77
|
const hasRef = typeof obj.$ref === 'string' && typeof obj.__$refResolved === 'undefined';
|
|
70
78
|
let addedScope = false;
|
|
71
79
|
const isRootScope = scope.length === 0;
|
|
@@ -74,9 +82,9 @@ export const collectReferences = (obj, results, scope, path) => {
|
|
|
74
82
|
if (typeof obj.id === 'string' && isAbsoluteUri(obj.id) && (!scopeId || !isAbsoluteUri(scopeId))) {
|
|
75
83
|
scopeId = obj.id;
|
|
76
84
|
}
|
|
77
|
-
if (typeof scopeId === 'string' && (isRootScope || !hasRef)) {
|
|
85
|
+
if (typeof scopeId === 'string' && (isRootScope || !hasRef || options.useRefObjectScope === true)) {
|
|
78
86
|
const base = scope.length > 0 ? scope[scope.length - 1] : undefined;
|
|
79
|
-
scope.push(
|
|
87
|
+
scope.push(resolveSchemaScopeId(base, obj, scopeId));
|
|
80
88
|
addedScope = true;
|
|
81
89
|
}
|
|
82
90
|
if (hasRef) {
|
|
@@ -87,6 +95,22 @@ export const collectReferences = (obj, results, scope, path) => {
|
|
|
87
95
|
path: path.slice(0),
|
|
88
96
|
});
|
|
89
97
|
}
|
|
98
|
+
if (typeof obj.$recursiveRef === 'string' && typeof obj.__$recursiveRefResolved === 'undefined') {
|
|
99
|
+
results.push({
|
|
100
|
+
ref: resolveReference(scope[scope.length - 1], obj.$recursiveRef),
|
|
101
|
+
key: '$recursiveRef',
|
|
102
|
+
obj: obj,
|
|
103
|
+
path: path.slice(0),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (typeof obj.$dynamicRef === 'string' && typeof obj.__$dynamicRefResolved === 'undefined') {
|
|
107
|
+
results.push({
|
|
108
|
+
ref: resolveReference(scope[scope.length - 1], obj.$dynamicRef),
|
|
109
|
+
key: '$dynamicRef',
|
|
110
|
+
obj: obj,
|
|
111
|
+
path: path.slice(0),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
90
114
|
if (typeof obj.$schema === 'string' && typeof obj.__$schemaResolved === 'undefined') {
|
|
91
115
|
results.push({
|
|
92
116
|
ref: resolveReference(scope[scope.length - 1], obj.$schema),
|
|
@@ -95,25 +119,22 @@ export const collectReferences = (obj, results, scope, path) => {
|
|
|
95
119
|
path: path.slice(0),
|
|
96
120
|
});
|
|
97
121
|
}
|
|
98
|
-
let idx;
|
|
99
122
|
if (Array.isArray(obj)) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
path
|
|
103
|
-
collectReferences(obj[idx], results, scope, path);
|
|
123
|
+
for (let i = 0; i < obj.length; i++) {
|
|
124
|
+
path.push(i);
|
|
125
|
+
collectReferences(obj[i], results, scope, path, options, maxDepth, _depth + 1);
|
|
104
126
|
path.pop();
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
129
|
else {
|
|
108
130
|
const keys = Object.keys(obj);
|
|
109
|
-
|
|
110
|
-
while (idx--) {
|
|
131
|
+
for (const key of keys) {
|
|
111
132
|
// do not recurse through resolved references and other z-schema props
|
|
112
|
-
if (
|
|
133
|
+
if (isInternalKey(key) || NON_SCHEMA_KEYWORDS.includes(key)) {
|
|
113
134
|
continue;
|
|
114
135
|
}
|
|
115
|
-
path.push(
|
|
116
|
-
collectReferences(obj[
|
|
136
|
+
path.push(key);
|
|
137
|
+
collectReferences(obj[key], results, scope, path, options, maxDepth, _depth + 1);
|
|
117
138
|
path.pop();
|
|
118
139
|
}
|
|
119
140
|
}
|
|
@@ -151,29 +172,41 @@ const resolveReference = (base, ref) => {
|
|
|
151
172
|
}
|
|
152
173
|
return baseDir + ref;
|
|
153
174
|
};
|
|
175
|
+
const isSimpleIdentifier = (id) => id[0] !== '#' && !id.includes('/') && !id.includes('.') && !id.includes('#');
|
|
154
176
|
const resolveIdScope = (base, id) => {
|
|
155
177
|
if (isAbsoluteUri(id)) {
|
|
156
178
|
return id;
|
|
157
179
|
}
|
|
158
180
|
const baseStr = base ?? '';
|
|
159
181
|
// Treat simple identifiers (no '/', '.', or '#') as same-document fragment ids
|
|
160
|
-
if (
|
|
182
|
+
if (isSimpleIdentifier(id)) {
|
|
161
183
|
const hashIndex = baseStr.indexOf('#');
|
|
162
184
|
const baseNoFrag = hashIndex === -1 ? baseStr : baseStr.slice(0, hashIndex);
|
|
163
185
|
return baseNoFrag + '#' + id;
|
|
164
186
|
}
|
|
165
187
|
return resolveReference(base, id);
|
|
166
188
|
};
|
|
189
|
+
const resolveSchemaScopeId = (base, schema, id) => {
|
|
190
|
+
if (typeof schema.$id === 'string') {
|
|
191
|
+
return resolveReference(base, id);
|
|
192
|
+
}
|
|
193
|
+
return resolveIdScope(base, id);
|
|
194
|
+
};
|
|
167
195
|
export class SchemaCompiler {
|
|
168
196
|
validator;
|
|
169
197
|
constructor(validator) {
|
|
170
198
|
this.validator = validator;
|
|
171
199
|
}
|
|
172
200
|
collectAndCacheIds(schema) {
|
|
173
|
-
const ids = collectIds(schema);
|
|
201
|
+
const ids = collectIds(schema, this.validator.options.maxRecursionDepth);
|
|
174
202
|
for (const item of ids) {
|
|
175
203
|
if (item.absoluteUri) {
|
|
176
204
|
this.validator.scache.cacheSchemaByUri(item.absoluteUri, item.obj);
|
|
205
|
+
if (item.type === 'relative' && item.absoluteParent && isSimpleIdentifier(item.id)) {
|
|
206
|
+
const parentUri = item.absoluteParent.absoluteUri || item.absoluteParent.id;
|
|
207
|
+
const altAbsoluteUri = resolveReference(parentUri, item.id);
|
|
208
|
+
this.validator.scache.cacheSchemaByUri(altAbsoluteUri, item.obj);
|
|
209
|
+
}
|
|
177
210
|
}
|
|
178
211
|
else if (item.type === 'root') {
|
|
179
212
|
this.validator.scache.cacheSchemaByUri(item.id, item.obj);
|
|
@@ -185,7 +218,7 @@ export class SchemaCompiler {
|
|
|
185
218
|
// if schema is a string, assume it's a uri
|
|
186
219
|
if (typeof schema === 'string') {
|
|
187
220
|
const loadedSchema = this.validator.scache.getSchemaByUri(report, schema);
|
|
188
|
-
if (
|
|
221
|
+
if (typeof loadedSchema === 'undefined') {
|
|
189
222
|
report.addError('SCHEMA_NOT_REACHABLE', [schema]);
|
|
190
223
|
return false;
|
|
191
224
|
}
|
|
@@ -232,21 +265,21 @@ export class SchemaCompiler {
|
|
|
232
265
|
const isValidExceptReferences = report.isValid();
|
|
233
266
|
delete schema.__$missingReferences;
|
|
234
267
|
// collect all references that need to be resolved - $ref and $schema
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
268
|
+
const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
|
|
269
|
+
const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
|
|
270
|
+
for (const refObj of refs) {
|
|
238
271
|
// resolve all the collected references into __xxxResolved pointer
|
|
239
|
-
const refObj = refs[idx];
|
|
240
272
|
let response = this.validator.scache.getSchemaByUri(report, refObj.ref, schema);
|
|
241
273
|
// we can try to use custom schemaReader if available
|
|
242
|
-
if (
|
|
274
|
+
if (typeof response === 'undefined') {
|
|
243
275
|
const schemaReader = getSchemaReader();
|
|
244
276
|
if (schemaReader) {
|
|
245
277
|
const remotePath = getRemotePath(refObj.ref);
|
|
246
278
|
// it's supposed to return a valid schema
|
|
247
279
|
const s = schemaReader(remotePath);
|
|
248
280
|
if (s) {
|
|
249
|
-
// it needs to have the id
|
|
281
|
+
// it needs to have the id (cast: schemaReader returns JsonSchema, but
|
|
282
|
+
// at this pre-compilation stage we treat it as an internal object)
|
|
250
283
|
s.id = remotePath;
|
|
251
284
|
// try to compile the schema
|
|
252
285
|
const subreport = new Report(report);
|
|
@@ -260,7 +293,7 @@ export class SchemaCompiler {
|
|
|
260
293
|
}
|
|
261
294
|
}
|
|
262
295
|
}
|
|
263
|
-
if (
|
|
296
|
+
if (typeof response === 'undefined') {
|
|
264
297
|
const hasNotValid = report.hasError('REMOTE_NOT_VALID', [refObj.ref]);
|
|
265
298
|
const isAbsolute = isAbsoluteUri(refObj.ref);
|
|
266
299
|
let isDownloaded = false;
|
|
@@ -314,23 +347,15 @@ export class SchemaCompiler {
|
|
|
314
347
|
let compiled = 0, lastLoopCompiled;
|
|
315
348
|
do {
|
|
316
349
|
// remove all UNRESOLVABLE_REFERENCE errors before compiling array again
|
|
317
|
-
|
|
318
|
-
while (idx--) {
|
|
319
|
-
if (report.errors[idx].code === 'UNRESOLVABLE_REFERENCE') {
|
|
320
|
-
report.errors.splice(idx, 1);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
350
|
+
report.errors = report.errors.filter((e) => e.code !== 'UNRESOLVABLE_REFERENCE');
|
|
323
351
|
// remember how many were compiled in the last loop
|
|
324
352
|
lastLoopCompiled = compiled;
|
|
325
353
|
// count how many are compiled now
|
|
326
354
|
compiled = this.compileArrayOfSchemasLoop(report, arr);
|
|
327
355
|
// fix __$missingReferences if possible
|
|
328
|
-
|
|
329
|
-
while (idx--) {
|
|
330
|
-
const sch = arr[idx];
|
|
356
|
+
for (const sch of arr) {
|
|
331
357
|
if (sch.__$missingReferences) {
|
|
332
|
-
let idx2 = sch.__$missingReferences.length;
|
|
333
|
-
while (idx2--) {
|
|
358
|
+
for (let idx2 = sch.__$missingReferences.length - 1; idx2 >= 0; idx2--) {
|
|
334
359
|
const refObj = sch.__$missingReferences[idx2];
|
|
335
360
|
const response = arr.find((x) => x.id === refObj.ref);
|
|
336
361
|
if (response) {
|
|
@@ -350,11 +375,11 @@ export class SchemaCompiler {
|
|
|
350
375
|
return report.isValid();
|
|
351
376
|
}
|
|
352
377
|
compileArrayOfSchemasLoop(mainReport, arr) {
|
|
353
|
-
let
|
|
354
|
-
|
|
378
|
+
let compiledCount = 0;
|
|
379
|
+
for (const schema of arr) {
|
|
355
380
|
// try to compile each schema separately
|
|
356
381
|
const report = new Report(mainReport);
|
|
357
|
-
const isValid = this.compileSchema(report,
|
|
382
|
+
const isValid = this.compileSchema(report, schema);
|
|
358
383
|
if (isValid) {
|
|
359
384
|
compiledCount++;
|
|
360
385
|
}
|