z-schema 6.0.2 → 7.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +154 -134
  2. package/bin/z-schema +128 -124
  3. package/dist/Errors.js +50 -0
  4. package/dist/FormatValidators.js +136 -0
  5. package/{src → dist}/JsonValidation.js +184 -213
  6. package/dist/Report.js +220 -0
  7. package/{src → dist}/SchemaCache.js +67 -82
  8. package/{src → dist}/SchemaCompilation.js +89 -129
  9. package/dist/SchemaValidation.js +631 -0
  10. package/{src → dist}/Utils.js +96 -104
  11. package/dist/ZSchema-umd-min.js +1 -0
  12. package/dist/ZSchema-umd.js +13791 -0
  13. package/dist/ZSchema.cjs +13785 -0
  14. package/dist/ZSchema.js +366 -0
  15. package/dist/schemas/hyper-schema.json +156 -0
  16. package/dist/schemas/schema.json +151 -0
  17. package/dist/types/Errors.d.ts +44 -0
  18. package/dist/types/FormatValidators.d.ts +12 -0
  19. package/dist/types/JsonValidation.d.ts +37 -0
  20. package/dist/types/Report.d.ts +87 -0
  21. package/dist/types/SchemaCache.d.ts +26 -0
  22. package/dist/types/SchemaCompilation.d.ts +1 -0
  23. package/dist/types/SchemaValidation.d.ts +6 -0
  24. package/dist/types/Utils.d.ts +64 -0
  25. package/dist/types/ZSchema.d.ts +97 -0
  26. package/package.json +54 -43
  27. package/src/Errors.ts +56 -0
  28. package/src/FormatValidators.ts +136 -0
  29. package/src/JsonValidation.ts +624 -0
  30. package/src/Report.ts +337 -0
  31. package/src/SchemaCache.ts +189 -0
  32. package/src/SchemaCompilation.ts +293 -0
  33. package/src/SchemaValidation.ts +629 -0
  34. package/src/Utils.ts +286 -0
  35. package/src/ZSchema.ts +469 -0
  36. package/src/schemas/_ +0 -0
  37. package/dist/ZSchema-browser-min.js +0 -2
  38. package/dist/ZSchema-browser-min.js.map +0 -1
  39. package/dist/ZSchema-browser-test.js +0 -32247
  40. package/dist/ZSchema-browser.js +0 -12745
  41. package/index.d.ts +0 -175
  42. package/src/Errors.js +0 -60
  43. package/src/FormatValidators.js +0 -129
  44. package/src/Polyfills.js +0 -16
  45. package/src/Report.js +0 -299
  46. package/src/SchemaValidation.js +0 -619
  47. package/src/ZSchema.js +0 -409
