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