z-schema 7.0.0 → 7.0.6
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 +7 -11
- package/bin/z-schema +0 -0
- package/cjs/index.d.ts +192 -117
- package/cjs/index.js +949 -998
- package/{src/FormatValidators.ts → dist/format-validators.js} +97 -65
- package/dist/index.js +1 -1
- package/dist/json-schema.js +40 -0
- package/dist/{JsonValidation.js → json-validation.js} +75 -69
- package/dist/{Report.js → report.js} +35 -45
- package/dist/schema-cache.js +109 -0
- package/dist/schema-compiler.js +255 -0
- package/dist/{SchemaValidation.js → schema-validator.js} +153 -149
- package/dist/types/{Errors.d.ts → errors.d.ts} +2 -0
- package/dist/types/format-validators.d.ts +10 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/json-schema.d.ts +50 -0
- package/dist/types/json-validation.d.ts +7 -0
- package/dist/types/{Report.d.ts → report.d.ts} +22 -23
- package/dist/types/schema-cache.d.ts +17 -0
- package/dist/types/schema-compiler.d.ts +16 -0
- package/dist/types/schema-validator.d.ts +10 -0
- package/dist/types/utils/array.d.ts +2 -0
- package/dist/types/utils/clone.d.ts +2 -0
- package/dist/types/utils/json.d.ts +7 -0
- package/dist/types/utils/symbols.d.ts +2 -0
- package/dist/types/utils/unicode.d.ts +14 -0
- package/dist/types/utils/uri.d.ts +4 -0
- package/dist/types/utils/what-is.d.ts +3 -0
- package/dist/types/z-schema.d.ts +75 -0
- package/dist/utils/array.js +27 -0
- package/dist/utils/clone.js +61 -0
- package/dist/utils/json.js +59 -0
- package/dist/utils/symbols.js +2 -0
- package/dist/utils/unicode.js +45 -0
- package/dist/utils/uri.js +15 -0
- package/dist/utils/what-is.js +29 -0
- package/dist/{ZSchema.js → z-schema.js} +66 -77
- package/package.json +8 -4
- package/src/{Errors.ts → errors.ts} +4 -0
- package/src/format-validators.ts +191 -0
- package/src/index.ts +12 -1
- package/src/json-schema.ts +97 -0
- package/src/{JsonValidation.ts → json-validation.ts} +137 -127
- package/src/{Report.ts → report.ts} +60 -70
- package/src/schema-cache.ts +122 -0
- package/src/schema-compiler.ts +300 -0
- package/src/{SchemaValidation.ts → schema-validator.ts} +213 -215
- package/src/utils/array.ts +29 -0
- package/src/utils/clone.ts +63 -0
- package/src/utils/json.ts +74 -0
- package/src/utils/symbols.ts +3 -0
- package/src/utils/unicode.ts +43 -0
- package/src/utils/uri.ts +18 -0
- package/src/utils/what-is.ts +46 -0
- package/src/{ZSchema.ts → z-schema.ts} +108 -113
- package/umd/ZSchema.js +949 -998
- package/umd/ZSchema.min.js +1 -1
- package/dist/FormatValidators.js +0 -136
- package/dist/SchemaCache.js +0 -173
- package/dist/SchemaCompilation.js +0 -259
- package/dist/Utils.js +0 -266
- package/dist/types/FormatValidators.d.ts +0 -12
- package/dist/types/JsonValidation.d.ts +0 -37
- package/dist/types/SchemaCache.d.ts +0 -26
- package/dist/types/SchemaCompilation.d.ts +0 -1
- package/dist/types/SchemaValidation.d.ts +0 -6
- package/dist/types/Utils.d.ts +0 -64
- package/dist/types/ZSchema.d.ts +0 -97
- package/src/SchemaCache.ts +0 -189
- package/src/SchemaCompilation.ts +0 -293
- package/src/Utils.ts +0 -286
- /package/dist/{Errors.js → errors.js} +0 -0
- /package/dist/schemas/{hyper-schema.json → draft-04-hyper-schema.json} +0 -0
- /package/dist/schemas/{schema.json → draft-04-schema.json} +0 -0
- /package/src/schemas/{hyper-schema.json → draft-04-hyper-schema.json} +0 -0
- /package/src/schemas/{schema.json → draft-04-schema.json} +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import get from 'lodash.get';
|
|
2
|
-
import { Errors } from './
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { ErrorCode, ErrorParam, Errors } from './errors.js';
|
|
3
|
+
import { whatIs } from './utils/what-is.js';
|
|
4
|
+
import { schemaSymbol, jsonSymbol } from './utils/symbols.js';
|
|
5
|
+
import { ValidateCallback, ZSchemaOptions } from './z-schema.js';
|
|
6
|
+
import { JsonSchema, JsonSchemaInternal } from './json-schema.js';
|
|
7
|
+
import { isAbsoluteUri } from './utils/uri.js';
|
|
5
8
|
|
|
6
9
|
export interface SchemaError extends Error {
|
|
7
10
|
/**
|
|
@@ -36,12 +39,12 @@ export interface SchemaErrorDetail {
|
|
|
36
39
|
* Format parameters that can be used to format a custom error message.
|
|
37
40
|
* Example: ["string","array"]
|
|
38
41
|
*/
|
|
39
|
-
params:
|
|
42
|
+
params: ErrorParam[];
|
|
40
43
|
/**
|
|
41
44
|
* A JSON path indicating the location of the error.
|
|
42
45
|
* Example: "#/projects/1"
|
|
43
46
|
*/
|
|
44
|
-
path: string | string
|
|
47
|
+
path: string | Array<string | number>;
|
|
45
48
|
/**
|
|
46
49
|
* The schema rule description, which is included for certain errors where
|
|
47
50
|
* this information is useful (e.g. to describe a constraint).
|
|
@@ -70,35 +73,24 @@ type TaskProcessFn = (result: ReturnType<TaskFn>) => void;
|
|
|
70
73
|
type AsyncTask = [TaskFn, TaskFnArgs, TaskProcessFn];
|
|
71
74
|
|
|
72
75
|
export class Report {
|
|
73
|
-
|
|
76
|
+
asyncTasks: AsyncTask[] = [];
|
|
77
|
+
commonErrorMessage?: string;
|
|
78
|
+
errors: SchemaErrorDetail[] = [];
|
|
79
|
+
json?: unknown;
|
|
80
|
+
path: Array<number | string> = [];
|
|
81
|
+
rootSchema?: JsonSchemaInternal;
|
|
82
|
+
|
|
74
83
|
parentReport?: Report;
|
|
75
84
|
options: ZSchemaOptions;
|
|
76
85
|
reportOptions: ReportOptions;
|
|
77
|
-
path: string[];
|
|
78
|
-
asyncTasks: AsyncTask[];
|
|
79
|
-
rootSchema?: {
|
|
80
|
-
id?: string;
|
|
81
|
-
};
|
|
82
|
-
commonErrorMessage?: string;
|
|
83
|
-
json?: unknown;
|
|
84
86
|
|
|
85
|
-
constructor(
|
|
87
|
+
constructor(zschemaOptions: ZSchemaOptions); // primary
|
|
88
|
+
constructor(parentReport: Report); // subreport
|
|
89
|
+
constructor(parentReport: Report, reportOptions: ReportOptions); // subreport with options
|
|
90
|
+
constructor(parentOrOptions: ZSchemaOptions | Report, reportOptions?: ReportOptions) {
|
|
86
91
|
this.parentReport = parentOrOptions instanceof Report ? parentOrOptions : undefined;
|
|
87
|
-
|
|
88
92
|
this.options = parentOrOptions instanceof Report ? parentOrOptions.options : parentOrOptions || {};
|
|
89
|
-
|
|
90
93
|
this.reportOptions = reportOptions || {};
|
|
91
|
-
|
|
92
|
-
this.errors = [];
|
|
93
|
-
/**
|
|
94
|
-
* @type {string[]}
|
|
95
|
-
*/
|
|
96
|
-
this.path = [];
|
|
97
|
-
this.asyncTasks = [];
|
|
98
|
-
|
|
99
|
-
this.rootSchema = undefined;
|
|
100
|
-
this.commonErrorMessage = undefined;
|
|
101
|
-
this.json = undefined;
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
isValid(): boolean {
|
|
@@ -108,11 +100,15 @@ export class Report {
|
|
|
108
100
|
return this.errors.length === 0;
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
addAsyncTask
|
|
112
|
-
|
|
103
|
+
addAsyncTask<FV, FN extends (...args: any[]) => FV>(
|
|
104
|
+
fn: FN,
|
|
105
|
+
args: Parameters<FN>,
|
|
106
|
+
asyncTaskResultProcessFn: (result: ReturnType<FN>) => void
|
|
107
|
+
) {
|
|
108
|
+
this.asyncTasks.push([fn, args, asyncTaskResultProcessFn as TaskProcessFn]);
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
getAncestor(id) {
|
|
111
|
+
getAncestor(id: string): Report | undefined {
|
|
116
112
|
if (!this.parentReport) {
|
|
117
113
|
return undefined;
|
|
118
114
|
}
|
|
@@ -122,7 +118,7 @@ export class Report {
|
|
|
122
118
|
return this.parentReport.getAncestor(id);
|
|
123
119
|
}
|
|
124
120
|
|
|
125
|
-
processAsyncTasks(timeout, callback) {
|
|
121
|
+
processAsyncTasks(timeout: number | undefined, callback: ValidateCallback) {
|
|
126
122
|
const validationTimeout = timeout || 2000;
|
|
127
123
|
let tasksCount = this.asyncTasks.length;
|
|
128
124
|
let idx = tasksCount;
|
|
@@ -136,17 +132,15 @@ export class Report {
|
|
|
136
132
|
}, 0);
|
|
137
133
|
};
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
}
|
|
135
|
+
const respond = (asyncTaskResultProcessFn: TaskProcessFn) => (asyncTaskResult: TaskResult) => {
|
|
136
|
+
if (timedOut) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
asyncTaskResultProcessFn(asyncTaskResult);
|
|
140
|
+
if (--tasksCount === 0) {
|
|
141
|
+
finish();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
150
144
|
|
|
151
145
|
// finish if tasks are completed or there are any errors and breaking on first error was requested
|
|
152
146
|
if (tasksCount === 0 || (this.errors.length > 0 && this.options.breakOnFirstError)) {
|
|
@@ -169,11 +163,8 @@ export class Report {
|
|
|
169
163
|
}, validationTimeout);
|
|
170
164
|
}
|
|
171
165
|
|
|
172
|
-
getPath(returnPathAsString) {
|
|
173
|
-
|
|
174
|
-
* @type {string[]|string}
|
|
175
|
-
*/
|
|
176
|
-
let path = [];
|
|
166
|
+
getPath(returnPathAsString?: boolean) {
|
|
167
|
+
let path: Array<string | number> = [];
|
|
177
168
|
if (this.parentReport) {
|
|
178
169
|
path = path.concat(this.parentReport.path);
|
|
179
170
|
}
|
|
@@ -187,7 +178,7 @@ export class Report {
|
|
|
187
178
|
.map(function (segment) {
|
|
188
179
|
segment = segment.toString();
|
|
189
180
|
|
|
190
|
-
if (
|
|
181
|
+
if (isAbsoluteUri(segment)) {
|
|
191
182
|
return 'uri(' + segment + ')';
|
|
192
183
|
}
|
|
193
184
|
|
|
@@ -199,13 +190,13 @@ export class Report {
|
|
|
199
190
|
return path;
|
|
200
191
|
}
|
|
201
192
|
|
|
202
|
-
getSchemaId() {
|
|
193
|
+
getSchemaId(): string | undefined {
|
|
203
194
|
if (!this.rootSchema) {
|
|
204
|
-
return
|
|
195
|
+
return undefined;
|
|
205
196
|
}
|
|
206
197
|
|
|
207
198
|
// get the error path as an array
|
|
208
|
-
let path = [];
|
|
199
|
+
let path: Array<string | number> = [];
|
|
209
200
|
if (this.parentReport) {
|
|
210
201
|
path = path.concat(this.parentReport.path);
|
|
211
202
|
}
|
|
@@ -224,17 +215,17 @@ export class Report {
|
|
|
224
215
|
return this.rootSchema.id;
|
|
225
216
|
}
|
|
226
217
|
|
|
227
|
-
hasError(
|
|
218
|
+
hasError(errCode: string, errParams: Array<any>) {
|
|
228
219
|
let idx = this.errors.length;
|
|
229
220
|
while (idx--) {
|
|
230
|
-
if (this.errors[idx].code ===
|
|
221
|
+
if (this.errors[idx].code === errCode) {
|
|
231
222
|
// assume match
|
|
232
223
|
let match = true;
|
|
233
224
|
|
|
234
225
|
// check the params too
|
|
235
226
|
let idx2 = this.errors[idx].params.length;
|
|
236
227
|
while (idx2--) {
|
|
237
|
-
if (this.errors[idx].params[idx2] !==
|
|
228
|
+
if (this.errors[idx].params[idx2] !== errParams[idx2]) {
|
|
238
229
|
match = false;
|
|
239
230
|
}
|
|
240
231
|
}
|
|
@@ -248,14 +239,15 @@ export class Report {
|
|
|
248
239
|
return false;
|
|
249
240
|
}
|
|
250
241
|
|
|
251
|
-
addError(
|
|
252
|
-
if (!
|
|
242
|
+
addError(errCode: ErrorCode, errParams?: ErrorParam[], subReports?: Report | Report[], schema?: JsonSchema) {
|
|
243
|
+
if (!errCode) {
|
|
253
244
|
throw new Error('No errorCode passed into addError()');
|
|
254
245
|
}
|
|
255
|
-
this.addCustomError(
|
|
246
|
+
this.addCustomError(errCode, Errors[errCode], errParams, subReports, schema);
|
|
256
247
|
}
|
|
257
248
|
|
|
258
|
-
|
|
249
|
+
// this returns the root object being validated (the one passed into validator.validate)
|
|
250
|
+
getJson(): unknown {
|
|
259
251
|
if (this.json) {
|
|
260
252
|
return this.json;
|
|
261
253
|
}
|
|
@@ -268,14 +260,11 @@ export class Report {
|
|
|
268
260
|
addCustomError(
|
|
269
261
|
errorCode: string,
|
|
270
262
|
errorMessage: string,
|
|
271
|
-
params
|
|
272
|
-
subReports?: Report
|
|
273
|
-
schema?:
|
|
274
|
-
title?: string;
|
|
275
|
-
description?: string;
|
|
276
|
-
}
|
|
263
|
+
params?: ErrorParam[],
|
|
264
|
+
subReports?: Report | Report[],
|
|
265
|
+
schema?: JsonSchema
|
|
277
266
|
) {
|
|
278
|
-
if (this.errors.length >= this.reportOptions.maxErrors) {
|
|
267
|
+
if (typeof this.reportOptions.maxErrors === 'number' && this.errors.length >= this.reportOptions.maxErrors) {
|
|
279
268
|
return;
|
|
280
269
|
}
|
|
281
270
|
|
|
@@ -287,9 +276,9 @@ export class Report {
|
|
|
287
276
|
|
|
288
277
|
let idx = params.length;
|
|
289
278
|
while (idx--) {
|
|
290
|
-
const
|
|
291
|
-
const param =
|
|
292
|
-
errorMessage = errorMessage.replace('{' + idx + '}', param);
|
|
279
|
+
const paramType = whatIs(params[idx]);
|
|
280
|
+
const param = paramType === 'object' || paramType === 'null' ? JSON.stringify(params[idx]) : params[idx];
|
|
281
|
+
errorMessage = errorMessage.replace('{' + idx + '}', param.toString());
|
|
293
282
|
}
|
|
294
283
|
|
|
295
284
|
const err: SchemaErrorDetail = {
|
|
@@ -300,8 +289,9 @@ export class Report {
|
|
|
300
289
|
schemaId: this.getSchemaId(),
|
|
301
290
|
};
|
|
302
291
|
|
|
303
|
-
|
|
304
|
-
err[
|
|
292
|
+
// TODO v8: remove Symbol usage
|
|
293
|
+
(err as any)[schemaSymbol] = schema;
|
|
294
|
+
(err as any)[jsonSymbol] = this.getJson();
|
|
305
295
|
|
|
306
296
|
if (schema && typeof schema === 'string') {
|
|
307
297
|
err.description = schema;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ZSchema } from './z-schema.js';
|
|
2
|
+
import isequal from 'lodash.isequal';
|
|
3
|
+
import { Report } from './report.js';
|
|
4
|
+
import { findId, JsonSchemaInternal } from './json-schema.js';
|
|
5
|
+
import { getQueryPath, getRemotePath } from './utils/uri.js';
|
|
6
|
+
import { deepClone } from './utils/clone.js';
|
|
7
|
+
import { decodeJSONPointer } from './utils/json.js';
|
|
8
|
+
|
|
9
|
+
export type SchemaCacheStorage = Record<string, JsonSchemaInternal>;
|
|
10
|
+
export type ReferenceSchemaCacheStorage = Array<[JsonSchemaInternal, JsonSchemaInternal]>;
|
|
11
|
+
|
|
12
|
+
export class SchemaCache {
|
|
13
|
+
cache: SchemaCacheStorage = {};
|
|
14
|
+
referenceCache: ReferenceSchemaCacheStorage = [];
|
|
15
|
+
|
|
16
|
+
constructor(private validator: ZSchema) {}
|
|
17
|
+
|
|
18
|
+
cacheSchemaByUri(uri: string, schema: JsonSchemaInternal) {
|
|
19
|
+
const remotePath = getRemotePath(uri);
|
|
20
|
+
if (remotePath) {
|
|
21
|
+
this.cache[remotePath] = schema;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
removeFromCacheByUri(uri: string) {
|
|
26
|
+
const remotePath = getRemotePath(uri);
|
|
27
|
+
if (remotePath) {
|
|
28
|
+
delete this.cache[remotePath];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
checkCacheForUri(uri: string) {
|
|
33
|
+
const remotePath = getRemotePath(uri);
|
|
34
|
+
return remotePath ? this.cache[remotePath] != null : false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getSchema(report: Report, refOrSchema: string | JsonSchemaInternal) {
|
|
38
|
+
if (typeof refOrSchema === 'string') {
|
|
39
|
+
// ref input
|
|
40
|
+
return this.getSchemaByUri(report, refOrSchema);
|
|
41
|
+
}
|
|
42
|
+
if (typeof refOrSchema === 'object') {
|
|
43
|
+
// schema obj input
|
|
44
|
+
return this.getSchemaByReference(report, refOrSchema);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`unexpected code reached`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getSchemaByUri(report: Report, uri: string, root?: JsonSchemaInternal) {
|
|
50
|
+
const remotePath = getRemotePath(uri);
|
|
51
|
+
const queryPath = getQueryPath(uri);
|
|
52
|
+
let result = remotePath ? this.cache[remotePath] : root;
|
|
53
|
+
|
|
54
|
+
if (result && remotePath) {
|
|
55
|
+
// we need to avoid compiling schemas in a recursive loop
|
|
56
|
+
const compileRemote = result !== root;
|
|
57
|
+
// now we need to compile and validate resolved schema (in case it's not already)
|
|
58
|
+
if (compileRemote) {
|
|
59
|
+
report.path.push(remotePath);
|
|
60
|
+
|
|
61
|
+
let remoteReport;
|
|
62
|
+
|
|
63
|
+
const anscestorReport = result.id ? report.getAncestor(result.id) : undefined;
|
|
64
|
+
if (anscestorReport) {
|
|
65
|
+
remoteReport = anscestorReport;
|
|
66
|
+
} else {
|
|
67
|
+
remoteReport = new Report(report);
|
|
68
|
+
if (this.validator.sc.compileSchema(remoteReport, result)) {
|
|
69
|
+
const savedOptions = this.validator.options;
|
|
70
|
+
try {
|
|
71
|
+
// If custom validationOptions were provided to setRemoteReference(),
|
|
72
|
+
// use them instead of the default options
|
|
73
|
+
this.validator.options = result.__$validationOptions || this.validator.options;
|
|
74
|
+
this.validator.sv.validateSchema(remoteReport, result);
|
|
75
|
+
} finally {
|
|
76
|
+
this.validator.options = savedOptions;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const remoteReportIsValid = remoteReport.isValid();
|
|
81
|
+
if (!remoteReportIsValid) {
|
|
82
|
+
report.addError('REMOTE_NOT_VALID', [uri], remoteReport);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
report.path.pop();
|
|
86
|
+
|
|
87
|
+
if (!remoteReportIsValid) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (result && queryPath) {
|
|
94
|
+
const parts = queryPath.split('/');
|
|
95
|
+
for (let idx = 0, lim = parts.length; result && idx < lim; idx++) {
|
|
96
|
+
const key = decodeJSONPointer(parts[idx]);
|
|
97
|
+
if (idx === 0) {
|
|
98
|
+
// it's an id
|
|
99
|
+
result = findId(result, key);
|
|
100
|
+
} else {
|
|
101
|
+
// it's a path behind id
|
|
102
|
+
result = result[key as keyof typeof result] as JsonSchemaInternal | undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getSchemaByReference(report: Report, schema: JsonSchemaInternal) {
|
|
111
|
+
let i = this.referenceCache.length;
|
|
112
|
+
while (i--) {
|
|
113
|
+
if (isequal(this.referenceCache[i][0], schema)) {
|
|
114
|
+
return this.referenceCache[i][1];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// not found
|
|
118
|
+
const schemaClone = deepClone(schema);
|
|
119
|
+
this.referenceCache.push([schema, schemaClone]);
|
|
120
|
+
return schemaClone;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { ZSchema } from './z-schema.js';
|
|
2
|
+
import { JsonSchemaInternal } from './json-schema.js';
|
|
3
|
+
import { Report } from './report.js';
|
|
4
|
+
import { isAbsoluteUri, isRelativeUri } from './utils/uri.js';
|
|
5
|
+
|
|
6
|
+
export interface Reference {
|
|
7
|
+
ref: string;
|
|
8
|
+
key: '$ref' | '$schema';
|
|
9
|
+
obj: JsonSchemaInternal;
|
|
10
|
+
path: Array<string | number>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const collectReferences = (
|
|
14
|
+
obj: JsonSchemaInternal,
|
|
15
|
+
results?: Reference[],
|
|
16
|
+
scope?: string[],
|
|
17
|
+
path?: Reference['path']
|
|
18
|
+
) => {
|
|
19
|
+
results = results || [];
|
|
20
|
+
scope = scope || [];
|
|
21
|
+
path = path || [];
|
|
22
|
+
|
|
23
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
24
|
+
return results;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof obj.id === 'string') {
|
|
28
|
+
scope.push(obj.id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof obj.$ref === 'string' && typeof obj.__$refResolved === 'undefined') {
|
|
32
|
+
results.push({
|
|
33
|
+
ref: mergeReference(scope, obj.$ref),
|
|
34
|
+
key: '$ref',
|
|
35
|
+
obj: obj,
|
|
36
|
+
path: path.slice(0),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (typeof obj.$schema === 'string' && typeof obj.__$schemaResolved === 'undefined') {
|
|
40
|
+
results.push({
|
|
41
|
+
ref: mergeReference(scope, obj.$schema),
|
|
42
|
+
key: '$schema',
|
|
43
|
+
obj: obj,
|
|
44
|
+
path: path.slice(0),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let idx;
|
|
49
|
+
if (Array.isArray(obj)) {
|
|
50
|
+
idx = obj.length;
|
|
51
|
+
while (idx--) {
|
|
52
|
+
path.push(idx);
|
|
53
|
+
collectReferences(obj[idx], results, scope, path);
|
|
54
|
+
path.pop();
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const keys = Object.keys(obj);
|
|
58
|
+
idx = keys.length;
|
|
59
|
+
while (idx--) {
|
|
60
|
+
// do not recurse through resolved references and other z-schema props
|
|
61
|
+
if (keys[idx].indexOf('__$') === 0) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
path.push(keys[idx]);
|
|
65
|
+
collectReferences((obj as any)[keys[idx]], results, scope, path);
|
|
66
|
+
path.pop();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof obj.id === 'string') {
|
|
71
|
+
scope.pop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return results;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mergeReference = (scope: string[], ref: string) => {
|
|
78
|
+
if (isAbsoluteUri(ref)) {
|
|
79
|
+
return ref;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let joinedScope = scope.join('');
|
|
83
|
+
const isScopeAbsolute = isAbsoluteUri(joinedScope);
|
|
84
|
+
const isScopeRelative = isRelativeUri(joinedScope);
|
|
85
|
+
const isRefRelative = isRelativeUri(ref);
|
|
86
|
+
let toRemove;
|
|
87
|
+
|
|
88
|
+
if (isScopeAbsolute && isRefRelative) {
|
|
89
|
+
toRemove = joinedScope.match(/\/[^/]*$/);
|
|
90
|
+
if (toRemove) {
|
|
91
|
+
joinedScope = joinedScope.slice(0, toRemove.index! + 1);
|
|
92
|
+
}
|
|
93
|
+
} else if (isScopeRelative && isRefRelative) {
|
|
94
|
+
joinedScope = '';
|
|
95
|
+
} else {
|
|
96
|
+
toRemove = joinedScope.match(/[^#/]+$/);
|
|
97
|
+
if (toRemove) {
|
|
98
|
+
joinedScope = joinedScope.slice(0, toRemove.index);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let res = joinedScope + ref;
|
|
103
|
+
res = res.replace(/##/, '#');
|
|
104
|
+
return res;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export class SchemaCompiler {
|
|
108
|
+
constructor(private validator: ZSchema) {}
|
|
109
|
+
|
|
110
|
+
compileSchema(report: Report, schema: JsonSchemaInternal) {
|
|
111
|
+
report.commonErrorMessage = 'SCHEMA_COMPILATION_FAILED';
|
|
112
|
+
|
|
113
|
+
// if schema is a string, assume it's a uri
|
|
114
|
+
if (typeof schema === 'string') {
|
|
115
|
+
const loadedSchema = this.validator.scache.getSchemaByUri(report, schema);
|
|
116
|
+
if (!loadedSchema) {
|
|
117
|
+
report.addError('SCHEMA_NOT_REACHABLE', [schema]);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
schema = loadedSchema;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// if schema is an array, assume it's an array of schemas
|
|
124
|
+
if (Array.isArray(schema)) {
|
|
125
|
+
return this.compileArrayOfSchemas(report, schema);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// if we have an id than it should be cached already (if this instance has compiled it)
|
|
129
|
+
if (schema.__$compiled && schema.id && this.validator.scache.checkCacheForUri(schema.id) === false) {
|
|
130
|
+
schema.__$compiled = undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// do not re-compile schemas
|
|
134
|
+
if (schema.__$compiled) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (schema.id && typeof schema.id === 'string') {
|
|
139
|
+
// add this to our schemaCache (before compilation in case we have references including id)
|
|
140
|
+
this.validator.scache.cacheSchemaByUri(schema.id, schema);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// this method can be called recursively, so we need to remember our root
|
|
144
|
+
let isRoot = false;
|
|
145
|
+
if (!report.rootSchema) {
|
|
146
|
+
report.rootSchema = schema;
|
|
147
|
+
isRoot = true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// delete all __$missingReferences from previous compilation attempts
|
|
151
|
+
const isValidExceptReferences = report.isValid();
|
|
152
|
+
delete schema.__$missingReferences;
|
|
153
|
+
|
|
154
|
+
// collect all references that need to be resolved - $ref and $schema
|
|
155
|
+
const refs = collectReferences(schema);
|
|
156
|
+
let idx = refs.length;
|
|
157
|
+
while (idx--) {
|
|
158
|
+
// resolve all the collected references into __xxxResolved pointer
|
|
159
|
+
const refObj = refs[idx];
|
|
160
|
+
let response = this.validator.scache.getSchemaByUri(report, refObj.ref, schema);
|
|
161
|
+
|
|
162
|
+
// we can try to use custom schemaReader if available
|
|
163
|
+
if (!response) {
|
|
164
|
+
const schemaReader = this.validator.getSchemaReader();
|
|
165
|
+
if (schemaReader) {
|
|
166
|
+
// it's supposed to return a valid schema
|
|
167
|
+
const s = schemaReader(refObj.ref);
|
|
168
|
+
if (s) {
|
|
169
|
+
// it needs to have the id
|
|
170
|
+
s.id = refObj.ref;
|
|
171
|
+
// try to compile the schema
|
|
172
|
+
const subreport = new Report(report);
|
|
173
|
+
if (!this.compileSchema(subreport, s)) {
|
|
174
|
+
// copy errors to report
|
|
175
|
+
report.errors = report.errors.concat(subreport.errors);
|
|
176
|
+
} else {
|
|
177
|
+
response = this.validator.scache.getSchemaByUri(report, refObj.ref, schema);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!response) {
|
|
184
|
+
const hasNotValid = report.hasError('REMOTE_NOT_VALID', [refObj.ref]);
|
|
185
|
+
const isAbsolute = isAbsoluteUri(refObj.ref);
|
|
186
|
+
let isDownloaded = false;
|
|
187
|
+
const ignoreUnresolvableRemotes = this.validator.options.ignoreUnresolvableReferences === true;
|
|
188
|
+
|
|
189
|
+
if (isAbsolute) {
|
|
190
|
+
// we shouldn't add UNRESOLVABLE_REFERENCE for schemas we already have downloaded
|
|
191
|
+
// and set through setRemoteReference method
|
|
192
|
+
isDownloaded = this.validator.scache.checkCacheForUri(refObj.ref);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (hasNotValid) {
|
|
196
|
+
// already has REMOTE_NOT_VALID error for this one
|
|
197
|
+
} else if (ignoreUnresolvableRemotes && isAbsolute) {
|
|
198
|
+
// ignoreUnresolvableRemotes is on and remote isAbsolute
|
|
199
|
+
} else if (isDownloaded) {
|
|
200
|
+
// remote is downloaded, so no UNRESOLVABLE_REFERENCE
|
|
201
|
+
} else {
|
|
202
|
+
report.path.push(...refObj.path);
|
|
203
|
+
report.addError('UNRESOLVABLE_REFERENCE', [refObj.ref]);
|
|
204
|
+
report.path = report.path.slice(0, -refObj.path.length);
|
|
205
|
+
|
|
206
|
+
// pusblish unresolved references out
|
|
207
|
+
if (isValidExceptReferences) {
|
|
208
|
+
schema.__$missingReferences = schema.__$missingReferences || [];
|
|
209
|
+
schema.__$missingReferences.push(refObj);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// this might create circular references
|
|
214
|
+
refObj.obj[`__${refObj.key}Resolved`] = response;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const isValid = report.isValid();
|
|
218
|
+
if (isValid) {
|
|
219
|
+
schema.__$compiled = true;
|
|
220
|
+
} else {
|
|
221
|
+
if (schema.id && typeof schema.id === 'string') {
|
|
222
|
+
// remove this schema from schemaCache because it failed to compile
|
|
223
|
+
this.validator.scache.removeFromCacheByUri(schema.id);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// we don't need the root pointer anymore
|
|
228
|
+
if (isRoot) {
|
|
229
|
+
report.rootSchema = undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return isValid;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
compileArrayOfSchemas(report: Report, arr: JsonSchemaInternal[]) {
|
|
236
|
+
let compiled = 0,
|
|
237
|
+
lastLoopCompiled;
|
|
238
|
+
|
|
239
|
+
do {
|
|
240
|
+
// remove all UNRESOLVABLE_REFERENCE errors before compiling array again
|
|
241
|
+
let idx = report.errors.length;
|
|
242
|
+
while (idx--) {
|
|
243
|
+
if (report.errors[idx].code === 'UNRESOLVABLE_REFERENCE') {
|
|
244
|
+
report.errors.splice(idx, 1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// remember how many were compiled in the last loop
|
|
249
|
+
lastLoopCompiled = compiled;
|
|
250
|
+
|
|
251
|
+
// count how many are compiled now
|
|
252
|
+
compiled = this.compileArrayOfSchemasLoop(report, arr);
|
|
253
|
+
|
|
254
|
+
// fix __$missingReferences if possible
|
|
255
|
+
idx = arr.length;
|
|
256
|
+
while (idx--) {
|
|
257
|
+
const sch = arr[idx];
|
|
258
|
+
if (sch.__$missingReferences) {
|
|
259
|
+
let idx2 = sch.__$missingReferences.length;
|
|
260
|
+
while (idx2--) {
|
|
261
|
+
const refObj = sch.__$missingReferences[idx2];
|
|
262
|
+
const response = arr.find((x) => x.id === refObj.ref);
|
|
263
|
+
if (response) {
|
|
264
|
+
// this might create circular references
|
|
265
|
+
refObj.obj[`__${refObj.key}Resolved`] = response;
|
|
266
|
+
// it's resolved now so delete it
|
|
267
|
+
sch.__$missingReferences.splice(idx2, 1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (sch.__$missingReferences.length === 0) {
|
|
271
|
+
delete sch.__$missingReferences;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// keep repeating if not all compiled and at least one more was compiled in the last loop
|
|
277
|
+
} while (compiled !== arr.length && compiled !== lastLoopCompiled);
|
|
278
|
+
|
|
279
|
+
return report.isValid();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
compileArrayOfSchemasLoop(mainReport: Report, arr: JsonSchemaInternal[]) {
|
|
283
|
+
let idx = arr.length,
|
|
284
|
+
compiledCount = 0;
|
|
285
|
+
|
|
286
|
+
while (idx--) {
|
|
287
|
+
// try to compile each schema separately
|
|
288
|
+
const report = new Report(mainReport);
|
|
289
|
+
const isValid = this.compileSchema(report, arr[idx]);
|
|
290
|
+
if (isValid) {
|
|
291
|
+
compiledCount++;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// copy errors to report
|
|
295
|
+
mainReport.errors = mainReport.errors.concat(report.errors);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return compiledCount;
|
|
299
|
+
}
|
|
300
|
+
}
|