json-2-joi 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE.txt +203 -203
  2. package/README.md +361 -361
  3. package/index.js +14 -14
  4. package/lib/resolver.js +699 -696
  5. package/package.json +53 -53
package/lib/resolver.js CHANGED
@@ -1,696 +1,699 @@
1
- const Joi = require('joi');
2
- const Hoek = require('@hapi/hoek');
3
- const Bourne = require('@hapi/bourne');
4
- const debugCreator = require('debug');
5
-
6
- const debug = debugCreator('enjoi');
7
-
8
- debugCreator.formatters.f = function (f) {
9
- return f();
10
- }
11
-
12
- const schemaSchema = Joi.alternatives(Joi.object().unknown(true), Joi.string()).required();
13
- const extensionSchema = Joi.alternatives().try(Joi.object().unknown(true), Joi.function())
14
-
15
- const optionsSchema = Joi.object({
16
- subSchemas: Joi.object().unknown(true).default({}),
17
- extensions: Joi.array().items(extensionSchema).default([]),
18
- refineType: Joi.func().allow(null),
19
- refineSchema: Joi.func().allow(null),
20
- refineDescription: Joi.func().allow(null),
21
- allowNull: Joi.boolean().default(false),
22
- forbidArrayNull: Joi.boolean().default(true),
23
- strictArrayRequired: Joi.boolean().default(false),
24
- strictRequired: Joi.boolean().default(false),
25
- strictEnum: Joi.boolean().default(true),
26
- enableEnum: Joi.boolean().default(true),
27
- customizedNullValues: Joi.array().items(Joi.any()).default([null]).min(1),
28
- joiOptions: Joi.object().unknown(true).default({})
29
- });
30
-
31
- const overridableOptions = {
32
- refineType: Joi.func().allow(null),
33
- refineSchema: Joi.func().allow(null),
34
- refineDescription: Joi.func().allow(null),
35
- allowNull: Joi.boolean(),
36
- forbidArrayNull: Joi.boolean(),
37
- strictArrayRequired: Joi.boolean(),
38
- strictRequired: Joi.boolean(),
39
- noDefaults: Joi.boolean(),
40
- strictEnum: Joi.boolean(),
41
- enableEnum: Joi.boolean(),
42
- customizedNullValues: Joi.array().items(Joi.any()).min(1),
43
- };
44
- const overridableOptionKeys = Object.keys(overridableOptions);
45
- const overrideOptionsSchema = Joi.object(overridableOptions);
46
-
47
- function mergeOverridableOptions(target, ...sources) {
48
- sources.forEach((src) => {
49
- overridableOptionKeys.forEach((key) => {
50
- if (typeof src[key] !== 'undefined') {
51
- target[key] = src[key];
52
- }
53
- })
54
- });
55
- return target;
56
- }
57
-
58
- function isNumber(value) {
59
- return typeof value === 'number';
60
- }
61
-
62
- function isObject(value) {
63
- return value !== null && typeof value === 'object';
64
- }
65
-
66
- function isNotEmptyObject(value) {
67
- return isObject(value) && Object.keys(value).length > 0;
68
- }
69
-
70
- function allowNullIfNeeded(schema, { allowNull = false, forbidArrayNull = true, customizedNullValues = [null] }) {
71
- if (allowNull) {
72
- const definition = schema.describe();
73
- if (definition.flags && definition.flags.only && definition.allow) {
74
- // Already has enum restriction
75
- return schema;
76
- }
77
- if (forbidArrayNull && schema.type === 'array') {
78
- return schema;
79
- }
80
- return schema.allow(...customizedNullValues);
81
- }
82
- return schema;
83
- }
84
-
85
- function addEnumRestriction(joiSchema, enumList, { allowNull = false, strictEnum = true, enableEnum = true }) {
86
- if (!enableEnum || !Array.isArray(enumList) || enumList.length === 0) {
87
- return joiSchema;
88
- }
89
-
90
- const valids = [].concat(enumList);
91
- if (allowNull && !strictEnum) {
92
- if (joiSchema.type === 'string') {
93
- valids.push('');
94
- }
95
- valids.push(null);
96
- }
97
- return joiSchema.valid(...valids);
98
- }
99
-
100
- function normalizedId(id) {
101
- return id.replace(/[^a-zA-Z0-9]/g, '_');
102
- }
103
-
104
- function getJoiInstance(joiOptions, extensions, joiInstance) {
105
- if (joiInstance) {
106
- return joiInstance;
107
- }
108
- if (isNotEmptyObject(joiOptions)) {
109
- joiInstance = Joi.defaults((schema) => schema.options(joiOptions));
110
- } else {
111
- joiInstance = Joi;
112
- }
113
-
114
- return joiInstance.extend(
115
- {
116
- type: 'object',
117
- base: joiInstance.object(),
118
- coerce: {
119
- from: 'string',
120
- method(value) {
121
- if (typeof value !== 'string' || (value[0] !== '{' && !/^\s*\{/.test(value))) {
122
- return;
123
- }
124
- try {
125
- return { value: Bourne.parse(value) };
126
- } catch (ignoreErr) { /* eslint-disable-line no-unused-vars */
127
- // Purposefully ignoring error
128
- }
129
- }
130
- }
131
- },
132
- {
133
- type: 'array',
134
- base: joiInstance.array(),
135
- coerce: {
136
- from: 'string',
137
- method(value) {
138
- if (typeof value !== 'string' || (value[0] !== '[' && !/^\s*\[/.test(value))) {
139
- return;
140
- }
141
- try {
142
- return { value: Bourne.parse(value) };
143
- } catch (ignoreErr) { /* eslint-disable-line no-unused-vars */
144
- // Purposefully ignoring error
145
- }
146
- }
147
- }
148
- },
149
- ...extensions
150
- );
151
- }
152
-
153
- const mapToObj = m => {
154
- return Array.from(m).reduce((obj, [key, value]) => {
155
- obj[key] = Joi.isSchema(value) ? value.describe() : value;
156
- return obj;
157
- }, {});
158
- };
159
-
160
- class SchemaResolver {
161
- constructor(options, joiInstance) {
162
- const validatedResult = optionsSchema.validate(options);
163
-
164
- Hoek.assert(!validatedResult.error, validatedResult.error);
165
-
166
- const { subSchemas = {}, extensions = [], joiOptions } = validatedResult.value;
167
-
168
- this.resolveOptions = mergeOverridableOptions({}, validatedResult.value);
169
- this.subSchemas = subSchemas;
170
- this.joiOptions = joiOptions;
171
- this.joiSharedSchemas = new Map();
172
- this.resolveOptions.noDefaults = this.joiOptions.noDefaults;
173
-
174
- this.joi = getJoiInstance(joiOptions, extensions, joiInstance);
175
-
176
- for (let sharedSchemaId in this.subSchemas) {
177
- const sharedSchema = this.subSchemas[sharedSchemaId];
178
- if (typeof sharedSchema === 'object' && !sharedSchema.$id) {
179
- // shorthand schema type does not need to set $id
180
- sharedSchema.$id = sharedSchemaId;
181
- }
182
- this.joiSharedSchemas.set(sharedSchemaId, this.resolve(sharedSchema, this.resolveOptions));
183
- }
184
-
185
- const itr = this.joiSharedSchemas.keys();
186
- this.initialSharedSchemaIds = [];
187
- for (const id of itr) {
188
- this.initialSharedSchemaIds.push(id);
189
- }
190
- }
191
-
192
- resolveShareSchemas(schema, resolveOptions, sharedProperties = '$defs', baseUri = '') {
193
- const localShared = schema[sharedProperties]
194
- if (localShared) {
195
- for (let sharedSchemaId in localShared) {
196
- this.joiSharedSchemas.set(
197
- `${baseUri}#/${sharedProperties}/${sharedSchemaId}`,
198
- this.resolve(localShared[sharedSchemaId], resolveOptions, baseUri)
199
- );
200
- }
201
- }
202
- }
203
-
204
- convert(schema, options = {}) {
205
- const validateSchema = schemaSchema.validate(schema);
206
- const { value: overrideOptions, error: overrideOptionsError } = overrideOptionsSchema.validate(options);
207
-
208
- Hoek.assert(!validateSchema.error, validateSchema.error);
209
- Hoek.assert(!overrideOptionsError, overrideOptionsError);
210
-
211
- const schemaId = schema.$id || '';
212
- this.subSchemas[schemaId] = schema;
213
-
214
- const joiSchema = this.resolve(schema, mergeOverridableOptions({}, this.resolveOptions, overrideOptions));
215
-
216
- // Cleanup all shared schema registered other than the initial shared ones
217
- delete this.subSchemas[schemaId];
218
- const itr = this.joiSharedSchemas.keys();
219
- for (const id of itr) {
220
- if (!this.initialSharedSchemaIds.includes(id)) {
221
- delete this.subSchemas[id];
222
- this.joiSharedSchemas.delete(id);
223
- }
224
- }
225
- return joiSchema;
226
- }
227
-
228
- resolve(schema, resolveOptions = {}, baseUri = (schema.$id || ''), isPropertyRequired = false) {
229
- if (this.joi.isSchema(schema)) {
230
- return schema;
231
- }
232
-
233
- const schemaId = schema.$id;
234
- let resolvedSchema;
235
-
236
- if (schemaId) {
237
- baseUri = schemaId;
238
- if (!this.subSchemas[schemaId]) {
239
- this.subSchemas[schemaId] = schema;
240
- }
241
- }
242
-
243
- this.resolveShareSchemas(schema, resolveOptions, '$defs', baseUri);
244
- this.resolveShareSchemas(schema, resolveOptions, 'definitions', baseUri);
245
-
246
- if (typeof schema === 'string') {
247
- // If schema is itself a string, interpret it as a type
248
- resolvedSchema = this.resolveType({ type: schema }, resolveOptions, baseUri, isPropertyRequired);
249
- } else if (schema.$ref) {
250
- if (schema.$ref.trim() === '#') {
251
- return baseUri ? this.joi.link(`#${normalizedId(baseUri)}`) : this.joi.link('/');
252
- }
253
-
254
- resolvedSchema = this.resolve(this.resolveReference(schema.$ref, baseUri), resolveOptions, baseUri, isPropertyRequired);
255
- } else {
256
- const partialSchemas = [];
257
- if (schema.type) {
258
- partialSchemas.push(this.resolveType(schema, resolveOptions, baseUri, isPropertyRequired));
259
- } else if (schema.properties) {
260
- // if no type is specified, just properties
261
- partialSchemas.push(this.object(schema, resolveOptions, baseUri))
262
- } else if (schema.format) {
263
- // if no type is specified, just format
264
- partialSchemas.push(this.string(schema))
265
- } else if (schema.enum) {
266
- // If no type is specified, just enum
267
- partialSchemas.push(
268
- addEnumRestriction(this.joi.any().example(schema.enum[0]), schema.enum, resolveOptions)
269
- );
270
- } else if (schema.const) {
271
- // Constant value
272
- partialSchemas.push(this.getValueJoiType(schema.const).valid(schema.const));
273
- }
274
- if (schema.anyOf) {
275
- partialSchemas.push(this.resolveAnyOf(schema, resolveOptions, baseUri));
276
- }
277
- if (schema.allOf) {
278
- partialSchemas.push(this.resolveAllOf(schema, resolveOptions, baseUri));
279
- }
280
- if (schema.oneOf) {
281
- partialSchemas.push(this.resolveOneOf(schema, resolveOptions, baseUri));
282
- }
283
- if (schema.not) {
284
- partialSchemas.push(this.resolveNot(schema, resolveOptions, baseUri));
285
- }
286
- if (partialSchemas.length === 0) {
287
- //Fall through to whatever.
288
- console.warn('WARNING: schema missing a \'type\' or \'$ref\' or \'enum\': \n%s', JSON.stringify(schema, null, 2));
289
- //TODO: Handle better
290
- partialSchemas.push(this.joi.any());
291
- }
292
- resolvedSchema = partialSchemas.length === 1 ? partialSchemas[0] : this.joi.alternatives(partialSchemas).match('all');
293
- }
294
-
295
- if (resolveOptions.refineSchema) {
296
- resolvedSchema = resolveOptions.refineSchema(resolvedSchema, schema);
297
- }
298
-
299
- if (schema.default !== undefined && !resolveOptions.noDefaults) {
300
- resolvedSchema = resolvedSchema.default(schema.default)
301
- }
302
-
303
- resolvedSchema = this.copyMetaAnnotations(schema, resolvedSchema);
304
-
305
- if (schemaId && !(this.joiSharedSchemas.get(schemaId))) {
306
- resolvedSchema = resolvedSchema.id(normalizedId(schemaId));
307
- this.joiSharedSchemas.set(schemaId, resolvedSchema);
308
- }
309
-
310
- if (schema.$anchor) {
311
- this.joiSharedSchemas.set(`${baseUri}#${schema.$anchor}`, resolvedSchema);
312
- }
313
-
314
- if (isPropertyRequired) {
315
- resolvedSchema = resolvedSchema.required();
316
- if (resolveOptions.strictRequired) {
317
- resolvedSchema = resolvedSchema.invalid(null, '');
318
- }
319
- } else {
320
- resolvedSchema = allowNullIfNeeded(resolvedSchema, resolveOptions);
321
- }
322
-
323
- return resolvedSchema;
324
- }
325
-
326
- resolveReference(value, baseUri = '') {
327
- debug('----- Resolving schema reference: %s with baseUri: %s ----- ', value, baseUri);
328
- debug('Registered JOI schema for shared: %f', () => {
329
- return JSON.stringify(mapToObj(this.joiSharedSchemas), null, 2);
330
- });
331
- const hashIdx = value.indexOf('#');
332
- let refSchema;
333
-
334
- if (hashIdx === -1) {
335
- // Locate through $ref: 'refId'
336
- // debug('# Resolving reference through id: %s', value);
337
- refSchema = this.joiSharedSchemas.get(value);
338
- } else {
339
- const id = value.substring(0, hashIdx);
340
- const path = value.substring(hashIdx + 1);
341
-
342
- const canonicalURI = id ? value : `${baseUri}${value}`;
343
-
344
- if (canonicalURI.indexOf('/') === -1) {
345
- // Locate through $ref: [baseUri]#anchorSchema
346
- // debug('# Resolving reference through $anchor: %s', canonicalURI);
347
- refSchema = this.joiSharedSchemas.get(canonicalURI);
348
- } else if (canonicalURI.indexOf('/properties') === -1) {
349
- // Locate through $ref: [baseUri]#/$defs/shared
350
- // debug('# Resolving reference through direct canonicalURI: %s', canonicalURI);
351
- refSchema = this.joiSharedSchemas.get(canonicalURI);
352
- } else {
353
- // Locate through $ref: [baseUri]#/$defs/shared/properties/level1 or [baseUri]#/properties/product
354
- // Can only get raw schema and build Joi again because it might not completely built yet
355
- // debug('# Resolving reference through relative path: baseUri - %s; id - %s; path - %s', baseUri, id, path);
356
-
357
- const paths = path.split('/');
358
- refSchema = this.subSchemas[id || baseUri];
359
-
360
- while (paths.length > 0 && refSchema) {
361
- if (paths[0]) {
362
- refSchema = refSchema[paths[0]];
363
- }
364
- paths.splice(0, 1);
365
- }
366
- }
367
- }
368
-
369
- Hoek.assert(refSchema, `Can not find schema reference: ${value} with baseUri: ${baseUri}`);
370
-
371
- return refSchema;
372
- }
373
-
374
- resolveType(schema, resolveOptions, baseUri, isPropertyRequired = false) {
375
- let joischema;
376
-
377
- const joitype = (type, options, format) => {
378
- let joischema;
379
-
380
- if (options.refineType) {
381
- type = options.refineType(type, format);
382
- }
383
-
384
- switch (type) {
385
- case 'array':
386
- joischema = this.array(schema, options, baseUri, isPropertyRequired);
387
- break;
388
- case 'boolean':
389
- joischema = this.joi.boolean();
390
- break;
391
- case 'integer':
392
- case 'number':
393
- joischema = this.number(schema);
394
- break;
395
- case 'object':
396
- joischema = this.object(schema, options, baseUri);
397
- break;
398
- case 'string':
399
- joischema = this.string(schema);
400
- break;
401
- case 'null':
402
- joischema = this.joi.any().valid(...options.customizedNullValues);
403
- break;
404
- default:
405
- joischema = this.joi.types()[type];
406
- }
407
-
408
- Hoek.assert(joischema, 'Could not resolve type: ' + schema.type + '.');
409
-
410
- return joischema;
411
- }
412
-
413
- let nullIdx;
414
- if (Array.isArray(schema.type)) {
415
- nullIdx = schema.type.findIndex((type) => type === 'null');
416
- if (nullIdx > -1) {
417
- schema.type.splice(nullIdx, 1);
418
- }
419
- if (schema.type.length === 1) {
420
- schema.type = schema.type[0];
421
- } else if (schema.type.length === 0) {
422
- schema.type = 'null';
423
- }
424
- }
425
-
426
- if (Array.isArray(schema.type)) {
427
- const schemas = [];
428
-
429
- for (let i = 0; i < schema.type.length; i++) {
430
- schemas.push(joitype(schema.type[i], resolveOptions, schema.format));
431
- }
432
-
433
- joischema = this.joi.alternatives(schemas);
434
- } else {
435
- joischema = joitype(schema.type, resolveOptions, schema.format);
436
- }
437
- if (nullIdx > -1) {
438
- joischema = allowNullIfNeeded(joischema,
439
- { allowNull: true, customizedNullValues: resolveOptions.customizedNullValues });
440
- }
441
-
442
- const typeDefinitionMap = {
443
- title: 'label'
444
- };
445
- Object.keys(typeDefinitionMap).forEach(function (key) {
446
- if (schema[key] !== undefined) {
447
- joischema = joischema[typeDefinitionMap[key]](schema[key]);
448
- }
449
- });
450
-
451
- let desc = schema.description;
452
- if (resolveOptions.refineDescription) {
453
- desc = resolveOptions.refineDescription(schema);
454
- }
455
- if (desc) {
456
- joischema = joischema.description(desc);
457
- }
458
-
459
- let example;
460
- if (schema.examples && schema.examples.length > 0) {
461
- example = schema.examples[0];
462
- }
463
- if (!example && typeof schema.default !== 'undefined') {
464
- example = schema.default;
465
- }
466
- if (!example && schema.enum && schema.enum.length > 0) {
467
- example = schema.enum[0];
468
- }
469
- if (example) {
470
- joischema = joischema.type === 'array' && !Array.isArray(example) ? joischema.example([example]) : joischema.example(example);
471
- }
472
-
473
- return addEnumRestriction(joischema, schema.enum, resolveOptions);
474
- }
475
-
476
- copyMetaAnnotations(schema, joischema) {
477
- const metaDataNames = ['title', 'deprecated', 'readOnly', 'writeOnly'];
478
- const metas = {};
479
- for (const metaName of metaDataNames) {
480
- if (typeof schema[metaName] !== 'undefined') {
481
- metas[metaName] = schema[metaName];
482
- }
483
- }
484
-
485
- if (Object.keys(metas).length === 0) {
486
- return joischema;
487
- }
488
-
489
- return joischema.meta(metas);
490
- }
491
-
492
- resolveOneOf(schema, resolveOptions, baseUri) {
493
- Hoek.assert(Array.isArray(schema.oneOf), 'Expected oneOf to be an array.');
494
-
495
- return this.joi.alternatives(schema.oneOf.map(schema => this.resolve(schema, resolveOptions, baseUri))).match('one');
496
- }
497
-
498
- resolveAnyOf(schema, resolveOptions, baseUri) {
499
- Hoek.assert(Array.isArray(schema.anyOf), 'Expected anyOf to be an array.');
500
-
501
- return this.joi.alternatives(schema.anyOf.map(schema => this.resolve(schema, resolveOptions, baseUri))).match('any');
502
- }
503
-
504
- resolveAllOf(schema, resolveOptions, baseUri) {
505
- Hoek.assert(Array.isArray(schema.allOf), 'Expected allOf to be an array.');
506
-
507
- return this.joi.alternatives(schema.allOf.map(schema => this.resolve(schema, resolveOptions, baseUri))).match('all');
508
- }
509
-
510
- resolveNot(schema, resolveOptions, baseUri) {
511
- Hoek.assert(isObject(schema.not), 'Expected Not to be an object.');
512
-
513
- return this.joi.alternatives().conditional(
514
- '.',
515
- {
516
- not: this.resolve(schema.not, resolveOptions, baseUri),
517
- then: this.joi.any(),
518
- otherwise: this.joi.any().forbidden()
519
- }
520
- );
521
- }
522
-
523
- resolveProperties(jsonSchema, resolveOptions, baseUri) {
524
- const schemas = {};
525
-
526
- if (!isObject(jsonSchema.properties)) {
527
- return;
528
- }
529
-
530
- Object.keys(jsonSchema.properties).forEach((key) => {
531
- const property = jsonSchema.properties[key];
532
- const isPropertyRequired = Array.isArray(jsonSchema.required) && !!~jsonSchema.required.indexOf(key);
533
-
534
- const joischema = this.resolve(property, resolveOptions, baseUri, isPropertyRequired);
535
-
536
- schemas[key] = joischema;
537
- });
538
-
539
- return schemas;
540
- }
541
-
542
- object(schema, resolveOptions, baseUri) {
543
- let joischema = this.joi.object(this.resolveProperties(schema, resolveOptions, baseUri));
544
-
545
- if (isObject(schema.additionalProperties)) {
546
- joischema = joischema.pattern(/^/, this.resolve(schema.additionalProperties, resolveOptions, baseUri));
547
- } else if (typeof this.joiOptions.allowUnknown === 'undefined' || typeof schema.additionalProperties !== 'undefined') {
548
- joischema = joischema.unknown(schema.additionalProperties !== false);
549
- }
550
-
551
- isNumber(schema.minProperties) && (joischema = joischema.min(schema.minProperties));
552
- isNumber(schema.maxProperties) && (joischema = joischema.max(schema.maxProperties));
553
-
554
- return joischema;
555
- }
556
-
557
- resolveAsArray(value, resolveOptions, baseUri, isPropertyRequired = false) {
558
- return [].concat(value).map((v) => {
559
- const vSchema = this.resolve(v, resolveOptions, baseUri);
560
- if (resolveOptions.strictArrayRequired && isPropertyRequired) {
561
- return vSchema.invalid(...resolveOptions.customizedNullValues);
562
- }
563
- return allowNullIfNeeded(vSchema, resolveOptions)
564
- });
565
- }
566
-
567
- array(schema, resolveOptions, baseUri, isPropertyRequired = false) {
568
- let joischema = this.joi.array();
569
- let items;
570
-
571
- if (schema.items) {
572
- items = this.resolveAsArray(schema.items, resolveOptions, baseUri, isPropertyRequired);
573
-
574
- joischema = joischema.items(...items);
575
- } else if (schema.ordered) {
576
- items = this.resolveAsArray(schema.ordered, resolveOptions, baseUri, isPropertyRequired);
577
- joischema = joischema.ordered(...items);
578
- }
579
-
580
- if (items && schema.additionalItems === false) {
581
- joischema = joischema.max(items.length);
582
- }
583
-
584
- if (isPropertyRequired && resolveOptions.strictArrayRequired && !schema.minItems) {
585
- schema.minItems = 1;
586
- }
587
- isNumber(schema.minItems) && (joischema = joischema.min(schema.minItems));
588
- isNumber(schema.maxItems) && (joischema = joischema.max(schema.maxItems));
589
-
590
- if (schema.uniqueItems) {
591
- joischema = joischema.unique();
592
- }
593
- if (schema.contains) {
594
- joischema = joischema.has(this.resolve(schema.contains, resolveOptions, baseUri));
595
- }
596
-
597
- return joischema;
598
- }
599
-
600
- number(schema) {
601
- let joischema = this.joi.number();
602
-
603
- if (schema.type === 'integer') {
604
- joischema = joischema.integer();
605
- }
606
-
607
- isNumber(schema.minimum) && (joischema = joischema.min(schema.minimum));
608
- isNumber(schema.maximum) && (joischema = joischema.max(schema.maximum));
609
- isNumber(schema.exclusiveMinimum) && (joischema = joischema.greater(schema.exclusiveMinimum));
610
- isNumber(schema.exclusiveMaximum) && (joischema = joischema.less(schema.exclusiveMaximum));
611
- isNumber(schema.multipleOf) && schema.multipleOf !== 0 && (joischema = joischema.multiple(schema.multipleOf));
612
-
613
- return joischema;
614
- }
615
-
616
- string(schema) {
617
- let joischema = this.joi.string();
618
-
619
- const dateRegex = '(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])';
620
- const timeRegex = '([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(.[0-9]+)?(Z|(\\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))';
621
- const dateTimeRegex = dateRegex + 'T' + timeRegex;
622
-
623
- switch (schema.format) {
624
- case 'date':
625
- return joischema.regex(new RegExp('^' + dateRegex + '$', 'i'), 'JsonSchema date format');
626
- case 'time':
627
- return joischema.regex(new RegExp('^' + timeRegex + '$', 'i'), 'JsonSchema time format');
628
- case 'date-time':
629
- return joischema.regex(new RegExp('^' + dateTimeRegex + '$', 'i'), 'JsonSchema date-time format');
630
- case 'binary':
631
- joischema = this.binary(schema);
632
- break;
633
- case 'email':
634
- return joischema.email();
635
- case 'hostname':
636
- return joischema.hostname();
637
- case 'ipv4':
638
- return joischema.ip({
639
- version: ['ipv4']
640
- });
641
- case 'ipv6':
642
- return joischema.ip({
643
- version: ['ipv6']
644
- });
645
- case 'uri':
646
- return joischema.uri();
647
- case 'byte':
648
- joischema = joischema.base64();
649
- break;
650
- case 'uuid':
651
- return joischema.guid({ version: ['uuidv4'] });
652
- case 'guid':
653
- return joischema.guid();
654
- }
655
- return this.regularString(schema, joischema);
656
- }
657
-
658
- regularString(schema, joischema) {
659
- schema.pattern && (joischema = joischema.regex(new RegExp(schema.pattern)));
660
-
661
- if ((typeof schema.minLength === 'undefined' || schema.minLength === 0) && !schema.pattern && !schema.format && !schema.enum) {
662
- joischema = joischema.allow('');
663
- }
664
-
665
- isNumber(schema.minLength) && (joischema = joischema.min(schema.minLength));
666
- isNumber(schema.maxLength) && (joischema = joischema.max(schema.maxLength));
667
- return joischema;
668
- }
669
-
670
- binary(schema) {
671
- let joischema = this.joi.binary();
672
- isNumber(schema.minLength) && (joischema = joischema.min(schema.minLength));
673
- isNumber(schema.maxLength) && (joischema = joischema.max(schema.maxLength));
674
- return joischema;
675
- }
676
-
677
- getValueJoiType(value) {
678
- let joiType;
679
- switch (typeof value) {
680
- case 'string':
681
- joiType = this.joi.string();
682
- break;
683
- case 'number':
684
- joiType = this.joi.number();
685
- break;
686
- case 'boolean':
687
- joiType = this.joi.boolean();
688
- break;
689
- default:
690
- joiType = this.joi.any();
691
- }
692
- return joiType;
693
- }
694
- }
695
-
696
- module.exports = SchemaResolver;
1
+ const Joi = require('joi');
2
+ const Hoek = require('@hapi/hoek');
3
+ const Bourne = require('@hapi/bourne');
4
+ const debugCreator = require('debug');
5
+
6
+ const debug = debugCreator('enjoi');
7
+
8
+ debugCreator.formatters.f = function (f) {
9
+ return f();
10
+ }
11
+
12
+ const schemaSchema = Joi.alternatives(Joi.object().unknown(true), Joi.string()).required();
13
+ const extensionSchema = Joi.alternatives().try(Joi.object().unknown(true), Joi.function())
14
+
15
+ const optionsSchema = Joi.object({
16
+ subSchemas: Joi.object().unknown(true).default({}),
17
+ extensions: Joi.array().items(extensionSchema).default([]),
18
+ refineType: Joi.func().allow(null),
19
+ refineSchema: Joi.func().allow(null),
20
+ refineDescription: Joi.func().allow(null),
21
+ allowNull: Joi.boolean().default(false),
22
+ forbidArrayNull: Joi.boolean().default(true),
23
+ strictArrayRequired: Joi.boolean().default(false),
24
+ strictRequired: Joi.boolean().default(false),
25
+ strictEnum: Joi.boolean().default(true),
26
+ enableEnum: Joi.boolean().default(true),
27
+ customizedNullValues: Joi.array().items(Joi.any()).default([null]).min(1),
28
+ joiOptions: Joi.object().unknown(true).default({})
29
+ });
30
+
31
+ const overridableOptions = {
32
+ refineType: Joi.func().allow(null),
33
+ refineSchema: Joi.func().allow(null),
34
+ refineDescription: Joi.func().allow(null),
35
+ allowNull: Joi.boolean(),
36
+ forbidArrayNull: Joi.boolean(),
37
+ strictArrayRequired: Joi.boolean(),
38
+ strictRequired: Joi.boolean(),
39
+ noDefaults: Joi.boolean(),
40
+ strictEnum: Joi.boolean(),
41
+ enableEnum: Joi.boolean(),
42
+ customizedNullValues: Joi.array().items(Joi.any()).min(1),
43
+ };
44
+ const overridableOptionKeys = Object.keys(overridableOptions);
45
+ const overrideOptionsSchema = Joi.object(overridableOptions);
46
+
47
+ function mergeOverridableOptions(target, ...sources) {
48
+ sources.forEach((src) => {
49
+ overridableOptionKeys.forEach((key) => {
50
+ if (typeof src[key] !== 'undefined') {
51
+ target[key] = src[key];
52
+ }
53
+ })
54
+ });
55
+ return target;
56
+ }
57
+
58
+ function isNumber(value) {
59
+ return typeof value === 'number';
60
+ }
61
+
62
+ function isObject(value) {
63
+ return value !== null && typeof value === 'object';
64
+ }
65
+
66
+ function isNotEmptyObject(value) {
67
+ return isObject(value) && Object.keys(value).length > 0;
68
+ }
69
+
70
+ function allowNullIfNeeded(schema, { allowNull = false, forbidArrayNull = true, customizedNullValues = [null] }) {
71
+ if (allowNull) {
72
+ const definition = schema.describe();
73
+ if (definition.flags && definition.flags.only && definition.allow) {
74
+ // Already has enum restriction
75
+ return schema;
76
+ }
77
+ if (!schema.type && (schema.allOf || schema.anyOf || schema.oneOf || schema.not)) {
78
+ return schema;
79
+ }
80
+ if (forbidArrayNull && schema.type === 'array') {
81
+ return schema;
82
+ }
83
+ return schema.allow(...customizedNullValues);
84
+ }
85
+ return schema;
86
+ }
87
+
88
+ function addEnumRestriction(joiSchema, enumList, { allowNull = false, strictEnum = true, enableEnum = true }) {
89
+ if (!enableEnum || !Array.isArray(enumList) || enumList.length === 0) {
90
+ return joiSchema;
91
+ }
92
+
93
+ const valids = [].concat(enumList);
94
+ if (allowNull && !strictEnum) {
95
+ if (joiSchema.type === 'string') {
96
+ valids.push('');
97
+ }
98
+ valids.push(null);
99
+ }
100
+ return joiSchema.valid(...valids);
101
+ }
102
+
103
+ function normalizedId(id) {
104
+ return id.replace(/[^a-zA-Z0-9]/g, '_');
105
+ }
106
+
107
+ function getJoiInstance(joiOptions, extensions, joiInstance) {
108
+ if (joiInstance) {
109
+ return joiInstance;
110
+ }
111
+ if (isNotEmptyObject(joiOptions)) {
112
+ joiInstance = Joi.defaults((schema) => schema.options(joiOptions));
113
+ } else {
114
+ joiInstance = Joi;
115
+ }
116
+
117
+ return joiInstance.extend(
118
+ {
119
+ type: 'object',
120
+ base: joiInstance.object(),
121
+ coerce: {
122
+ from: 'string',
123
+ method(value) {
124
+ if (typeof value !== 'string' || (value[0] !== '{' && !/^\s*\{/.test(value))) {
125
+ return;
126
+ }
127
+ try {
128
+ return { value: Bourne.parse(value) };
129
+ } catch (ignoreErr) { /* eslint-disable-line no-unused-vars */
130
+ // Purposefully ignoring error
131
+ }
132
+ }
133
+ }
134
+ },
135
+ {
136
+ type: 'array',
137
+ base: joiInstance.array(),
138
+ coerce: {
139
+ from: 'string',
140
+ method(value) {
141
+ if (typeof value !== 'string' || (value[0] !== '[' && !/^\s*\[/.test(value))) {
142
+ return;
143
+ }
144
+ try {
145
+ return { value: Bourne.parse(value) };
146
+ } catch (ignoreErr) { /* eslint-disable-line no-unused-vars */
147
+ // Purposefully ignoring error
148
+ }
149
+ }
150
+ }
151
+ },
152
+ ...extensions
153
+ );
154
+ }
155
+
156
+ const mapToObj = m => {
157
+ return Array.from(m).reduce((obj, [key, value]) => {
158
+ obj[key] = Joi.isSchema(value) ? value.describe() : value;
159
+ return obj;
160
+ }, {});
161
+ };
162
+
163
+ class SchemaResolver {
164
+ constructor(options, joiInstance) {
165
+ const validatedResult = optionsSchema.validate(options);
166
+
167
+ Hoek.assert(!validatedResult.error, validatedResult.error);
168
+
169
+ const { subSchemas = {}, extensions = [], joiOptions } = validatedResult.value;
170
+
171
+ this.resolveOptions = mergeOverridableOptions({}, validatedResult.value);
172
+ this.subSchemas = subSchemas;
173
+ this.joiOptions = joiOptions;
174
+ this.joiSharedSchemas = new Map();
175
+ this.resolveOptions.noDefaults = this.joiOptions.noDefaults;
176
+
177
+ this.joi = getJoiInstance(joiOptions, extensions, joiInstance);
178
+
179
+ for (let sharedSchemaId in this.subSchemas) {
180
+ const sharedSchema = this.subSchemas[sharedSchemaId];
181
+ if (typeof sharedSchema === 'object' && !sharedSchema.$id) {
182
+ // shorthand schema type does not need to set $id
183
+ sharedSchema.$id = sharedSchemaId;
184
+ }
185
+ this.joiSharedSchemas.set(sharedSchemaId, this.resolve(sharedSchema, this.resolveOptions));
186
+ }
187
+
188
+ const itr = this.joiSharedSchemas.keys();
189
+ this.initialSharedSchemaIds = [];
190
+ for (const id of itr) {
191
+ this.initialSharedSchemaIds.push(id);
192
+ }
193
+ }
194
+
195
+ resolveShareSchemas(schema, resolveOptions, sharedProperties = '$defs', baseUri = '') {
196
+ const localShared = schema[sharedProperties]
197
+ if (localShared) {
198
+ for (let sharedSchemaId in localShared) {
199
+ this.joiSharedSchemas.set(
200
+ `${baseUri}#/${sharedProperties}/${sharedSchemaId}`,
201
+ this.resolve(localShared[sharedSchemaId], resolveOptions, baseUri)
202
+ );
203
+ }
204
+ }
205
+ }
206
+
207
+ convert(schema, options = {}) {
208
+ const validateSchema = schemaSchema.validate(schema);
209
+ const { value: overrideOptions, error: overrideOptionsError } = overrideOptionsSchema.validate(options);
210
+
211
+ Hoek.assert(!validateSchema.error, validateSchema.error);
212
+ Hoek.assert(!overrideOptionsError, overrideOptionsError);
213
+
214
+ const schemaId = schema.$id || '';
215
+ this.subSchemas[schemaId] = schema;
216
+
217
+ const joiSchema = this.resolve(schema, mergeOverridableOptions({}, this.resolveOptions, overrideOptions));
218
+
219
+ // Cleanup all shared schema registered other than the initial shared ones
220
+ delete this.subSchemas[schemaId];
221
+ const itr = this.joiSharedSchemas.keys();
222
+ for (const id of itr) {
223
+ if (!this.initialSharedSchemaIds.includes(id)) {
224
+ delete this.subSchemas[id];
225
+ this.joiSharedSchemas.delete(id);
226
+ }
227
+ }
228
+ return joiSchema;
229
+ }
230
+
231
+ resolve(schema, resolveOptions = {}, baseUri = (schema.$id || ''), isPropertyRequired = false) {
232
+ if (this.joi.isSchema(schema)) {
233
+ return schema;
234
+ }
235
+
236
+ const schemaId = schema.$id;
237
+ let resolvedSchema;
238
+
239
+ if (schemaId) {
240
+ baseUri = schemaId;
241
+ if (!this.subSchemas[schemaId]) {
242
+ this.subSchemas[schemaId] = schema;
243
+ }
244
+ }
245
+
246
+ this.resolveShareSchemas(schema, resolveOptions, '$defs', baseUri);
247
+ this.resolveShareSchemas(schema, resolveOptions, 'definitions', baseUri);
248
+
249
+ if (typeof schema === 'string') {
250
+ // If schema is itself a string, interpret it as a type
251
+ resolvedSchema = this.resolveType({ type: schema }, resolveOptions, baseUri, isPropertyRequired);
252
+ } else if (schema.$ref) {
253
+ if (schema.$ref.trim() === '#') {
254
+ return baseUri ? this.joi.link(`#${normalizedId(baseUri)}`) : this.joi.link('/');
255
+ }
256
+
257
+ resolvedSchema = this.resolve(this.resolveReference(schema.$ref, baseUri), resolveOptions, baseUri, isPropertyRequired);
258
+ } else {
259
+ const partialSchemas = [];
260
+ if (schema.type) {
261
+ partialSchemas.push(this.resolveType(schema, resolveOptions, baseUri, isPropertyRequired));
262
+ } else if (schema.properties) {
263
+ // if no type is specified, just properties
264
+ partialSchemas.push(this.object(schema, resolveOptions, baseUri))
265
+ } else if (schema.format) {
266
+ // if no type is specified, just format
267
+ partialSchemas.push(this.string(schema))
268
+ } else if (schema.enum) {
269
+ // If no type is specified, just enum
270
+ partialSchemas.push(
271
+ addEnumRestriction(this.joi.any().example(schema.enum[0]), schema.enum, resolveOptions)
272
+ );
273
+ } else if (schema.const) {
274
+ // Constant value
275
+ partialSchemas.push(this.getValueJoiType(schema.const).valid(schema.const));
276
+ }
277
+ if (schema.anyOf) {
278
+ partialSchemas.push(this.resolveAnyOf(schema, resolveOptions, baseUri));
279
+ }
280
+ if (schema.allOf) {
281
+ partialSchemas.push(this.resolveAllOf(schema, resolveOptions, baseUri));
282
+ }
283
+ if (schema.oneOf) {
284
+ partialSchemas.push(this.resolveOneOf(schema, resolveOptions, baseUri));
285
+ }
286
+ if (schema.not) {
287
+ partialSchemas.push(this.resolveNot(schema, resolveOptions, baseUri));
288
+ }
289
+ if (partialSchemas.length === 0) {
290
+ //Fall through to whatever.
291
+ console.warn('WARNING: schema missing a \'type\' or \'$ref\' or \'enum\': \n%s', JSON.stringify(schema, null, 2));
292
+ //TODO: Handle better
293
+ partialSchemas.push(this.joi.any());
294
+ }
295
+ resolvedSchema = partialSchemas.length === 1 ? partialSchemas[0] : this.joi.alternatives(partialSchemas).match('all');
296
+ }
297
+
298
+ if (resolveOptions.refineSchema) {
299
+ resolvedSchema = resolveOptions.refineSchema(resolvedSchema, schema);
300
+ }
301
+
302
+ if (schema.default !== undefined && !resolveOptions.noDefaults) {
303
+ resolvedSchema = resolvedSchema.default(schema.default)
304
+ }
305
+
306
+ resolvedSchema = this.copyMetaAnnotations(schema, resolvedSchema);
307
+
308
+ if (schemaId && !(this.joiSharedSchemas.get(schemaId))) {
309
+ resolvedSchema = resolvedSchema.id(normalizedId(schemaId));
310
+ this.joiSharedSchemas.set(schemaId, resolvedSchema);
311
+ }
312
+
313
+ if (schema.$anchor) {
314
+ this.joiSharedSchemas.set(`${baseUri}#${schema.$anchor}`, resolvedSchema);
315
+ }
316
+
317
+ if (isPropertyRequired) {
318
+ resolvedSchema = resolvedSchema.required();
319
+ if (resolveOptions.strictRequired) {
320
+ resolvedSchema = resolvedSchema.invalid(null, '');
321
+ }
322
+ } else {
323
+ resolvedSchema = allowNullIfNeeded(resolvedSchema, resolveOptions);
324
+ }
325
+
326
+ return resolvedSchema;
327
+ }
328
+
329
+ resolveReference(value, baseUri = '') {
330
+ debug('----- Resolving schema reference: %s with baseUri: %s ----- ', value, baseUri);
331
+ debug('Registered JOI schema for shared: %f', () => {
332
+ return JSON.stringify(mapToObj(this.joiSharedSchemas), null, 2);
333
+ });
334
+ const hashIdx = value.indexOf('#');
335
+ let refSchema;
336
+
337
+ if (hashIdx === -1) {
338
+ // Locate through $ref: 'refId'
339
+ // debug('# Resolving reference through id: %s', value);
340
+ refSchema = this.joiSharedSchemas.get(value);
341
+ } else {
342
+ const id = value.substring(0, hashIdx);
343
+ const path = value.substring(hashIdx + 1);
344
+
345
+ const canonicalURI = id ? value : `${baseUri}${value}`;
346
+
347
+ if (canonicalURI.indexOf('/') === -1) {
348
+ // Locate through $ref: [baseUri]#anchorSchema
349
+ // debug('# Resolving reference through $anchor: %s', canonicalURI);
350
+ refSchema = this.joiSharedSchemas.get(canonicalURI);
351
+ } else if (canonicalURI.indexOf('/properties') === -1) {
352
+ // Locate through $ref: [baseUri]#/$defs/shared
353
+ // debug('# Resolving reference through direct canonicalURI: %s', canonicalURI);
354
+ refSchema = this.joiSharedSchemas.get(canonicalURI);
355
+ } else {
356
+ // Locate through $ref: [baseUri]#/$defs/shared/properties/level1 or [baseUri]#/properties/product
357
+ // Can only get raw schema and build Joi again because it might not completely built yet
358
+ // debug('# Resolving reference through relative path: baseUri - %s; id - %s; path - %s', baseUri, id, path);
359
+
360
+ const paths = path.split('/');
361
+ refSchema = this.subSchemas[id || baseUri];
362
+
363
+ while (paths.length > 0 && refSchema) {
364
+ if (paths[0]) {
365
+ refSchema = refSchema[paths[0]];
366
+ }
367
+ paths.splice(0, 1);
368
+ }
369
+ }
370
+ }
371
+
372
+ Hoek.assert(refSchema, `Can not find schema reference: ${value} with baseUri: ${baseUri}`);
373
+
374
+ return refSchema;
375
+ }
376
+
377
+ resolveType(schema, resolveOptions, baseUri, isPropertyRequired = false) {
378
+ let joischema;
379
+
380
+ const joitype = (type, options, format) => {
381
+ let joischema;
382
+
383
+ if (options.refineType) {
384
+ type = options.refineType(type, format);
385
+ }
386
+
387
+ switch (type) {
388
+ case 'array':
389
+ joischema = this.array(schema, options, baseUri, isPropertyRequired);
390
+ break;
391
+ case 'boolean':
392
+ joischema = this.joi.boolean();
393
+ break;
394
+ case 'integer':
395
+ case 'number':
396
+ joischema = this.number(schema);
397
+ break;
398
+ case 'object':
399
+ joischema = this.object(schema, options, baseUri);
400
+ break;
401
+ case 'string':
402
+ joischema = this.string(schema);
403
+ break;
404
+ case 'null':
405
+ joischema = this.joi.any().valid(...options.customizedNullValues);
406
+ break;
407
+ default:
408
+ joischema = this.joi.types()[type];
409
+ }
410
+
411
+ Hoek.assert(joischema, 'Could not resolve type: ' + schema.type + '.');
412
+
413
+ return joischema;
414
+ }
415
+
416
+ let nullIdx;
417
+ if (Array.isArray(schema.type)) {
418
+ nullIdx = schema.type.findIndex((type) => type === 'null');
419
+ if (nullIdx > -1) {
420
+ schema.type.splice(nullIdx, 1);
421
+ }
422
+ if (schema.type.length === 1) {
423
+ schema.type = schema.type[0];
424
+ } else if (schema.type.length === 0) {
425
+ schema.type = 'null';
426
+ }
427
+ }
428
+
429
+ if (Array.isArray(schema.type)) {
430
+ const schemas = [];
431
+
432
+ for (let i = 0; i < schema.type.length; i++) {
433
+ schemas.push(joitype(schema.type[i], resolveOptions, schema.format));
434
+ }
435
+
436
+ joischema = this.joi.alternatives(schemas);
437
+ } else {
438
+ joischema = joitype(schema.type, resolveOptions, schema.format);
439
+ }
440
+ if (nullIdx > -1) {
441
+ joischema = allowNullIfNeeded(joischema,
442
+ { allowNull: true, customizedNullValues: resolveOptions.customizedNullValues });
443
+ }
444
+
445
+ const typeDefinitionMap = {
446
+ title: 'label'
447
+ };
448
+ Object.keys(typeDefinitionMap).forEach(function (key) {
449
+ if (schema[key] !== undefined) {
450
+ joischema = joischema[typeDefinitionMap[key]](schema[key]);
451
+ }
452
+ });
453
+
454
+ let desc = schema.description;
455
+ if (resolveOptions.refineDescription) {
456
+ desc = resolveOptions.refineDescription(schema);
457
+ }
458
+ if (desc) {
459
+ joischema = joischema.description(desc);
460
+ }
461
+
462
+ let example;
463
+ if (schema.examples && schema.examples.length > 0) {
464
+ example = schema.examples[0];
465
+ }
466
+ if (!example && typeof schema.default !== 'undefined') {
467
+ example = schema.default;
468
+ }
469
+ if (!example && schema.enum && schema.enum.length > 0) {
470
+ example = schema.enum[0];
471
+ }
472
+ if (example) {
473
+ joischema = joischema.type === 'array' && !Array.isArray(example) ? joischema.example([example]) : joischema.example(example);
474
+ }
475
+
476
+ return addEnumRestriction(joischema, schema.enum, resolveOptions);
477
+ }
478
+
479
+ copyMetaAnnotations(schema, joischema) {
480
+ const metaDataNames = ['title', 'deprecated', 'readOnly', 'writeOnly'];
481
+ const metas = {};
482
+ for (const metaName of metaDataNames) {
483
+ if (typeof schema[metaName] !== 'undefined') {
484
+ metas[metaName] = schema[metaName];
485
+ }
486
+ }
487
+
488
+ if (Object.keys(metas).length === 0) {
489
+ return joischema;
490
+ }
491
+
492
+ return joischema.meta(metas);
493
+ }
494
+
495
+ resolveOneOf(schema, resolveOptions, baseUri) {
496
+ Hoek.assert(Array.isArray(schema.oneOf), 'Expected oneOf to be an array.');
497
+
498
+ return this.joi.alternatives(schema.oneOf.map(schema => this.resolve(schema, resolveOptions, baseUri))).match('one');
499
+ }
500
+
501
+ resolveAnyOf(schema, resolveOptions, baseUri) {
502
+ Hoek.assert(Array.isArray(schema.anyOf), 'Expected anyOf to be an array.');
503
+
504
+ return this.joi.alternatives(schema.anyOf.map(schema => this.resolve(schema, resolveOptions, baseUri))).match('any');
505
+ }
506
+
507
+ resolveAllOf(schema, resolveOptions, baseUri) {
508
+ Hoek.assert(Array.isArray(schema.allOf), 'Expected allOf to be an array.');
509
+
510
+ return this.joi.alternatives(schema.allOf.map(schema => this.resolve(schema, resolveOptions, baseUri))).match('all');
511
+ }
512
+
513
+ resolveNot(schema, resolveOptions, baseUri) {
514
+ Hoek.assert(isObject(schema.not), 'Expected Not to be an object.');
515
+
516
+ return this.joi.alternatives().conditional(
517
+ '.',
518
+ {
519
+ not: this.resolve(schema.not, resolveOptions, baseUri),
520
+ then: this.joi.any(),
521
+ otherwise: this.joi.any().forbidden()
522
+ }
523
+ );
524
+ }
525
+
526
+ resolveProperties(jsonSchema, resolveOptions, baseUri) {
527
+ const schemas = {};
528
+
529
+ if (!isObject(jsonSchema.properties)) {
530
+ return;
531
+ }
532
+
533
+ Object.keys(jsonSchema.properties).forEach((key) => {
534
+ const property = jsonSchema.properties[key];
535
+ const isPropertyRequired = Array.isArray(jsonSchema.required) && !!~jsonSchema.required.indexOf(key);
536
+
537
+ const joischema = this.resolve(property, resolveOptions, baseUri, isPropertyRequired);
538
+
539
+ schemas[key] = joischema;
540
+ });
541
+
542
+ return schemas;
543
+ }
544
+
545
+ object(schema, resolveOptions, baseUri) {
546
+ let joischema = this.joi.object(this.resolveProperties(schema, resolveOptions, baseUri));
547
+
548
+ if (isObject(schema.additionalProperties)) {
549
+ joischema = joischema.pattern(/^/, this.resolve(schema.additionalProperties, resolveOptions, baseUri));
550
+ } else if (typeof this.joiOptions.allowUnknown === 'undefined' || typeof schema.additionalProperties !== 'undefined') {
551
+ joischema = joischema.unknown(schema.additionalProperties !== false);
552
+ }
553
+
554
+ isNumber(schema.minProperties) && (joischema = joischema.min(schema.minProperties));
555
+ isNumber(schema.maxProperties) && (joischema = joischema.max(schema.maxProperties));
556
+
557
+ return joischema;
558
+ }
559
+
560
+ resolveAsArray(value, resolveOptions, baseUri, isPropertyRequired = false) {
561
+ return [].concat(value).map((v) => {
562
+ const vSchema = this.resolve(v, resolveOptions, baseUri);
563
+ if (resolveOptions.strictArrayRequired && isPropertyRequired) {
564
+ return vSchema.invalid(...resolveOptions.customizedNullValues);
565
+ }
566
+ return allowNullIfNeeded(vSchema, resolveOptions)
567
+ });
568
+ }
569
+
570
+ array(schema, resolveOptions, baseUri, isPropertyRequired = false) {
571
+ let joischema = this.joi.array();
572
+ let items;
573
+
574
+ if (schema.items) {
575
+ items = this.resolveAsArray(schema.items, resolveOptions, baseUri, isPropertyRequired);
576
+
577
+ joischema = joischema.items(...items);
578
+ } else if (schema.ordered) {
579
+ items = this.resolveAsArray(schema.ordered, resolveOptions, baseUri, isPropertyRequired);
580
+ joischema = joischema.ordered(...items);
581
+ }
582
+
583
+ if (items && schema.additionalItems === false) {
584
+ joischema = joischema.max(items.length);
585
+ }
586
+
587
+ if (isPropertyRequired && resolveOptions.strictArrayRequired && !schema.minItems) {
588
+ schema.minItems = 1;
589
+ }
590
+ isNumber(schema.minItems) && (joischema = joischema.min(schema.minItems));
591
+ isNumber(schema.maxItems) && (joischema = joischema.max(schema.maxItems));
592
+
593
+ if (schema.uniqueItems) {
594
+ joischema = joischema.unique();
595
+ }
596
+ if (schema.contains) {
597
+ joischema = joischema.has(this.resolve(schema.contains, resolveOptions, baseUri));
598
+ }
599
+
600
+ return joischema;
601
+ }
602
+
603
+ number(schema) {
604
+ let joischema = this.joi.number();
605
+
606
+ if (schema.type === 'integer') {
607
+ joischema = joischema.integer();
608
+ }
609
+
610
+ isNumber(schema.minimum) && (joischema = joischema.min(schema.minimum));
611
+ isNumber(schema.maximum) && (joischema = joischema.max(schema.maximum));
612
+ isNumber(schema.exclusiveMinimum) && (joischema = joischema.greater(schema.exclusiveMinimum));
613
+ isNumber(schema.exclusiveMaximum) && (joischema = joischema.less(schema.exclusiveMaximum));
614
+ isNumber(schema.multipleOf) && schema.multipleOf !== 0 && (joischema = joischema.multiple(schema.multipleOf));
615
+
616
+ return joischema;
617
+ }
618
+
619
+ string(schema) {
620
+ let joischema = this.joi.string();
621
+
622
+ const dateRegex = '(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])';
623
+ const timeRegex = '([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(.[0-9]+)?(Z|(\\+|-)([01][0-9]|2[0-3]):([0-5][0-9]))';
624
+ const dateTimeRegex = dateRegex + 'T' + timeRegex;
625
+
626
+ switch (schema.format) {
627
+ case 'date':
628
+ return joischema.regex(new RegExp('^' + dateRegex + '$', 'i'), 'JsonSchema date format');
629
+ case 'time':
630
+ return joischema.regex(new RegExp('^' + timeRegex + '$', 'i'), 'JsonSchema time format');
631
+ case 'date-time':
632
+ return joischema.regex(new RegExp('^' + dateTimeRegex + '$', 'i'), 'JsonSchema date-time format');
633
+ case 'binary':
634
+ joischema = this.binary(schema);
635
+ break;
636
+ case 'email':
637
+ return joischema.email();
638
+ case 'hostname':
639
+ return joischema.hostname();
640
+ case 'ipv4':
641
+ return joischema.ip({
642
+ version: ['ipv4']
643
+ });
644
+ case 'ipv6':
645
+ return joischema.ip({
646
+ version: ['ipv6']
647
+ });
648
+ case 'uri':
649
+ return joischema.uri();
650
+ case 'byte':
651
+ joischema = joischema.base64();
652
+ break;
653
+ case 'uuid':
654
+ return joischema.guid({ version: ['uuidv4'] });
655
+ case 'guid':
656
+ return joischema.guid();
657
+ }
658
+ return this.regularString(schema, joischema);
659
+ }
660
+
661
+ regularString(schema, joischema) {
662
+ schema.pattern && (joischema = joischema.regex(new RegExp(schema.pattern)));
663
+
664
+ if ((typeof schema.minLength === 'undefined' || schema.minLength === 0) && !schema.pattern && !schema.format && !schema.enum) {
665
+ joischema = joischema.allow('');
666
+ }
667
+
668
+ isNumber(schema.minLength) && (joischema = joischema.min(schema.minLength));
669
+ isNumber(schema.maxLength) && (joischema = joischema.max(schema.maxLength));
670
+ return joischema;
671
+ }
672
+
673
+ binary(schema) {
674
+ let joischema = this.joi.binary();
675
+ isNumber(schema.minLength) && (joischema = joischema.min(schema.minLength));
676
+ isNumber(schema.maxLength) && (joischema = joischema.max(schema.maxLength));
677
+ return joischema;
678
+ }
679
+
680
+ getValueJoiType(value) {
681
+ let joiType;
682
+ switch (typeof value) {
683
+ case 'string':
684
+ joiType = this.joi.string();
685
+ break;
686
+ case 'number':
687
+ joiType = this.joi.number();
688
+ break;
689
+ case 'boolean':
690
+ joiType = this.joi.boolean();
691
+ break;
692
+ default:
693
+ joiType = this.joi.any();
694
+ }
695
+ return joiType;
696
+ }
697
+ }
698
+
699
+ module.exports = SchemaResolver;