package/src/Report.ts ADDED
@@ -0,0 +1,337 @@
1
+ import get from 'lodash.get';
2
+ import { Errors } from './Errors.js';
3
+ import * as Utils from './Utils.js';
4
+ import { ZSchemaOptions } from './ZSchema.js';
5
+
6
+ export interface SchemaError extends Error {
7
+ /**
8
+ * Implements the Error.name contract. The value is always "z-schema validation error".
9
+ */
10
+ name: string;
11
+
12
+ /**
13
+ * An identifier indicating the type of error.
14
+ * Example: "JSON_OBJECT_VALIDATION_FAILED"
15
+ */
16
+ message: string;
17
+
18
+ /**
19
+ * Returns details for each error that occurred during validation.
20
+ * See Options.breakOnFirstError.
21
+ */
22
+ details?: SchemaErrorDetail[];
23
+ }
24
+
25
+ export interface SchemaErrorDetail {
26
+ /**
27
+ * Example: "Expected type string but found type array"
28
+ */
29
+ message: string;
30
+ /**
31
+ * An error identifier that can be used to format a custom error message.
32
+ * Example: "INVALID_TYPE"
33
+ */
34
+ code: string;
35
+ /**
36
+ * Format parameters that can be used to format a custom error message.
37
+ * Example: ["string","array"]
38
+ */
39
+ params: Array<string>;
40
+ /**
41
+ * A JSON path indicating the location of the error.
42
+ * Example: "#/projects/1"
43
+ */
44
+ path: string | string[];
45
+ /**
46
+ * The schema rule description, which is included for certain errors where
47
+ * this information is useful (e.g. to describe a constraint).
48
+ */
49
+ title?: string;
50
+ description?: string;
51
+
52
+ /**
53
+ * Returns details for sub-schemas that failed to match. For example, if the schema
54
+ * uses the "oneOf" constraint to accept several alternative possibilities, each
55
+ * alternative will have its own inner detail object explaining why it failed to match.
56
+ */
57
+ inner?: SchemaErrorDetail[];
58
+
59
+ schemaId?: string;
60
+ }
61
+
62
+ export interface ReportOptions {
63
+ maxErrors?: number;
64
+ }
65
+
66
+ type TaskResult = unknown;
67
+ type TaskFn = (...args: unknown[]) => TaskResult;
68
+ type TaskFnArgs = Parameters<TaskFn>;
69
+ type TaskProcessFn = (result: ReturnType<TaskFn>) => void;
70
+ type AsyncTask = [TaskFn, TaskFnArgs, TaskProcessFn];
71
+
72
+ export class Report {
73
+ errors: SchemaErrorDetail[];
74
+ parentReport?: Report;
75
+ options: ZSchemaOptions;
76
+ reportOptions: ReportOptions;
77
+ path: string[];
78
+ asyncTasks: AsyncTask[];
79
+ rootSchema?: {
80
+ id?: string;
81
+ };
82
+ commonErrorMessage?: string;
83
+ json?: unknown;
84
+
85
+ constructor(parentOrOptions, reportOptions?) {
86
+ this.parentReport = parentOrOptions instanceof Report ? parentOrOptions : undefined;
87
+
88
+ this.options = parentOrOptions instanceof Report ? parentOrOptions.options : parentOrOptions || {};
89
+
90
+ 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
+ }
103
+
104
+ isValid(): boolean {
105
+ if (this.asyncTasks.length > 0) {
106
+ throw new Error("Async tasks pending, can't answer isValid");
107
+ }
108
+ return this.errors.length === 0;
109
+ }
110
+
111
+ addAsyncTask(fn, args, asyncTaskResultProcessFn) {
112
+ this.asyncTasks.push([fn, args, asyncTaskResultProcessFn]);
113
+ }
114
+
115
+ getAncestor(id) {
116
+ if (!this.parentReport) {
117
+ return undefined;
118
+ }
119
+ if (this.parentReport.getSchemaId() === id) {
120
+ return this.parentReport;
121
+ }
122
+ return this.parentReport.getAncestor(id);
123
+ }
124
+
125
+ processAsyncTasks(timeout, callback) {
126
+ const validationTimeout = timeout || 2000;
127
+ let tasksCount = this.asyncTasks.length;
128
+ let idx = tasksCount;
129
+ let timedOut = false;
130
+
131
+ const finish = () => {
132
+ setTimeout(() => {
133
+ const valid = this.errors.length === 0,
134
+ err = valid ? null : this.errors;
135
+ callback(err, valid);
136
+ }, 0);
137
+ };
138
+
139
+ function respond(asyncTaskResultProcessFn: TaskProcessFn) {
140
+ return function (asyncTaskResult) {
141
+ if (timedOut) {
142
+ return;
143
+ }
144
+ asyncTaskResultProcessFn(asyncTaskResult);
145
+ if (--tasksCount === 0) {
146
+ finish();
147
+ }
148
+ };
149
+ }
150
+
151
+ // finish if tasks are completed or there are any errors and breaking on first error was requested
152
+ if (tasksCount === 0 || (this.errors.length > 0 && this.options.breakOnFirstError)) {
153
+ finish();
154
+ return;
155
+ }
156
+
157
+ while (idx--) {
158
+ const [fn, fnArgs, processFn] = this.asyncTasks[idx];
159
+ const respondCallback = respond(processFn);
160
+ fn(...fnArgs, respondCallback);
161
+ }
162
+
163
+ setTimeout(() => {
164
+ if (tasksCount > 0) {
165
+ timedOut = true;
166
+ this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
167
+ callback(this.errors, false);
168
+ }
169
+ }, validationTimeout);
170
+ }
171
+
172
+ getPath(returnPathAsString) {
173
+ /**
174
+ * @type {string[]|string}
175
+ */
176
+ let path = [];
177
+ if (this.parentReport) {
178
+ path = path.concat(this.parentReport.path);
179
+ }
180
+ path = path.concat(this.path);
181
+
182
+ if (returnPathAsString !== true) {
183
+ // Sanitize the path segments (http://tools.ietf.org/html/rfc6901#section-4)
184
+ return (
185
+ '#/' +
186
+ path
187
+ .map(function (segment) {
188
+ segment = segment.toString();
189
+
190
+ if (Utils.isAbsoluteUri(segment)) {
191
+ return 'uri(' + segment + ')';
192
+ }
193
+
194
+ return segment.replace(/~/g, '~0').replace(/\//g, '~1');
195
+ })
196
+ .join('/')
197
+ );
198
+ }
199
+ return path;
200
+ }
201
+
202
+ getSchemaId() {
203
+ if (!this.rootSchema) {
204
+ return null;
205
+ }
206
+
207
+ // get the error path as an array
208
+ let path = [];
209
+ if (this.parentReport) {
210
+ path = path.concat(this.parentReport.path);
211
+ }
212
+ path = path.concat(this.path);
213
+
214
+ // try to find id in the error path
215
+ while (path.length > 0) {
216
+ const obj = get(this.rootSchema, path);
217
+ if (obj && obj.id) {
218
+ return obj.id;
219
+ }
220
+ path.pop();
221
+ }
222
+
223
+ // return id of the root
224
+ return this.rootSchema.id;
225
+ }
226
+
227
+ hasError(errorCode, params) {
228
+ let idx = this.errors.length;
229
+ while (idx--) {
230
+ if (this.errors[idx].code === errorCode) {
231
+ // assume match
232
+ let match = true;
233
+
234
+ // check the params too
235
+ let idx2 = this.errors[idx].params.length;
236
+ while (idx2--) {
237
+ if (this.errors[idx].params[idx2] !== params[idx2]) {
238
+ match = false;
239
+ }
240
+ }
241
+
242
+ // if match, return true
243
+ if (match) {
244
+ return match;
245
+ }
246
+ }
247
+ }
248
+ return false;
249
+ }
250
+
251
+ addError(errorCode, params, subReports?, schema?) {
252
+ if (!errorCode) {
253
+ throw new Error('No errorCode passed into addError()');
254
+ }
255
+ this.addCustomError(errorCode, Errors[errorCode], params, subReports, schema);
256
+ }
257
+
258
+ getJson() {
259
+ if (this.json) {
260
+ return this.json;
261
+ }
262
+ if (this.parentReport) {
263
+ return this.parentReport.getJson();
264
+ }
265
+ return undefined;
266
+ }
267
+
268
+ addCustomError(
269
+ errorCode: string,
270
+ errorMessage: string,
271
+ params: string[],
272
+ subReports?: Report[] | Report,
273
+ schema?: {
274
+ title?: string;
275
+ description?: string;
276
+ }
277
+ ) {
278
+ if (this.errors.length >= this.reportOptions.maxErrors) {
279
+ return;
280
+ }
281
+
282
+ if (!errorMessage) {
283
+ throw new Error('No errorMessage known for code ' + errorCode);
284
+ }
285
+
286
+ params = params || [];
287
+
288
+ let idx = params.length;
289
+ while (idx--) {
290
+ const whatIs = Utils.whatIs(params[idx]);
291
+ const param = whatIs === 'object' || whatIs === 'null' ? JSON.stringify(params[idx]) : params[idx];
292
+ errorMessage = errorMessage.replace('{' + idx + '}', param);
293
+ }
294
+
295
+ const err: SchemaErrorDetail = {
296
+ code: errorCode,
297
+ params: params,
298
+ message: errorMessage,
299
+ path: this.getPath(this.options.reportPathAsArray),
300
+ schemaId: this.getSchemaId(),
301
+ };
302
+
303
+ err[Utils.schemaSymbol] = schema;
304
+ err[Utils.jsonSymbol] = this.getJson();
305
+
306
+ if (schema && typeof schema === 'string') {
307
+ err.description = schema;
308
+ } else if (schema && typeof schema === 'object') {
309
+ if (schema.title) {
310
+ err.title = schema.title;
311
+ }
312
+ if (schema.description) {
313
+ err.description = schema.description;
314
+ }
315
+ }
316
+
317
+ if (subReports != null) {
318
+ if (!Array.isArray(subReports)) {
319
+ subReports = [subReports];
320
+ }
321
+ err.inner = [];
322
+ idx = subReports.length;
323
+ while (idx--) {
324
+ const subReport = subReports[idx];
325
+ let idx2 = subReport.errors.length;
326
+ while (idx2--) {
327
+ err.inner.push(subReport.errors[idx2]);
328
+ }
329
+ }
330
+ if (err.inner.length === 0) {
331
+ err.inner = undefined;
332
+ }
333
+ }
334
+
335
+ this.errors.push(err);
336
+ }
337
+ }
@@ -0,0 +1,189 @@
1
+ import isequal from 'lodash.isequal';
2
+ import { Report } from './Report.js';
3
+ import { compileSchema } from './SchemaCompilation.js';
4
+ import * as SchemaValidation from './SchemaValidation.js';
5
+ import * as Utils from './Utils.js';
6
+
7
+ function decodeJSONPointer(str) {
8
+ // http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07#section-3
9
+ return decodeURIComponent(str).replace(/~[0-1]/g, function (x) {
10
+ return x === '~1' ? '/' : '~';
11
+ });
12
+ }
13
+
14
+ export function getRemotePath(uri) {
15
+ const io = uri.indexOf('#');
16
+ return io === -1 ? uri : uri.slice(0, io);
17
+ }
18
+
19
+ function getQueryPath(uri) {
20
+ const io = uri.indexOf('#');
21
+ const res = io === -1 ? undefined : uri.slice(io + 1);
22
+ // WARN: do not slice slash, #/ means take root and go down from it
23
+ // if (res && res[0] === "/") { res = res.slice(1); }
24
+ return res;
25
+ }
26
+
27
+ function findId(schema, id) {
28
+ // process only arrays and objects
29
+ if (typeof schema !== 'object' || schema === null) {
30
+ return;
31
+ }
32
+
33
+ // no id means root so return itself
34
+ if (!id) {
35
+ return schema;
36
+ }
37
+
38
+ if (schema.id) {
39
+ if (schema.id === id || (schema.id[0] === '#' && schema.id.substring(1) === id)) {
40
+ return schema;
41
+ }
42
+ }
43
+
44
+ let idx, result;
45
+ if (Array.isArray(schema)) {
46
+ idx = schema.length;
47
+ while (idx--) {
48
+ result = findId(schema[idx], id);
49
+ if (result) {
50
+ return result;
51
+ }
52
+ }
53
+ } else {
54
+ const keys = Object.keys(schema);
55
+ idx = keys.length;
56
+ while (idx--) {
57
+ const k = keys[idx];
58
+ if (k.indexOf('__$') === 0) {
59
+ continue;
60
+ }
61
+ result = findId(schema[k], id);
62
+ if (result) {
63
+ return result;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ *
71
+ * @param {*} uri
72
+ * @param {*} schema
73
+ *
74
+ * @returns {void}
75
+ */
76
+ export function cacheSchemaByUri(uri, schema) {
77
+ const remotePath = getRemotePath(uri);
78
+ if (remotePath) {
79
+ this.cache[remotePath] = schema;
80
+ }
81
+ }
82
+
83
+ /**
84
+ *
85
+ * @param {*} uri
86
+ *
87
+ * @returns {void}
88
+ */
89
+ export function removeFromCacheByUri(uri) {
90
+ const remotePath = getRemotePath(uri);
91
+ if (remotePath) {
92
+ delete this.cache[remotePath];
93
+ }
94
+ }
95
+
96
+ /**
97
+ *
98
+ * @param {*} uri
99
+ *
100
+ * @returns {boolean}
101
+ */
102
+ export function checkCacheForUri(uri) {
103
+ const remotePath = getRemotePath(uri);
104
+ return remotePath ? this.cache[remotePath] != null : false;
105
+ }
106
+
107
+ export function getSchema(report, schema) {
108
+ if (typeof schema === 'object') {
109
+ schema = getSchemaByReference.call(this, report, schema);
110
+ }
111
+ if (typeof schema === 'string') {
112
+ schema = getSchemaByUri.call(this, report, schema);
113
+ }
114
+ return schema;
115
+ }
116
+
117
+ export function getSchemaByReference(report, key) {
118
+ let i = this.referenceCache.length;
119
+ while (i--) {
120
+ if (isequal(this.referenceCache[i][0], key)) {
121
+ return this.referenceCache[i][1];
122
+ }
123
+ }
124
+ // not found
125
+ const schema = Utils.cloneDeep(key);
126
+ this.referenceCache.push([key, schema]);
127
+ return schema;
128
+ }
129
+
130
+ export function getSchemaByUri(report, uri, root) {
131
+ const remotePath = getRemotePath(uri);
132
+ const queryPath = getQueryPath(uri);
133
+ let result = remotePath ? this.cache[remotePath] : root;
134
+
135
+ if (result && remotePath) {
136
+ // we need to avoid compiling schemas in a recursive loop
137
+ const compileRemote = result !== root;
138
+ // now we need to compile and validate resolved schema (in case it's not already)
139
+ if (compileRemote) {
140
+ report.path.push(remotePath);
141
+
142
+ let remoteReport;
143
+
144
+ const anscestorReport = report.getAncestor(result.id);
145
+ if (anscestorReport) {
146
+ remoteReport = anscestorReport;
147
+ } else {
148
+ remoteReport = new Report(report);
149
+ if (compileSchema.call(this, remoteReport, result)) {
150
+ const savedOptions = this.options;
151
+ try {
152
+ // If custom validationOptions were provided to setRemoteReference(),
153
+ // use them instead of the default options
154
+ this.options = result.__$validationOptions || this.options;
155
+ SchemaValidation.validateSchema.call(this, remoteReport, result);
156
+ } finally {
157
+ this.options = savedOptions;
158
+ }
159
+ }
160
+ }
161
+ const remoteReportIsValid = remoteReport.isValid();
162
+ if (!remoteReportIsValid) {
163
+ report.addError('REMOTE_NOT_VALID', [uri], remoteReport);
164
+ }
165
+
166
+ report.path.pop();
167
+
168
+ if (!remoteReportIsValid) {
169
+ return undefined;
170
+ }
171
+ }
172
+ }
173
+
174
+ if (result && queryPath) {
175
+ const parts = queryPath.split('/');
176
+ for (let idx = 0, lim = parts.length; result && idx < lim; idx++) {
177
+ const key = decodeJSONPointer(parts[idx]);
178
+ if (idx === 0) {
179
+ // it's an id
180
+ result = findId(result, key);
181
+ } else {
182
+ // it's a path behind id
183
+ result = result[key];
184
+ }
185
+ }
186
+ }
187
+
188
+ return result;
189
+ }