schema-shield 0.0.6 → 1.0.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.
- package/README.md +219 -65
- package/dist/formats.d.ts.map +1 -1
- package/dist/index.d.ts +25 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1837 -484
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/index.mjs +1837 -484
- package/dist/keywords/array-keywords.d.ts.map +1 -1
- package/dist/keywords/object-keywords.d.ts.map +1 -1
- package/dist/keywords/other-keywords.d.ts.map +1 -1
- package/dist/keywords/string-keywords.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/deep-freeze.d.ts +5 -0
- package/dist/utils/deep-freeze.d.ts.map +1 -0
- package/dist/utils/has-changed.d.ts +2 -0
- package/dist/utils/has-changed.d.ts.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/{utils.d.ts → utils/main-utils.d.ts} +7 -9
- package/dist/utils/main-utils.d.ts.map +1 -0
- package/dist/utils/pattern-matcher.d.ts +3 -0
- package/dist/utils/pattern-matcher.d.ts.map +1 -0
- package/lib/formats.ts +468 -155
- package/lib/index.ts +702 -107
- package/lib/keywords/array-keywords.ts +260 -52
- package/lib/keywords/number-keywords.ts +1 -1
- package/lib/keywords/object-keywords.ts +295 -88
- package/lib/keywords/other-keywords.ts +263 -70
- package/lib/keywords/string-keywords.ts +123 -7
- package/lib/types.ts +5 -18
- package/lib/utils/deep-freeze.ts +208 -0
- package/lib/utils/has-changed.ts +51 -0
- package/lib/utils/index.ts +4 -0
- package/lib/{utils.ts → utils/main-utils.ts} +63 -77
- package/lib/utils/pattern-matcher.ts +66 -0
- package/package.json +2 -2
- package/tsconfig.json +4 -4
- package/dist/utils.d.ts.map +0 -1
package/lib/index.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
+
/****************** Path: lib/index.ts ******************/
|
|
1
2
|
import {
|
|
2
3
|
DefineErrorFunction,
|
|
3
4
|
ValidationError,
|
|
4
|
-
deepClone,
|
|
5
5
|
getDefinedErrorFunctionForKey,
|
|
6
6
|
getNamedFunction,
|
|
7
|
-
|
|
8
|
-
} from "./utils";
|
|
7
|
+
resolvePath
|
|
8
|
+
} from "./utils/main-utils";
|
|
9
9
|
|
|
10
10
|
import { Formats } from "./formats";
|
|
11
11
|
import { Types } from "./types";
|
|
12
12
|
import { keywords } from "./keywords";
|
|
13
|
+
import { deepCloneUnfreeze } from "./utils/deep-freeze";
|
|
13
14
|
|
|
14
|
-
export { ValidationError } from "./utils";
|
|
15
|
+
export { ValidationError } from "./utils/main-utils";
|
|
16
|
+
export { deepCloneUnfreeze as deepClone } from "./utils/deep-freeze";
|
|
15
17
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
export type Result = void | ValidationError;
|
|
18
|
+
export type Result = void | ValidationError | true;
|
|
19
19
|
|
|
20
20
|
export interface KeywordFunction {
|
|
21
21
|
(
|
|
@@ -44,22 +44,37 @@ export interface CompiledSchema {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface Validator {
|
|
47
|
-
(data: any): {
|
|
47
|
+
(data: any): {
|
|
48
|
+
data: any;
|
|
49
|
+
error: ValidationError | null | true;
|
|
50
|
+
valid: boolean;
|
|
51
|
+
};
|
|
48
52
|
compiledSchema: CompiledSchema;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
interface ValidatorItem {
|
|
56
|
+
name: string;
|
|
57
|
+
validate: ValidateFunction;
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
export class SchemaShield {
|
|
52
61
|
private types: Record<string, TypeFunction | false> = {};
|
|
53
62
|
private formats: Record<string, FormatFunction | false> = {};
|
|
54
63
|
private keywords: Record<string, KeywordFunction | false> = {};
|
|
55
64
|
private immutable = false;
|
|
65
|
+
private rootSchema: CompiledSchema | null = null;
|
|
66
|
+
private idRegistry: Map<string, CompiledSchema> = new Map();
|
|
67
|
+
private failFast: boolean = true;
|
|
56
68
|
|
|
57
69
|
constructor({
|
|
58
|
-
immutable = false
|
|
70
|
+
immutable = false,
|
|
71
|
+
failFast = true
|
|
59
72
|
}: {
|
|
60
73
|
immutable?: boolean;
|
|
74
|
+
failFast?: boolean;
|
|
61
75
|
} = {}) {
|
|
62
76
|
this.immutable = immutable;
|
|
77
|
+
this.failFast = failFast;
|
|
63
78
|
|
|
64
79
|
for (const [type, validator] of Object.entries(Types)) {
|
|
65
80
|
if (validator) {
|
|
@@ -100,6 +115,10 @@ export class SchemaShield {
|
|
|
100
115
|
return this.formats[format];
|
|
101
116
|
}
|
|
102
117
|
|
|
118
|
+
isDefaultFormatValidator(format: string, validator: FormatFunction): boolean {
|
|
119
|
+
return (Formats as Record<string, FormatFunction | false>)[format] === validator;
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
addKeyword(name: string, validator: KeywordFunction, overwrite = false) {
|
|
104
123
|
if (this.keywords[name] && !overwrite) {
|
|
105
124
|
throw new ValidationError(`Keyword "${name}" already exists`);
|
|
@@ -111,175 +130,690 @@ export class SchemaShield {
|
|
|
111
130
|
return this.keywords[keyword];
|
|
112
131
|
}
|
|
113
132
|
|
|
133
|
+
getSchemaRef(path: string): CompiledSchema | undefined {
|
|
134
|
+
if (!this.rootSchema) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
return resolvePath(this.rootSchema, path);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getSchemaById(id: string): CompiledSchema | undefined {
|
|
141
|
+
return this.idRegistry.get(id);
|
|
142
|
+
}
|
|
143
|
+
|
|
114
144
|
compile(schema: any): Validator {
|
|
145
|
+
this.idRegistry.clear();
|
|
115
146
|
const compiledSchema = this.compileSchema(schema);
|
|
147
|
+
this.rootSchema = compiledSchema;
|
|
148
|
+
if ((compiledSchema as any)._hasRef === true) {
|
|
149
|
+
this.linkReferences(compiledSchema);
|
|
150
|
+
}
|
|
151
|
+
|
|
116
152
|
if (!compiledSchema.$validate) {
|
|
117
|
-
if (
|
|
153
|
+
if (schema === false) {
|
|
154
|
+
const defineError = getDefinedErrorFunctionForKey(
|
|
155
|
+
"oneOf",
|
|
156
|
+
compiledSchema,
|
|
157
|
+
this.failFast
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
161
|
+
"Validate_False",
|
|
162
|
+
(data) => defineError("Value is not valid", { data })
|
|
163
|
+
);
|
|
164
|
+
} else if (schema === true) {
|
|
165
|
+
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
166
|
+
"Validate_Any",
|
|
167
|
+
() => {}
|
|
168
|
+
);
|
|
169
|
+
} else if (this.isSchemaLike(schema) === false) {
|
|
118
170
|
throw new ValidationError("Invalid schema");
|
|
171
|
+
} else {
|
|
172
|
+
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
173
|
+
"Validate_Any",
|
|
174
|
+
() => {}
|
|
175
|
+
);
|
|
119
176
|
}
|
|
120
|
-
|
|
121
|
-
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
122
|
-
"any",
|
|
123
|
-
() => {}
|
|
124
|
-
);
|
|
125
177
|
}
|
|
126
178
|
|
|
127
179
|
const validate: Validator = (data: any) => {
|
|
128
|
-
|
|
129
|
-
const error = compiledSchema.$validate(clonedData);
|
|
180
|
+
this.rootSchema = compiledSchema;
|
|
130
181
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
182
|
+
const clonedData = this.immutable ? deepCloneUnfreeze(data) : data;
|
|
183
|
+
const res = compiledSchema.$validate!(clonedData);
|
|
184
|
+
|
|
185
|
+
if (res) {
|
|
186
|
+
return { data: clonedData, error: res, valid: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { data: clonedData, error: null, valid: true };
|
|
136
190
|
};
|
|
137
191
|
|
|
138
192
|
validate.compiledSchema = compiledSchema;
|
|
139
|
-
|
|
140
193
|
return validate;
|
|
141
194
|
}
|
|
142
195
|
|
|
196
|
+
private isPlainObject(value: any): value is Record<string, any> {
|
|
197
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private isTrivialAlwaysValidSubschema(value: any): boolean {
|
|
201
|
+
return (
|
|
202
|
+
value === true ||
|
|
203
|
+
(this.isPlainObject(value) && Object.keys(value).length === 0)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private shallowArrayEquals(a: any[], b: any[]): boolean {
|
|
208
|
+
if (a === b) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (a.length !== b.length) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < a.length; i++) {
|
|
217
|
+
if (a[i] !== b[i]) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private flattenAssociativeBranches(
|
|
226
|
+
key: "allOf" | "anyOf",
|
|
227
|
+
branches: any[]
|
|
228
|
+
): any[] {
|
|
229
|
+
const out: any[] = [];
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < branches.length; i++) {
|
|
232
|
+
const item = branches[i];
|
|
233
|
+
if (
|
|
234
|
+
this.isPlainObject(item) &&
|
|
235
|
+
Object.keys(item).length === 1 &&
|
|
236
|
+
Array.isArray(item[key])
|
|
237
|
+
) {
|
|
238
|
+
const nested = this.flattenAssociativeBranches(key, item[key]);
|
|
239
|
+
for (let j = 0; j < nested.length; j++) {
|
|
240
|
+
out.push(nested[j]);
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
out.push(item);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private flattenSingleWrapperOneOf(branches: any[]): any[] {
|
|
251
|
+
let current = branches;
|
|
252
|
+
|
|
253
|
+
while (current.length === 1) {
|
|
254
|
+
const item = current[0];
|
|
255
|
+
if (
|
|
256
|
+
this.isPlainObject(item) &&
|
|
257
|
+
Object.keys(item).length === 1 &&
|
|
258
|
+
Array.isArray(item.oneOf)
|
|
259
|
+
) {
|
|
260
|
+
current = item.oneOf;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return current;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private normalizeSchemaForCompile(schema: Record<string, any>): Record<string, any> {
|
|
270
|
+
let normalized = schema;
|
|
271
|
+
const schemaKeys = Object.keys(schema);
|
|
272
|
+
const hasOnlyKey = (key: string) =>
|
|
273
|
+
schemaKeys.length === 1 && schemaKeys[0] === key;
|
|
274
|
+
|
|
275
|
+
const setNormalized = (key: string, value: any) => {
|
|
276
|
+
if (normalized === schema) {
|
|
277
|
+
normalized = { ...schema };
|
|
278
|
+
}
|
|
279
|
+
normalized[key] = value;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (Array.isArray(schema.allOf)) {
|
|
283
|
+
const flattenedAllOf = this.flattenAssociativeBranches(
|
|
284
|
+
"allOf",
|
|
285
|
+
schema.allOf
|
|
286
|
+
).filter(
|
|
287
|
+
(item) =>
|
|
288
|
+
!(
|
|
289
|
+
this.isPlainObject(item) && Object.keys(item).length === 0
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
hasOnlyKey("allOf") &&
|
|
295
|
+
flattenedAllOf.length === 1 &&
|
|
296
|
+
this.isPlainObject(flattenedAllOf[0])
|
|
297
|
+
) {
|
|
298
|
+
return flattenedAllOf[0];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!this.shallowArrayEquals(flattenedAllOf, schema.allOf)) {
|
|
302
|
+
setNormalized("allOf", flattenedAllOf);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (Array.isArray(schema.anyOf)) {
|
|
307
|
+
const flattenedAnyOf = this.flattenAssociativeBranches(
|
|
308
|
+
"anyOf",
|
|
309
|
+
schema.anyOf
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
if (
|
|
313
|
+
hasOnlyKey("anyOf") &&
|
|
314
|
+
flattenedAnyOf.length === 1 &&
|
|
315
|
+
this.isPlainObject(flattenedAnyOf[0])
|
|
316
|
+
) {
|
|
317
|
+
return flattenedAnyOf[0];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!this.shallowArrayEquals(flattenedAnyOf, schema.anyOf)) {
|
|
321
|
+
setNormalized("anyOf", flattenedAnyOf);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (Array.isArray(schema.oneOf)) {
|
|
326
|
+
const flattenedOneOf = this.flattenSingleWrapperOneOf(schema.oneOf);
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
hasOnlyKey("oneOf") &&
|
|
330
|
+
flattenedOneOf.length === 1 &&
|
|
331
|
+
this.isPlainObject(flattenedOneOf[0])
|
|
332
|
+
) {
|
|
333
|
+
return flattenedOneOf[0];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!this.shallowArrayEquals(flattenedOneOf, schema.oneOf)) {
|
|
337
|
+
setNormalized("oneOf", flattenedOneOf);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return normalized;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private markSchemaHasRef(schema: CompiledSchema) {
|
|
345
|
+
if ((schema as any)._hasRef === true) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
Object.defineProperty(schema, "_hasRef", {
|
|
350
|
+
value: true,
|
|
351
|
+
enumerable: false,
|
|
352
|
+
configurable: false,
|
|
353
|
+
writable: false
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private shouldSkipKeyword(schema: Record<string, any>, key: string): boolean {
|
|
358
|
+
const value = schema[key];
|
|
359
|
+
|
|
360
|
+
switch (key) {
|
|
361
|
+
case "required":
|
|
362
|
+
return Array.isArray(value) && value.length === 0;
|
|
363
|
+
case "uniqueItems":
|
|
364
|
+
return value === false;
|
|
365
|
+
case "properties":
|
|
366
|
+
case "patternProperties":
|
|
367
|
+
case "dependencies":
|
|
368
|
+
return (
|
|
369
|
+
this.isPlainObject(value) &&
|
|
370
|
+
Object.keys(value).length === 0
|
|
371
|
+
);
|
|
372
|
+
case "propertyNames":
|
|
373
|
+
case "items":
|
|
374
|
+
return value === true;
|
|
375
|
+
case "additionalProperties":
|
|
376
|
+
if (value === true) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
value === false &&
|
|
382
|
+
this.isPlainObject(schema.patternProperties) &&
|
|
383
|
+
Object.keys(schema.patternProperties).length > 0
|
|
384
|
+
);
|
|
385
|
+
case "additionalItems":
|
|
386
|
+
return value === true || !Array.isArray(schema.items);
|
|
387
|
+
case "allOf": {
|
|
388
|
+
if (!Array.isArray(value)) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (value.length === 0) {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < value.length; i++) {
|
|
397
|
+
if (this.isTrivialAlwaysValidSubschema(value[i])) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
case "anyOf": {
|
|
407
|
+
if (!Array.isArray(value)) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
for (let i = 0; i < value.length; i++) {
|
|
412
|
+
if (this.isTrivialAlwaysValidSubschema(value[i])) {
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
default:
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private hasRequiredDefaults(schema: Record<string, any>): boolean {
|
|
425
|
+
const properties = schema.properties;
|
|
426
|
+
if (!this.isPlainObject(properties)) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const keys = Object.keys(properties);
|
|
431
|
+
for (let i = 0; i < keys.length; i++) {
|
|
432
|
+
const subSchema = properties[keys[i]];
|
|
433
|
+
if (this.isPlainObject(subSchema) && "default" in subSchema) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private isDefaultTypeValidator(type: string, validator: TypeFunction): boolean {
|
|
442
|
+
return (Types as Record<string, TypeFunction | false>)[type] === validator;
|
|
443
|
+
}
|
|
444
|
+
|
|
143
445
|
private compileSchema(schema: Partial<CompiledSchema> | any): CompiledSchema {
|
|
144
|
-
if (!
|
|
446
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
145
447
|
if (schema === true) {
|
|
146
|
-
schema = {
|
|
147
|
-
anyOf: [{}]
|
|
148
|
-
};
|
|
448
|
+
schema = { anyOf: [{}] }; // Always valid
|
|
149
449
|
} else if (schema === false) {
|
|
150
|
-
schema = {
|
|
151
|
-
oneOf: []
|
|
152
|
-
};
|
|
450
|
+
schema = { oneOf: [] }; // Always invalid
|
|
153
451
|
} else {
|
|
154
|
-
schema = {
|
|
155
|
-
oneOf: [schema]
|
|
156
|
-
};
|
|
452
|
+
schema = { oneOf: [schema] };
|
|
157
453
|
}
|
|
158
454
|
}
|
|
159
455
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
456
|
+
schema = this.normalizeSchemaForCompile(schema);
|
|
457
|
+
|
|
458
|
+
const compiledSchema: CompiledSchema = deepCloneUnfreeze(
|
|
459
|
+
schema
|
|
460
|
+
) as CompiledSchema;
|
|
163
461
|
|
|
164
|
-
let
|
|
462
|
+
let schemaHasRef = false;
|
|
463
|
+
|
|
464
|
+
if (typeof schema.$id === "string") {
|
|
465
|
+
this.idRegistry.set(schema.$id, compiledSchema);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if ("$ref" in schema) {
|
|
469
|
+
schemaHasRef = true;
|
|
470
|
+
const refValidator = this.getKeyword("$ref");
|
|
471
|
+
if (refValidator) {
|
|
472
|
+
const defineError = getDefinedErrorFunctionForKey(
|
|
473
|
+
"$ref",
|
|
474
|
+
schema["$ref"],
|
|
475
|
+
this.failFast
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
479
|
+
"Validate_Reference",
|
|
480
|
+
(data) =>
|
|
481
|
+
(refValidator as KeywordFunction)(
|
|
482
|
+
compiledSchema,
|
|
483
|
+
data,
|
|
484
|
+
defineError,
|
|
485
|
+
this
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.markSchemaHasRef(compiledSchema);
|
|
491
|
+
return compiledSchema;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const validators: ValidatorItem[] = [];
|
|
495
|
+
const activeNames: string[] = [];
|
|
165
496
|
|
|
166
497
|
if ("type" in schema) {
|
|
498
|
+
const defineTypeError = getDefinedErrorFunctionForKey(
|
|
499
|
+
"type",
|
|
500
|
+
schema,
|
|
501
|
+
this.failFast
|
|
502
|
+
);
|
|
167
503
|
const types = Array.isArray(schema.type)
|
|
168
504
|
? schema.type
|
|
169
|
-
: schema.type.split(",").map((t) => t.trim());
|
|
505
|
+
: schema.type.split(",").map((t: string) => t.trim());
|
|
506
|
+
|
|
507
|
+
const typeFunctions: TypeFunction[] = [];
|
|
508
|
+
const typeNames: string[] = [];
|
|
509
|
+
const defaultTypeNames: string[] = [];
|
|
510
|
+
let allTypesDefault = true;
|
|
170
511
|
|
|
171
512
|
for (const type of types) {
|
|
172
513
|
const validator = this.getType(type);
|
|
173
514
|
if (validator) {
|
|
174
|
-
|
|
175
|
-
|
|
515
|
+
typeFunctions.push(validator);
|
|
516
|
+
typeNames.push(validator.name);
|
|
517
|
+
if (this.isDefaultTypeValidator(type, validator)) {
|
|
518
|
+
defaultTypeNames.push(type);
|
|
519
|
+
} else {
|
|
520
|
+
allTypesDefault = false;
|
|
521
|
+
}
|
|
176
522
|
}
|
|
177
523
|
}
|
|
178
524
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
525
|
+
if (typeFunctions.length === 0) {
|
|
526
|
+
throw getDefinedErrorFunctionForKey(
|
|
527
|
+
"type",
|
|
528
|
+
schema,
|
|
529
|
+
this.failFast
|
|
530
|
+
)("Invalid type for schema", { data: schema.type });
|
|
183
531
|
}
|
|
184
532
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
533
|
+
let combinedTypeValidator: ValidateFunction;
|
|
534
|
+
let typeMethodName = "";
|
|
535
|
+
|
|
536
|
+
if (typeFunctions.length === 1 && allTypesDefault) {
|
|
537
|
+
const singleTypeName = defaultTypeNames[0];
|
|
538
|
+
typeMethodName = singleTypeName;
|
|
539
|
+
|
|
540
|
+
switch (singleTypeName) {
|
|
541
|
+
case "object":
|
|
542
|
+
combinedTypeValidator = (data) => {
|
|
543
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
544
|
+
return defineTypeError("Invalid type", { data });
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
break;
|
|
548
|
+
case "array":
|
|
549
|
+
combinedTypeValidator = (data) => {
|
|
550
|
+
if (!Array.isArray(data)) {
|
|
551
|
+
return defineTypeError("Invalid type", { data });
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
break;
|
|
555
|
+
case "string":
|
|
556
|
+
combinedTypeValidator = (data) => {
|
|
557
|
+
if (typeof data !== "string") {
|
|
558
|
+
return defineTypeError("Invalid type", { data });
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
break;
|
|
562
|
+
case "number":
|
|
563
|
+
combinedTypeValidator = (data) => {
|
|
564
|
+
if (typeof data !== "number") {
|
|
565
|
+
return defineTypeError("Invalid type", { data });
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
break;
|
|
569
|
+
case "integer":
|
|
570
|
+
combinedTypeValidator = (data) => {
|
|
571
|
+
if (typeof data !== "number" || !Number.isInteger(data)) {
|
|
572
|
+
return defineTypeError("Invalid type", { data });
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
break;
|
|
576
|
+
case "boolean":
|
|
577
|
+
combinedTypeValidator = (data) => {
|
|
578
|
+
if (typeof data !== "boolean") {
|
|
579
|
+
return defineTypeError("Invalid type", { data });
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
break;
|
|
583
|
+
case "null":
|
|
584
|
+
combinedTypeValidator = (data) => {
|
|
585
|
+
if (data !== null) {
|
|
586
|
+
return defineTypeError("Invalid type", { data });
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
break;
|
|
590
|
+
default: {
|
|
591
|
+
const singleTypeFn = typeFunctions[0];
|
|
592
|
+
combinedTypeValidator = (data) => {
|
|
593
|
+
if (!singleTypeFn(data)) {
|
|
594
|
+
return defineTypeError("Invalid type", { data });
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} else if (typeFunctions.length > 1 && allTypesDefault) {
|
|
600
|
+
typeMethodName = defaultTypeNames.join("_OR_");
|
|
601
|
+
|
|
602
|
+
const allowsObject = defaultTypeNames.includes("object");
|
|
603
|
+
const allowsArray = defaultTypeNames.includes("array");
|
|
604
|
+
const allowsString = defaultTypeNames.includes("string");
|
|
605
|
+
const allowsNumber = defaultTypeNames.includes("number");
|
|
606
|
+
const allowsInteger = defaultTypeNames.includes("integer");
|
|
607
|
+
const allowsBoolean = defaultTypeNames.includes("boolean");
|
|
608
|
+
const allowsNull = defaultTypeNames.includes("null");
|
|
609
|
+
|
|
610
|
+
combinedTypeValidator = (data) => {
|
|
611
|
+
const dataType = typeof data;
|
|
612
|
+
|
|
613
|
+
if (dataType === "number") {
|
|
614
|
+
if (allowsNumber || (allowsInteger && Number.isInteger(data))) {
|
|
615
|
+
return;
|
|
192
616
|
}
|
|
617
|
+
|
|
618
|
+
return defineTypeError("Invalid type", { data });
|
|
193
619
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
620
|
+
|
|
621
|
+
if (dataType === "string") {
|
|
622
|
+
if (allowsString) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return defineTypeError("Invalid type", { data });
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (dataType === "boolean") {
|
|
630
|
+
if (allowsBoolean) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return defineTypeError("Invalid type", { data });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (dataType === "object") {
|
|
638
|
+
if (data === null) {
|
|
639
|
+
if (allowsNull) {
|
|
201
640
|
return;
|
|
202
641
|
}
|
|
642
|
+
|
|
643
|
+
return defineTypeError("Invalid type", { data });
|
|
203
644
|
}
|
|
645
|
+
|
|
646
|
+
if (Array.isArray(data)) {
|
|
647
|
+
if (allowsArray) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return defineTypeError("Invalid type", { data });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (allowsObject) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
204
658
|
return defineTypeError("Invalid type", { data });
|
|
205
659
|
}
|
|
206
|
-
|
|
660
|
+
|
|
661
|
+
return defineTypeError("Invalid type", { data });
|
|
662
|
+
};
|
|
663
|
+
} else if (typeFunctions.length === 1) {
|
|
664
|
+
typeMethodName = typeNames[0];
|
|
665
|
+
const singleTypeFn = typeFunctions[0];
|
|
666
|
+
combinedTypeValidator = (data) => {
|
|
667
|
+
if (!singleTypeFn(data)) {
|
|
668
|
+
return defineTypeError("Invalid type", { data });
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
} else {
|
|
672
|
+
typeMethodName = typeNames.join("_OR_");
|
|
673
|
+
combinedTypeValidator = (data) => {
|
|
674
|
+
for (let i = 0; i < typeFunctions.length; i++) {
|
|
675
|
+
if (typeFunctions[i](data)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return defineTypeError("Invalid type", { data });
|
|
680
|
+
};
|
|
207
681
|
}
|
|
682
|
+
|
|
683
|
+
validators.push({
|
|
684
|
+
name: typeMethodName,
|
|
685
|
+
validate: getNamedFunction(typeMethodName, combinedTypeValidator)
|
|
686
|
+
});
|
|
687
|
+
activeNames.push(typeMethodName);
|
|
208
688
|
}
|
|
209
689
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
690
|
+
const { type, $id, $ref, $validate, required, ...otherKeys } = schema; // Exclude handled keys
|
|
691
|
+
|
|
692
|
+
// In here we create an array of keys putting the require keyword last
|
|
693
|
+
// This is to ensure required properties are checked after defaults are applied
|
|
694
|
+
const keyOrder = required
|
|
695
|
+
? this.hasRequiredDefaults(schema)
|
|
696
|
+
? [...Object.keys(otherKeys), "required"]
|
|
697
|
+
: ["required", ...Object.keys(otherKeys)]
|
|
698
|
+
: Object.keys(otherKeys);
|
|
699
|
+
|
|
700
|
+
for (const key of keyOrder) {
|
|
701
|
+
const keywordFn = this.getKeyword(key);
|
|
702
|
+
|
|
703
|
+
if (!keywordFn) {
|
|
213
704
|
continue;
|
|
214
705
|
}
|
|
215
706
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const defineError = getDefinedErrorFunctionForKey(key, schema[key]);
|
|
219
|
-
if (compiledSchema.$validate) {
|
|
220
|
-
const prevValidator = compiledSchema.$validate;
|
|
221
|
-
methodName += `_AND_${keywordValidator.name}`;
|
|
222
|
-
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
223
|
-
methodName,
|
|
224
|
-
(data) => {
|
|
225
|
-
const error = prevValidator(data);
|
|
226
|
-
if (error) {
|
|
227
|
-
return error;
|
|
228
|
-
}
|
|
229
|
-
return (keywordValidator as KeywordFunction)(
|
|
230
|
-
compiledSchema,
|
|
231
|
-
data,
|
|
232
|
-
defineError,
|
|
233
|
-
this
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
);
|
|
237
|
-
} else {
|
|
238
|
-
methodName = keywordValidator.name;
|
|
239
|
-
compiledSchema.$validate = getNamedFunction<ValidateFunction>(
|
|
240
|
-
methodName,
|
|
241
|
-
(data) =>
|
|
242
|
-
(keywordValidator as KeywordFunction)(
|
|
243
|
-
compiledSchema,
|
|
244
|
-
data,
|
|
245
|
-
defineError,
|
|
246
|
-
this
|
|
247
|
-
)
|
|
248
|
-
);
|
|
249
|
-
}
|
|
707
|
+
if (this.shouldSkipKeyword(schema, key)) {
|
|
708
|
+
continue;
|
|
250
709
|
}
|
|
251
710
|
|
|
252
|
-
|
|
253
|
-
|
|
711
|
+
const defineError = getDefinedErrorFunctionForKey(
|
|
712
|
+
key,
|
|
713
|
+
schema[key],
|
|
714
|
+
this.failFast
|
|
715
|
+
);
|
|
716
|
+
const fnName = keywordFn.name || key;
|
|
717
|
+
|
|
718
|
+
validators.push({
|
|
719
|
+
name: fnName,
|
|
720
|
+
validate: getNamedFunction<ValidateFunction>(fnName, (data) =>
|
|
721
|
+
(keywordFn as KeywordFunction)(compiledSchema, data, defineError, this)
|
|
722
|
+
)
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
activeNames.push(fnName);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const literalKeywords = ["enum", "const", "default", "examples"];
|
|
729
|
+
for (const key of keyOrder) {
|
|
730
|
+
if (literalKeywords.includes(key)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (
|
|
735
|
+
schema[key] &&
|
|
736
|
+
typeof schema[key] === "object" &&
|
|
737
|
+
!Array.isArray(schema[key])
|
|
738
|
+
) {
|
|
254
739
|
if (key === "properties") {
|
|
255
740
|
for (const subKey of Object.keys(schema[key])) {
|
|
256
|
-
|
|
741
|
+
const compiledSubSchema = this.compileSchema(
|
|
257
742
|
schema[key][subKey]
|
|
258
743
|
);
|
|
744
|
+
|
|
745
|
+
if ((compiledSubSchema as any)._hasRef === true) {
|
|
746
|
+
schemaHasRef = true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
compiledSchema[key][subKey] = compiledSubSchema;
|
|
259
750
|
}
|
|
260
751
|
continue;
|
|
261
752
|
}
|
|
262
|
-
|
|
753
|
+
const compiledSubSchema = this.compileSchema(schema[key]);
|
|
754
|
+
if ((compiledSubSchema as any)._hasRef === true) {
|
|
755
|
+
schemaHasRef = true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
compiledSchema[key] = compiledSubSchema;
|
|
263
759
|
continue;
|
|
264
760
|
}
|
|
265
761
|
|
|
266
762
|
if (Array.isArray(schema[key])) {
|
|
267
|
-
|
|
268
|
-
this.isSchemaLike(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
763
|
+
for (let i = 0; i < schema[key].length; i++) {
|
|
764
|
+
if (this.isSchemaLike(schema[key][i])) {
|
|
765
|
+
const compiledSubSchema = this.compileSchema(schema[key][i]);
|
|
766
|
+
if ((compiledSubSchema as any)._hasRef === true) {
|
|
767
|
+
schemaHasRef = true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
compiledSchema[key][i] = compiledSubSchema;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
272
773
|
continue;
|
|
273
774
|
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (schemaHasRef) {
|
|
778
|
+
this.markSchemaHasRef(compiledSchema);
|
|
779
|
+
}
|
|
274
780
|
|
|
275
|
-
|
|
781
|
+
if (validators.length === 0) {
|
|
782
|
+
return compiledSchema;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (validators.length === 1) {
|
|
786
|
+
const v = validators[0];
|
|
787
|
+
compiledSchema.$validate = getNamedFunction(v.name, v.validate);
|
|
788
|
+
} else {
|
|
789
|
+
const compositeName = "Validate_" + activeNames.join("_AND_");
|
|
790
|
+
|
|
791
|
+
const masterValidator: ValidateFunction = (data) => {
|
|
792
|
+
for (let i = 0; i < validators.length; i++) {
|
|
793
|
+
const v = validators[i];
|
|
794
|
+
const error = v.validate(data);
|
|
795
|
+
if (error) {
|
|
796
|
+
return error;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
compiledSchema.$validate = getNamedFunction(
|
|
803
|
+
compositeName,
|
|
804
|
+
masterValidator
|
|
805
|
+
);
|
|
276
806
|
}
|
|
277
807
|
|
|
278
808
|
return compiledSchema as CompiledSchema;
|
|
279
809
|
}
|
|
280
810
|
|
|
281
811
|
isSchemaLike(subSchema: any): boolean {
|
|
282
|
-
if (
|
|
812
|
+
if (
|
|
813
|
+
subSchema &&
|
|
814
|
+
typeof subSchema === "object" &&
|
|
815
|
+
!Array.isArray(subSchema)
|
|
816
|
+
) {
|
|
283
817
|
if ("type" in subSchema) {
|
|
284
818
|
return true;
|
|
285
819
|
}
|
|
@@ -292,4 +826,65 @@ export class SchemaShield {
|
|
|
292
826
|
}
|
|
293
827
|
return false;
|
|
294
828
|
}
|
|
829
|
+
|
|
830
|
+
private linkReferences(root: CompiledSchema) {
|
|
831
|
+
const stack: any[] = [root];
|
|
832
|
+
|
|
833
|
+
while (stack.length > 0) {
|
|
834
|
+
const node = stack.pop();
|
|
835
|
+
|
|
836
|
+
if (!node || typeof node !== "object") continue;
|
|
837
|
+
|
|
838
|
+
if (
|
|
839
|
+
typeof node.$ref === "string" &&
|
|
840
|
+
typeof node.$validate === "function" &&
|
|
841
|
+
node.$validate.name === "Validate_Reference"
|
|
842
|
+
) {
|
|
843
|
+
const refPath = node.$ref as string;
|
|
844
|
+
|
|
845
|
+
let target: any = this.getSchemaRef(refPath);
|
|
846
|
+
if (typeof target === "undefined") {
|
|
847
|
+
target = this.getSchemaById(refPath);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (typeof target === "boolean") {
|
|
851
|
+
if (target === true) {
|
|
852
|
+
node.$validate = getNamedFunction("Validate_Ref_True", () => {});
|
|
853
|
+
} else {
|
|
854
|
+
const defineError = getDefinedErrorFunctionForKey(
|
|
855
|
+
"$ref",
|
|
856
|
+
node as any,
|
|
857
|
+
this.failFast
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
node.$validate = getNamedFunction(
|
|
861
|
+
"Validate_Ref_False",
|
|
862
|
+
(_data: any) => defineError("Value is not valid")
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (target && typeof target.$validate === "function") {
|
|
869
|
+
node.$validate = target.$validate;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
for (const key in node) {
|
|
874
|
+
const value = node[key];
|
|
875
|
+
if (!value) continue;
|
|
876
|
+
|
|
877
|
+
if (Array.isArray(value)) {
|
|
878
|
+
for (let i = 0; i < value.length; i++) {
|
|
879
|
+
const v = value[i];
|
|
880
|
+
if (v && typeof v === "object") {
|
|
881
|
+
stack.push(v);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
} else if (typeof value === "object") {
|
|
885
|
+
stack.push(value);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
295
890
|
}
|