@serum-enterprises/schema 2.0.1-beta.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/LICENSE +21 -0
- package/README.md +65 -0
- package/build/Schema.js +667 -0
- package/package.json +33 -0
- package/src/Schema.ts +887 -0
- package/tsconfig.json +30 -0
- package/types/Schema.d.ts +99 -0
package/src/Schema.ts
ADDED
@@ -0,0 +1,887 @@
|
|
1
|
+
import * as JSON from '@serum-enterprises/json';
|
2
|
+
import { Result } from '@serum-enterprises/result';
|
3
|
+
|
4
|
+
export class SchemaError extends Error { }
|
5
|
+
export class ValidationError extends Error { }
|
6
|
+
|
7
|
+
export abstract class Schema {
|
8
|
+
static get Any() {
|
9
|
+
return new AnyValidator();
|
10
|
+
}
|
11
|
+
|
12
|
+
static get Boolean() {
|
13
|
+
return new BooleanValidator();
|
14
|
+
}
|
15
|
+
|
16
|
+
static get Number() {
|
17
|
+
return new NumberValidator();
|
18
|
+
}
|
19
|
+
|
20
|
+
static get String() {
|
21
|
+
return new StringValidator();
|
22
|
+
}
|
23
|
+
|
24
|
+
static get Array() {
|
25
|
+
return new ArrayValidator();
|
26
|
+
}
|
27
|
+
|
28
|
+
static get Object() {
|
29
|
+
return new ObjectValidator();
|
30
|
+
}
|
31
|
+
|
32
|
+
static get Or() {
|
33
|
+
return new OrValidator();
|
34
|
+
}
|
35
|
+
|
36
|
+
static get And() {
|
37
|
+
return new AndValidator();
|
38
|
+
}
|
39
|
+
|
40
|
+
static get defaultRegistry(): Map<string, typeof Schema> {
|
41
|
+
return new Map<string, typeof Schema>([
|
42
|
+
['any', AnyValidator],
|
43
|
+
['boolean', BooleanValidator],
|
44
|
+
['number', NumberValidator],
|
45
|
+
['string', StringValidator],
|
46
|
+
['array', ArrayValidator],
|
47
|
+
['object', ObjectValidator],
|
48
|
+
['or', OrValidator],
|
49
|
+
['and', AndValidator]
|
50
|
+
]);
|
51
|
+
}
|
52
|
+
|
53
|
+
static fromJSON(schema: JSON.JSON, path: string = "schema", registry: Map<string, typeof Schema> = Schema.defaultRegistry): Result<Schema, SchemaError> {
|
54
|
+
if (!JSON.isObject(schema))
|
55
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
56
|
+
|
57
|
+
if (!JSON.isString(schema['type']))
|
58
|
+
return Result.Err(new SchemaError(`Expected ${path}.type to be a String`));
|
59
|
+
|
60
|
+
if (!registry.has(schema['type']))
|
61
|
+
return Result.Err(new SchemaError(`Expected ${path}.type to be a registered Validator`));
|
62
|
+
|
63
|
+
return (registry.get(schema['type']) as typeof Schema).fromJSON(schema, path, registry);
|
64
|
+
}
|
65
|
+
|
66
|
+
abstract validate(data: unknown, path?: string): Result<void, ValidationError>;
|
67
|
+
|
68
|
+
abstract toJSON(): JSON.Object;
|
69
|
+
}
|
70
|
+
|
71
|
+
class AnyValidator extends Schema {
|
72
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema"): Result<AnyValidator, SchemaError> {
|
73
|
+
const validator = new AnyValidator();
|
74
|
+
|
75
|
+
if (!JSON.isObject(schema))
|
76
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
77
|
+
|
78
|
+
return Result.Ok(validator);
|
79
|
+
}
|
80
|
+
|
81
|
+
constructor() {
|
82
|
+
super();
|
83
|
+
}
|
84
|
+
|
85
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
86
|
+
if (!JSON.isJSON(data))
|
87
|
+
return Result.Err(new ValidationError(`Expected ${path} to be JSON`));
|
88
|
+
|
89
|
+
return Result.Ok(void 0);
|
90
|
+
}
|
91
|
+
|
92
|
+
toJSON(): JSON.Object {
|
93
|
+
return {
|
94
|
+
type: 'any'
|
95
|
+
};
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
class BooleanValidator extends Schema {
|
100
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema"): Result<BooleanValidator, SchemaError> {
|
101
|
+
const validator = new BooleanValidator();
|
102
|
+
|
103
|
+
if (!JSON.isObject(schema))
|
104
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
105
|
+
|
106
|
+
if ('nullable' in schema) {
|
107
|
+
if (!JSON.isBoolean(schema['nullable']))
|
108
|
+
return Result.Err(new SchemaError(`Expected ${path}.nullable to be a Boolean`));
|
109
|
+
|
110
|
+
validator.nullable(schema['nullable']);
|
111
|
+
}
|
112
|
+
|
113
|
+
if ('equals' in schema) {
|
114
|
+
if (!JSON.isBoolean(schema['equals']))
|
115
|
+
return Result.Err(new SchemaError(`Expected ${path}.equals to be a Boolean`));
|
116
|
+
|
117
|
+
validator.equals(schema['equals']);
|
118
|
+
}
|
119
|
+
|
120
|
+
return Result.Ok(validator);
|
121
|
+
}
|
122
|
+
|
123
|
+
#nullable: { flag: boolean };
|
124
|
+
#equals: { flag: boolean; value: JSON.Boolean; };
|
125
|
+
|
126
|
+
constructor() {
|
127
|
+
super();
|
128
|
+
this.#nullable = { flag: false };
|
129
|
+
this.#equals = { flag: false, value: false };
|
130
|
+
}
|
131
|
+
|
132
|
+
nullable(flag: boolean): this {
|
133
|
+
this.#nullable = { flag };
|
134
|
+
|
135
|
+
return this;
|
136
|
+
}
|
137
|
+
|
138
|
+
equals(value: boolean): this {
|
139
|
+
this.#equals = { flag: true, value };
|
140
|
+
|
141
|
+
return this;
|
142
|
+
}
|
143
|
+
|
144
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
145
|
+
if (JSON.isBoolean(data)) {
|
146
|
+
if (this.#equals.flag && this.#equals.value !== data)
|
147
|
+
return Result.Err(new ValidationError(`Expected ${path} to be ${this.#equals.value}${this.#nullable.flag ? '' : ' or Null'}`));
|
148
|
+
|
149
|
+
return Result.Ok(void 0);
|
150
|
+
}
|
151
|
+
|
152
|
+
if (this.#nullable.flag && JSON.isNull(data))
|
153
|
+
return Result.Ok(void 0);
|
154
|
+
|
155
|
+
return Result.Err(new ValidationError(`Expected ${path} to be a Boolean${this.#nullable.flag ? '' : ' or Null'}`));
|
156
|
+
}
|
157
|
+
|
158
|
+
toJSON(): JSON.Object {
|
159
|
+
const schema: JSON.Object = {
|
160
|
+
type: 'boolean'
|
161
|
+
};
|
162
|
+
|
163
|
+
if (this.#nullable.flag)
|
164
|
+
schema['nullable'] = this.#nullable.flag;
|
165
|
+
|
166
|
+
if (this.#equals.flag)
|
167
|
+
schema['equals'] = this.#equals.value;
|
168
|
+
|
169
|
+
return schema;
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
class NumberValidator extends Schema {
|
174
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema"): Result<NumberValidator, SchemaError> {
|
175
|
+
const validator = new NumberValidator();
|
176
|
+
|
177
|
+
if (!JSON.isObject(schema))
|
178
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
179
|
+
|
180
|
+
if ('nullable' in schema) {
|
181
|
+
if (!JSON.isBoolean(schema['nullable']))
|
182
|
+
return Result.Err(new SchemaError(`Expected ${path}.nullable to be a Boolean`));
|
183
|
+
|
184
|
+
validator.nullable(schema['nullable']);
|
185
|
+
}
|
186
|
+
|
187
|
+
if ('equals' in schema) {
|
188
|
+
if (!JSON.isNumber(schema['equals']))
|
189
|
+
return Result.Err(new SchemaError(`Expected ${path}.equals to be a Number`));
|
190
|
+
|
191
|
+
validator.equals(schema['equals']);
|
192
|
+
}
|
193
|
+
|
194
|
+
if ('integer' in schema) {
|
195
|
+
if (!JSON.isBoolean(schema['integer']))
|
196
|
+
return Result.Err(new SchemaError(`Expected ${path}.integer to be a Boolean`));
|
197
|
+
|
198
|
+
validator.integer(schema['integer']);
|
199
|
+
}
|
200
|
+
|
201
|
+
if ('min' in schema) {
|
202
|
+
if (!JSON.isNumber(schema['min']))
|
203
|
+
return Result.Err(new SchemaError(`Expected ${path}.min to be a Number`));
|
204
|
+
|
205
|
+
validator.min(schema['min']);
|
206
|
+
}
|
207
|
+
|
208
|
+
if ('max' in schema) {
|
209
|
+
if (!JSON.isNumber(schema['max']))
|
210
|
+
return Result.Err(new SchemaError(`Expected ${path}.max to be a Number`));
|
211
|
+
|
212
|
+
validator.max(schema['max']);
|
213
|
+
}
|
214
|
+
|
215
|
+
return Result.Ok(validator);
|
216
|
+
}
|
217
|
+
|
218
|
+
#nullable: { flag: boolean };
|
219
|
+
#equals: { flag: boolean; value: JSON.Number; };
|
220
|
+
#integer: { flag: boolean; };
|
221
|
+
#min: { flag: boolean; value: JSON.Number; };
|
222
|
+
#max: { flag: boolean; value: JSON.Number; };
|
223
|
+
|
224
|
+
constructor() {
|
225
|
+
super();
|
226
|
+
this.#nullable = { flag: false };
|
227
|
+
this.#equals = { flag: false, value: 0 };
|
228
|
+
this.#integer = { flag: false };
|
229
|
+
this.#min = { flag: false, value: 0 };
|
230
|
+
this.#max = { flag: false, value: 0 };
|
231
|
+
}
|
232
|
+
|
233
|
+
nullable(flag: boolean): this {
|
234
|
+
this.#nullable = { flag };
|
235
|
+
|
236
|
+
return this;
|
237
|
+
}
|
238
|
+
|
239
|
+
equals(value: number): this {
|
240
|
+
this.#equals = { flag: true, value };
|
241
|
+
|
242
|
+
return this;
|
243
|
+
}
|
244
|
+
|
245
|
+
integer(flag: boolean = true): this {
|
246
|
+
this.#integer = { flag };
|
247
|
+
|
248
|
+
return this;
|
249
|
+
}
|
250
|
+
|
251
|
+
min(value: number): this {
|
252
|
+
this.#min = { flag: true, value };
|
253
|
+
|
254
|
+
return this;
|
255
|
+
}
|
256
|
+
|
257
|
+
max(value: number): this {
|
258
|
+
this.#max = { flag: true, value };
|
259
|
+
|
260
|
+
return this;
|
261
|
+
}
|
262
|
+
|
263
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
264
|
+
if (JSON.isNumber(data)) {
|
265
|
+
if (this.#equals.flag && this.#equals.value !== data)
|
266
|
+
return Result.Err(new ValidationError(`Expected ${path} to be ${this.#equals.value}`));
|
267
|
+
|
268
|
+
if (this.#integer.flag && !Number.isInteger(data))
|
269
|
+
return Result.Err(new ValidationError(`Expected ${path} to be an Integer`));
|
270
|
+
|
271
|
+
if (this.#min.flag && this.#min.value > data)
|
272
|
+
return Result.Err(new ValidationError(`Expected ${path} to be at least ${this.#min.value}`));
|
273
|
+
|
274
|
+
if (this.#max.flag && this.#max.value < data)
|
275
|
+
return Result.Err(new ValidationError(`Expected ${path} to be at most ${this.#max.value}`));
|
276
|
+
|
277
|
+
return Result.Ok(void 0);
|
278
|
+
}
|
279
|
+
|
280
|
+
if (this.#nullable.flag && JSON.isNull(data))
|
281
|
+
return Result.Ok(void 0);
|
282
|
+
|
283
|
+
return Result.Err(new ValidationError(`Expected ${path} to be a Number${this.#nullable.flag ? '' : ' or Null'}`));
|
284
|
+
}
|
285
|
+
|
286
|
+
toJSON(): JSON.Object {
|
287
|
+
const schema: JSON.Object = {
|
288
|
+
type: 'number'
|
289
|
+
};
|
290
|
+
|
291
|
+
if (this.#nullable.flag)
|
292
|
+
schema['nullable'] = this.#nullable.flag;
|
293
|
+
|
294
|
+
if (this.#equals.flag)
|
295
|
+
schema['equals'] = this.#equals.value;
|
296
|
+
|
297
|
+
if (this.#integer.flag)
|
298
|
+
schema['integer'] = this.#integer.flag;
|
299
|
+
|
300
|
+
if (this.#min.flag)
|
301
|
+
schema['min'] = this.#min.value;
|
302
|
+
|
303
|
+
if (this.#max.flag)
|
304
|
+
schema['max'] = this.#max.value;
|
305
|
+
|
306
|
+
return schema;
|
307
|
+
}
|
308
|
+
}
|
309
|
+
|
310
|
+
class StringValidator extends Schema {
|
311
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema"): Result<StringValidator, SchemaError> {
|
312
|
+
const validator = new StringValidator();
|
313
|
+
|
314
|
+
if (!JSON.isObject(schema))
|
315
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
316
|
+
|
317
|
+
if ('nullable' in schema) {
|
318
|
+
if (!JSON.isBoolean(schema['nullable']))
|
319
|
+
return Result.Err(new SchemaError(`Expected ${path}.nullable to be a Boolean`));
|
320
|
+
|
321
|
+
validator.nullable(schema['nullable']);
|
322
|
+
}
|
323
|
+
|
324
|
+
if ('equals' in schema) {
|
325
|
+
if (!JSON.isString(schema['equals']))
|
326
|
+
return Result.Err(new SchemaError(`Expected ${path}.equals to be a String`));
|
327
|
+
|
328
|
+
validator.equals(schema['equals']);
|
329
|
+
}
|
330
|
+
|
331
|
+
if ('min' in schema) {
|
332
|
+
if (!JSON.isNumber(schema['min']))
|
333
|
+
return Result.Err(new SchemaError(`Expected ${path}.min to be a Number`));
|
334
|
+
|
335
|
+
validator.min(schema['min']);
|
336
|
+
}
|
337
|
+
|
338
|
+
if ('max' in schema) {
|
339
|
+
if (!JSON.isNumber(schema['max']))
|
340
|
+
return Result.Err(new SchemaError(`Expected ${path}.max to be a Number`));
|
341
|
+
|
342
|
+
validator.max(schema['max']);
|
343
|
+
}
|
344
|
+
|
345
|
+
return Result.Ok(validator);
|
346
|
+
}
|
347
|
+
|
348
|
+
#nullable: { flag: boolean };
|
349
|
+
#equals: { flag: boolean; value: JSON.String; };
|
350
|
+
#min: { flag: boolean; value: JSON.Number; };
|
351
|
+
#max: { flag: boolean; value: JSON.Number; };
|
352
|
+
|
353
|
+
constructor() {
|
354
|
+
super();
|
355
|
+
this.#nullable = { flag: false };
|
356
|
+
this.#equals = { flag: false, value: "" };
|
357
|
+
this.#min = { flag: false, value: 0 };
|
358
|
+
this.#max = { flag: false, value: 0 };
|
359
|
+
}
|
360
|
+
|
361
|
+
nullable(flag: boolean): this {
|
362
|
+
this.#nullable = { flag };
|
363
|
+
|
364
|
+
return this;
|
365
|
+
}
|
366
|
+
|
367
|
+
equals(value: string): this {
|
368
|
+
this.#equals = { flag: true, value };
|
369
|
+
|
370
|
+
return this;
|
371
|
+
}
|
372
|
+
|
373
|
+
min(value: number): this {
|
374
|
+
this.#min = { flag: true, value };
|
375
|
+
|
376
|
+
return this;
|
377
|
+
}
|
378
|
+
|
379
|
+
max(value: number): this {
|
380
|
+
this.#max = { flag: true, value };
|
381
|
+
|
382
|
+
return this;
|
383
|
+
}
|
384
|
+
|
385
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
386
|
+
if (JSON.isString(data)) {
|
387
|
+
if (this.#equals.flag && this.#equals.value !== data)
|
388
|
+
return Result.Err(new ValidationError(`Expected ${path} to be ${this.#equals.value}`));
|
389
|
+
|
390
|
+
if (this.#min.flag && this.#min.value > data.length)
|
391
|
+
return Result.Err(new ValidationError(`Expected ${path} to be at least ${this.#min.value} characters long`));
|
392
|
+
|
393
|
+
if (this.#max.flag && this.#max.value < data.length)
|
394
|
+
return Result.Err(new ValidationError(`Expected ${path} to be at most ${this.#max.value} characters long`));
|
395
|
+
|
396
|
+
return Result.Ok(void 0);
|
397
|
+
}
|
398
|
+
|
399
|
+
if (this.#nullable.flag && JSON.isNull(data))
|
400
|
+
return Result.Ok(void 0);
|
401
|
+
|
402
|
+
return Result.Err(new ValidationError(`Expected ${path} to be a String${this.#nullable.flag ? '' : ' or Null'}`));
|
403
|
+
|
404
|
+
}
|
405
|
+
|
406
|
+
toJSON(): JSON.Object {
|
407
|
+
const schema: JSON.Object = {
|
408
|
+
type: 'string'
|
409
|
+
};
|
410
|
+
|
411
|
+
if (this.#nullable.flag)
|
412
|
+
schema['nullable'] = this.#nullable.flag;
|
413
|
+
|
414
|
+
if (this.#equals.flag)
|
415
|
+
schema['equals'] = this.#equals.value;
|
416
|
+
|
417
|
+
if (this.#min.flag)
|
418
|
+
schema['min'] = this.#min.value;
|
419
|
+
|
420
|
+
if (this.#max.flag)
|
421
|
+
schema['max'] = this.#max.value;
|
422
|
+
|
423
|
+
return schema;
|
424
|
+
}
|
425
|
+
}
|
426
|
+
|
427
|
+
class ArrayValidator extends Schema {
|
428
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema", registry: Map<string, typeof Schema> = Schema.defaultRegistry): Result<ArrayValidator, SchemaError> {
|
429
|
+
const validator = new ArrayValidator();
|
430
|
+
|
431
|
+
if (!JSON.isObject(schema))
|
432
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
433
|
+
|
434
|
+
if ('nullable' in schema) {
|
435
|
+
if (!JSON.isBoolean(schema['nullable']))
|
436
|
+
return Result.Err(new SchemaError(`Expected ${path}.nullable to be a Boolean`));
|
437
|
+
|
438
|
+
validator.nullable(schema['nullable']);
|
439
|
+
}
|
440
|
+
|
441
|
+
if ('min' in schema) {
|
442
|
+
if (!JSON.isNumber(schema['min']))
|
443
|
+
return Result.Err(new SchemaError(`Expected ${path}.min to be a Number`));
|
444
|
+
|
445
|
+
validator.min(schema['min']);
|
446
|
+
}
|
447
|
+
|
448
|
+
if ('max' in schema) {
|
449
|
+
if (!JSON.isNumber(schema['max']))
|
450
|
+
return Result.Err(new SchemaError(`Expected ${path}.max to be a Number`));
|
451
|
+
|
452
|
+
validator.max(schema['max']);
|
453
|
+
}
|
454
|
+
|
455
|
+
if ('every' in schema) {
|
456
|
+
if (!JSON.isObject(schema['every']))
|
457
|
+
return Result.Err(new SchemaError(`Expected ${path}.every to be an Object`));
|
458
|
+
|
459
|
+
const itemValidator: Result<Schema, SchemaError> = Schema.fromJSON(schema['every'], `${path}.every`, registry);
|
460
|
+
|
461
|
+
if (itemValidator.isOk())
|
462
|
+
validator.every(itemValidator.value);
|
463
|
+
|
464
|
+
if (itemValidator.isErr())
|
465
|
+
return itemValidator;
|
466
|
+
}
|
467
|
+
|
468
|
+
if ('tuple' in schema) {
|
469
|
+
if (!JSON.isArray(schema['tuple']))
|
470
|
+
return Result.Err(new SchemaError(`Expected ${path}.tuple to be an Array`));
|
471
|
+
|
472
|
+
const validatorResults: [{ [key: string]: Schema }, { [key: string]: SchemaError }] = [{}, {}];
|
473
|
+
|
474
|
+
for (let i = 0; i < schema['tuple'].length; i++) {
|
475
|
+
const value = schema['tuple'][i] as JSON.JSON;
|
476
|
+
|
477
|
+
if (!JSON.isObject(value))
|
478
|
+
validatorResults[1][i] = new SchemaError(`Expected ${path}.tuple[${i}] to be an Object`);
|
479
|
+
|
480
|
+
Schema.fromJSON(value, `${path}.tuple[${i}]`, registry).match(
|
481
|
+
value => { validatorResults[0][i] = value },
|
482
|
+
error => { validatorResults[1][i] = error }
|
483
|
+
);
|
484
|
+
}
|
485
|
+
|
486
|
+
if (Object.keys(validatorResults[1]).length > 0)
|
487
|
+
return Result.Err(new SchemaError(`Expected ${path}.tuple to be an Array where every Element is a valid Schema`, { cause: validatorResults[1] }));
|
488
|
+
|
489
|
+
validator.tuple(Object.entries(validatorResults[0]).map(([_, value]) => value));
|
490
|
+
}
|
491
|
+
|
492
|
+
return Result.Ok(validator);
|
493
|
+
}
|
494
|
+
|
495
|
+
#nullable: { flag: boolean };
|
496
|
+
#every: { flag: boolean; value: Schema; };
|
497
|
+
#min: { flag: boolean; value: number; };
|
498
|
+
#max: { flag: boolean; value: number; };
|
499
|
+
#tuple: { flag: boolean; value: Schema[]; };
|
500
|
+
|
501
|
+
constructor() {
|
502
|
+
super();
|
503
|
+
this.#nullable = { flag: false };
|
504
|
+
this.#every = { flag: false, value: new AnyValidator() };
|
505
|
+
this.#min = { flag: false, value: 0 };
|
506
|
+
this.#max = { flag: false, value: 0 };
|
507
|
+
this.#tuple = { flag: false, value: [] };
|
508
|
+
}
|
509
|
+
|
510
|
+
nullable(flag: boolean = true): this {
|
511
|
+
this.#nullable = { flag };
|
512
|
+
|
513
|
+
return this;
|
514
|
+
}
|
515
|
+
|
516
|
+
min(value: number): this {
|
517
|
+
this.#min = { flag: true, value };
|
518
|
+
|
519
|
+
return this;
|
520
|
+
}
|
521
|
+
|
522
|
+
max(value: number): this {
|
523
|
+
this.#max = { flag: true, value };
|
524
|
+
|
525
|
+
return this;
|
526
|
+
}
|
527
|
+
|
528
|
+
every(validator: Schema): this {
|
529
|
+
this.#every = { flag: true, value: validator };
|
530
|
+
|
531
|
+
return this;
|
532
|
+
}
|
533
|
+
|
534
|
+
tuple(validators: Schema[]): this {
|
535
|
+
this.#tuple = { flag: true, value: validators };
|
536
|
+
|
537
|
+
return this;
|
538
|
+
}
|
539
|
+
|
540
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
541
|
+
if (JSON.isArray(data)) {
|
542
|
+
if (this.#min.flag && this.#min.value > data.length)
|
543
|
+
return Result.Err(new ValidationError(`Expected ${path} to be at least ${this.#min.value} Elements long`));
|
544
|
+
|
545
|
+
if (this.#max.flag && this.#max.value < data.length)
|
546
|
+
return Result.Err(new ValidationError(`Expected ${path} to be at most ${this.#max.value} Elements long`));
|
547
|
+
|
548
|
+
if (this.#every.flag) {
|
549
|
+
const errors: ValidationError[] = data
|
550
|
+
.map((value, index) => this.#every.value.validate(value, `${path}[${index}]`))
|
551
|
+
.filter(value => value.isErr())
|
552
|
+
.map(value => value.error);
|
553
|
+
|
554
|
+
if (errors.length > 0)
|
555
|
+
return Result.Err(new ValidationError(`Expected ${path} to be an Array where every Element matches the item Validator`, { cause: errors }));
|
556
|
+
}
|
557
|
+
|
558
|
+
if (this.#tuple.flag) {
|
559
|
+
if (this.#tuple.value.length !== data.length)
|
560
|
+
return Result.Err(new ValidationError(`Expected ${path} to have exactly ${this.#tuple.value.length} Elements`));
|
561
|
+
|
562
|
+
const errors: ValidationError[] = this.#tuple.value
|
563
|
+
.map((validator, index) => validator.validate(data[index], `${path}[${index}]`))
|
564
|
+
.filter(value => value.isErr())
|
565
|
+
.map(value => value.error);
|
566
|
+
|
567
|
+
if (errors.length > 0)
|
568
|
+
return Result.Err(new ValidationError(`Expected ${path} to be a Tuple where every Element matches its respective Validator`, { cause: errors }));
|
569
|
+
}
|
570
|
+
|
571
|
+
return Result.Ok(void 0);
|
572
|
+
}
|
573
|
+
|
574
|
+
if (this.#nullable.flag && JSON.isNull(data))
|
575
|
+
return Result.Ok(void 0);
|
576
|
+
|
577
|
+
return Result.Err(new ValidationError(`Expected ${path} to be an Array${this.#nullable.flag ? '' : ' or Null'}`));
|
578
|
+
}
|
579
|
+
|
580
|
+
toJSON(): JSON.Object {
|
581
|
+
const schema: JSON.Object = {
|
582
|
+
type: 'array'
|
583
|
+
};
|
584
|
+
|
585
|
+
if (this.#nullable.flag)
|
586
|
+
schema['nullable'] = this.#nullable.flag;
|
587
|
+
|
588
|
+
if (this.#min.flag)
|
589
|
+
schema['min'] = this.#min.value;
|
590
|
+
|
591
|
+
if (this.#max.flag)
|
592
|
+
schema['max'] = this.#max.value;
|
593
|
+
|
594
|
+
if (this.#every.flag)
|
595
|
+
schema['every'] = this.#every.value.toJSON();
|
596
|
+
|
597
|
+
if (this.#tuple.flag)
|
598
|
+
schema['tuple'] = this.#tuple.value.map(validator => validator.toJSON());
|
599
|
+
|
600
|
+
return schema;
|
601
|
+
}
|
602
|
+
}
|
603
|
+
|
604
|
+
class ObjectValidator extends Schema {
|
605
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema", registry: Map<string, typeof Schema> = Schema.defaultRegistry): Result<ObjectValidator, SchemaError> {
|
606
|
+
const validator = new ObjectValidator();
|
607
|
+
|
608
|
+
if (!JSON.isObject(schema))
|
609
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
610
|
+
|
611
|
+
if ('nullable' in schema) {
|
612
|
+
if (!JSON.isBoolean(schema['nullable']))
|
613
|
+
return Result.Err(new SchemaError(`Expected ${path}.nullable to be a Boolean`));
|
614
|
+
|
615
|
+
validator.nullable(schema['nullable']);
|
616
|
+
}
|
617
|
+
|
618
|
+
if ('schema' in schema) {
|
619
|
+
if (!JSON.isObject(schema['schema']))
|
620
|
+
return Result.Err(new SchemaError(`Expected ${path}.schema to be an Object`));
|
621
|
+
|
622
|
+
const validatorResults: [{ [key: string]: Schema }, { [key: string]: SchemaError }] = [{}, {}];
|
623
|
+
|
624
|
+
for (let [key, value] of Object.entries(schema['schema'])) {
|
625
|
+
if (!JSON.isObject(value))
|
626
|
+
validatorResults[1][key] = new SchemaError(`Expected ${path}.schema.${key} to be an Object`);
|
627
|
+
|
628
|
+
Schema.fromJSON(value, path, registry).match(
|
629
|
+
value => { validatorResults[0][key] = value },
|
630
|
+
error => { validatorResults[1][key] = error }
|
631
|
+
);
|
632
|
+
}
|
633
|
+
|
634
|
+
if (Object.keys(validatorResults[1]).length > 0)
|
635
|
+
return Result.Err(new SchemaError(`Expected ${path}.schema to be an Object where every Property is a valid Schema`, { cause: validatorResults[1] }));
|
636
|
+
|
637
|
+
validator.schema(validatorResults[0]);
|
638
|
+
}
|
639
|
+
|
640
|
+
if ('inclusive' in schema) {
|
641
|
+
if (!JSON.isBoolean(schema['inclusive']))
|
642
|
+
return Result.Err(new SchemaError(`Expected ${path}.inclusive to be a Boolean`));
|
643
|
+
|
644
|
+
validator.inclusive(schema['inclusive']);
|
645
|
+
}
|
646
|
+
|
647
|
+
return Result.Ok(validator);
|
648
|
+
}
|
649
|
+
|
650
|
+
#nullable: { flag: boolean };
|
651
|
+
#schema: { flag: boolean; value: { [key: string]: Schema; } };
|
652
|
+
#inclusive: { flag: boolean; };
|
653
|
+
|
654
|
+
constructor() {
|
655
|
+
super();
|
656
|
+
this.#nullable = { flag: false };
|
657
|
+
this.#schema = { flag: false, value: {} };
|
658
|
+
this.#inclusive = { flag: false };
|
659
|
+
}
|
660
|
+
|
661
|
+
nullable(flag: boolean = true): this {
|
662
|
+
this.#nullable = { flag };
|
663
|
+
|
664
|
+
return this;
|
665
|
+
}
|
666
|
+
|
667
|
+
inclusive(flag: boolean = true): this {
|
668
|
+
this.#inclusive = { flag };
|
669
|
+
|
670
|
+
return this;
|
671
|
+
}
|
672
|
+
|
673
|
+
schema(value: { [key: string]: Schema }, flag: boolean = true): this {
|
674
|
+
this.#schema = { flag, value };
|
675
|
+
|
676
|
+
return this;
|
677
|
+
}
|
678
|
+
|
679
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
680
|
+
if (JSON.isObject(data)) {
|
681
|
+
if (this.#schema.flag) {
|
682
|
+
const errors: { [key: string]: ValidationError } = {};
|
683
|
+
|
684
|
+
for (let [key, validator] of Object.entries(this.#schema.value)) {
|
685
|
+
const result = validator.validate(data[key], `${path}.${key}`);
|
686
|
+
|
687
|
+
if (result.isErr())
|
688
|
+
errors[key] = result.error;
|
689
|
+
}
|
690
|
+
|
691
|
+
if (Object.keys(errors).length > 0)
|
692
|
+
return Result.Err(new ValidationError(`Expected ${path} to be an Object where every Property matches the Schema Constraint`, { cause: errors }));
|
693
|
+
|
694
|
+
// If inclusive is not set and the Object has more Properties than the Schema, return an Error
|
695
|
+
if (!this.#inclusive.flag && Object.keys(data).length !== Object.keys(this.#schema.value).length) {
|
696
|
+
const schemaKeys = Object.keys(this.#schema.value);
|
697
|
+
const errors = Object.keys(data).filter(key => !schemaKeys.includes(key))
|
698
|
+
.map(key => new ValidationError(`Expected ${path}.${key} not to exist on this Schema`));
|
699
|
+
|
700
|
+
return Result.Err(new ValidationError(`Expected ${path} to have only the Properties defined in the Schema`, { cause: errors }));
|
701
|
+
}
|
702
|
+
}
|
703
|
+
|
704
|
+
return Result.Ok(void 0);
|
705
|
+
}
|
706
|
+
|
707
|
+
if (this.#nullable.flag && JSON.isNull(data))
|
708
|
+
return Result.Ok(void 0);
|
709
|
+
|
710
|
+
return Result.Err(new ValidationError(`Expected ${path} to be an Object${this.#nullable.flag ? '' : ' or Null'}`));
|
711
|
+
}
|
712
|
+
|
713
|
+
toJSON(): JSON.Object {
|
714
|
+
const schema: JSON.Object = {
|
715
|
+
type: 'object'
|
716
|
+
};
|
717
|
+
|
718
|
+
if (this.#nullable.flag)
|
719
|
+
schema['nullable'] = this.#nullable.flag;
|
720
|
+
|
721
|
+
if (this.#schema.flag)
|
722
|
+
schema['schema'] = Object.fromEntries(Object.entries(this.#schema.value).map(([key, value]) => [key, value.toJSON()]));
|
723
|
+
|
724
|
+
if (this.#inclusive.flag)
|
725
|
+
schema['inclusive'] = this.#inclusive.flag;
|
726
|
+
|
727
|
+
return schema;
|
728
|
+
}
|
729
|
+
}
|
730
|
+
|
731
|
+
class OrValidator extends Schema {
|
732
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema", registry: Map<string, typeof Schema> = Schema.defaultRegistry): Result<OrValidator, SchemaError> {
|
733
|
+
const validator = new OrValidator();
|
734
|
+
|
735
|
+
if (!JSON.isObject(schema))
|
736
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
737
|
+
|
738
|
+
if ('oneOf' in schema) {
|
739
|
+
if (!JSON.isArray(schema['oneOf']))
|
740
|
+
return Result.Err(new SchemaError(`Expected ${path}.oneOf to be an Array`));
|
741
|
+
|
742
|
+
const validatorResults: [Schema[], SchemaError[]] = schema['oneOf']
|
743
|
+
.map(value => Schema.fromJSON(value, `${path}.oneOf`, registry))
|
744
|
+
.reduce<[Schema[], SchemaError[]]>((acc, result) => {
|
745
|
+
return result.match(
|
746
|
+
value => [[...acc[0], value], acc[1]],
|
747
|
+
error => [acc[0], [...acc[1], error]]
|
748
|
+
);
|
749
|
+
}, [[], []]);
|
750
|
+
|
751
|
+
if (validatorResults[1].length > 0)
|
752
|
+
return Result.Err(new SchemaError(`Expected ${path}.oneOf to be an Array where every Element is a valid Schema`, { cause: validatorResults[1] }));
|
753
|
+
|
754
|
+
validator.oneOf(validatorResults[0]);
|
755
|
+
}
|
756
|
+
|
757
|
+
return Result.Ok(validator);
|
758
|
+
}
|
759
|
+
|
760
|
+
#oneOf: { flag: boolean, value: Schema[] };
|
761
|
+
|
762
|
+
constructor() {
|
763
|
+
super();
|
764
|
+
this.#oneOf = { flag: false, value: [] };
|
765
|
+
}
|
766
|
+
|
767
|
+
oneOf(validators: Schema[]): this {
|
768
|
+
this.#oneOf = { flag: true, value: validators };
|
769
|
+
|
770
|
+
return this;
|
771
|
+
}
|
772
|
+
|
773
|
+
validate(data: unknown, path: string = 'data'): Result<void, ValidationError> {
|
774
|
+
if (!JSON.isJSON(data))
|
775
|
+
return Result.Err(new ValidationError(`Expected ${path} to be JSON`));
|
776
|
+
|
777
|
+
if (this.#oneOf.flag) {
|
778
|
+
let errors: ValidationError[] = this.#oneOf.value
|
779
|
+
.map(validator => validator.validate(data, path))
|
780
|
+
.reduce<ValidationError[]>((acc, value) => {
|
781
|
+
return value.match(
|
782
|
+
_ => acc,
|
783
|
+
error => [...acc, error]
|
784
|
+
);
|
785
|
+
}, []);
|
786
|
+
|
787
|
+
if (errors.length === this.#oneOf.value.length)
|
788
|
+
return Result.Err(new ValidationError(`Expected ${path} to match at least one of the OneOf Validators`, { cause: errors }));
|
789
|
+
|
790
|
+
return Result.Ok(void 0);
|
791
|
+
}
|
792
|
+
|
793
|
+
return Result.Ok(void 0);
|
794
|
+
}
|
795
|
+
|
796
|
+
toJSON(): JSON.Object {
|
797
|
+
const schema: JSON.Object = {
|
798
|
+
type: 'or'
|
799
|
+
};
|
800
|
+
|
801
|
+
if (this.#oneOf.flag)
|
802
|
+
schema['oneOf'] = this.#oneOf.value.map(validator => validator.toJSON());
|
803
|
+
|
804
|
+
return schema;
|
805
|
+
}
|
806
|
+
}
|
807
|
+
|
808
|
+
class AndValidator extends Schema {
|
809
|
+
static override fromJSON(schema: JSON.JSON, path: string = "schema", registry: Map<string, typeof Schema> = Schema.defaultRegistry): Result<AndValidator, SchemaError> {
|
810
|
+
const validator = new AndValidator();
|
811
|
+
|
812
|
+
if (!JSON.isObject(schema))
|
813
|
+
return Result.Err(new SchemaError(`Expected ${path} to be an Object`));
|
814
|
+
|
815
|
+
if ('allOf' in schema) {
|
816
|
+
if (!JSON.isArray(schema['allOf']))
|
817
|
+
return Result.Err(new SchemaError(`Expected ${path}.allOf to be an Array`));
|
818
|
+
|
819
|
+
const validatorResults: [Schema[], SchemaError[]] = [[], []];
|
820
|
+
|
821
|
+
for (let i = 0; i < schema['allOf'].length; i++) {
|
822
|
+
const value = schema['allOf'][i] as JSON.JSON;
|
823
|
+
|
824
|
+
if (!JSON.isObject(value))
|
825
|
+
validatorResults[1][i] = new SchemaError(`Expected ${path}.allOf[${i}] to be an Object`);
|
826
|
+
else
|
827
|
+
Schema.fromJSON(value, `${path}.allOf[${i}]`, registry).match(
|
828
|
+
value => { validatorResults[0].push(value) },
|
829
|
+
error => { validatorResults[1].push(error) }
|
830
|
+
);
|
831
|
+
}
|
832
|
+
|
833
|
+
if (validatorResults[1].length > 0)
|
834
|
+
return Result.Err(new SchemaError(`Expected ${path}.allOf to be an Array where every Element is a valid Schema`, { cause: validatorResults[1] }));
|
835
|
+
|
836
|
+
validator.allOf(Object.entries(validatorResults[0]).map(([_, value]) => value));
|
837
|
+
}
|
838
|
+
|
839
|
+
return Result.Ok(validator);
|
840
|
+
}
|
841
|
+
|
842
|
+
#allOf: { flag: boolean, value: Schema[] };
|
843
|
+
|
844
|
+
constructor() {
|
845
|
+
super();
|
846
|
+
this.#allOf = { flag: false, value: [] };
|
847
|
+
}
|
848
|
+
|
849
|
+
allOf(validators: Schema[]): this {
|
850
|
+
this.#allOf = { flag: true, value: validators };
|
851
|
+
|
852
|
+
return this;
|
853
|
+
}
|
854
|
+
|
855
|
+
validate(data: unknown, path?: string): Result<void, ValidationError> {
|
856
|
+
if (!JSON.isJSON(data))
|
857
|
+
return Result.Err(new ValidationError(`Expected ${path} to be JSON`));
|
858
|
+
|
859
|
+
if (this.#allOf.flag) {
|
860
|
+
const errors: ValidationError[] = this.#allOf.value
|
861
|
+
.map(validator => validator.validate(data, path))
|
862
|
+
.reduce<ValidationError[]>((acc, value) => {
|
863
|
+
return value.match(
|
864
|
+
_ => acc,
|
865
|
+
error => [...acc, error]
|
866
|
+
);
|
867
|
+
}, []);
|
868
|
+
|
869
|
+
if (errors.length > 0)
|
870
|
+
return Result.Err(new ValidationError(`Expected ${path} to match all of the AllOf Validators`, { cause: errors }));
|
871
|
+
}
|
872
|
+
|
873
|
+
return Result.Ok(void 0);
|
874
|
+
|
875
|
+
}
|
876
|
+
|
877
|
+
toJSON(): JSON.Object {
|
878
|
+
const schema: JSON.Object = {
|
879
|
+
type: 'and'
|
880
|
+
};
|
881
|
+
|
882
|
+
if (this.#allOf.flag)
|
883
|
+
schema['allOf'] = this.#allOf.value.map(validator => validator.toJSON());
|
884
|
+
|
885
|
+
return schema;
|
886
|
+
}
|
887
|
+
}
|