joi-to-json 5.0.3 → 5.0.4

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.
@@ -1,811 +1,822 @@
1
- /* eslint no-use-before-define: 'off' */
2
- const _ = require('lodash')
3
- const combinations = require('combinations')
4
-
5
- /**
6
- * Default Joi logical operator (or/and/nand/xor/oxor) parsers
7
- */
8
- const LOGICAL_OP_PARSER = {
9
- or: function (schema, dependency) {
10
- schema.anyOf = _.map(dependency.peers, (peer) => {
11
- return { required: [peer] }
12
- })
13
- },
14
- and: function (schema, dependency) {
15
- schema.oneOf = [{ required: dependency.peers }]
16
- schema.oneOf.push({
17
- allOf: _.map(dependency.peers, (peer) => {
18
- return { not: { required: [peer] } }
19
- })
20
- })
21
- },
22
- nand: function (schema, dependency) {
23
- schema.not = { required: dependency.peers }
24
- },
25
- xor: function (schema, dependency) {
26
- schema.if = {
27
- propertyNames: { enum: dependency.peers },
28
- minProperties: 2
29
- }
30
- schema.then = false
31
- schema.else = {
32
- oneOf: _.map(dependency.peers, (peer) => {
33
- return { required: [peer] }
34
- })
35
- }
36
- },
37
- oxor: function (schema, dependency) {
38
- schema.oneOf = _.map(dependency.peers, (peer) => {
39
- return { required: [peer] }
40
- })
41
- schema.oneOf.push({
42
- not: {
43
- oneOf: _.map(combinations(dependency.peers, 1, 2), (combination) => {
44
- return { required: combination }
45
- })
46
- }
47
- })
48
- },
49
- with: function (schema, dependency) {
50
- schema.dependentRequired = schema.dependentRequired || {}
51
- schema.dependentRequired[dependency.key] = dependency.peers
52
- },
53
- without: function (schema, dependency) {
54
- schema.if = { required: [dependency.key] }
55
- schema.then = {
56
- not: {
57
- anyOf: _.map(dependency.peers, (peer) => {
58
- return { required: [peer] }
59
- })
60
- }
61
- }
62
- }
63
- }
64
-
65
- /**
66
- * Recognize the `joi.override` representation in `describe()` output.
67
- *
68
- * `joi.override` is a Symbol that can be used in `joi.any().valid(…)`
69
- * statements, to reset the list of valid values. In `describe()` output, it
70
- * turns up as an object with 1 property:
71
- *
72
- * ```
73
- * { override: true }
74
- * ```
75
- */
76
- function isJoiOverride(e) {
77
- return typeof e === 'object'
78
- && e !== null
79
- && Object.keys(e).length === 1
80
- && e.override === true
81
- }
82
-
83
- class JoiJsonSchemaParser {
84
- constructor(opts = {}) {
85
- this.$schema = opts.$schema || 'http://json-schema.org/draft-07/schema#'
86
- this.includeSchemaDialect = opts.includeSchemaDialect || false
87
- this.childrenFieldName = this._getChildrenFieldName()
88
- this.optionsFieldName = this._getOptionsFieldName()
89
- this.ruleArgFieldName = this._getRuleArgFieldName()
90
- this.enumFieldName = this._getEnumFieldName()
91
- this.allowUnknownFlagName = this._getAllowUnknownFlagName()
92
- this.inheritedPreferences = []
93
-
94
- if (opts.logicalOpParser === false) {
95
- this.logicalOpParser = {}
96
- } else {
97
- this.logicalOpParser = _.merge({}, LOGICAL_OP_PARSER, opts.logicalOpParser)
98
- }
99
- }
100
-
101
- parse(joiSpec, definitions = {}, level = 0) {
102
- let schema = {}
103
-
104
- const hasPreferences = this._pushInheritiedPreferences(joiSpec)
105
-
106
- if (this._getPresence(joiSpec) === 'forbidden') {
107
- if (hasPreferences) {
108
- this._popInheritiedPreferences()
109
- }
110
- schema.not = {}
111
- return schema
112
- }
113
-
114
- this._setBasicProperties(schema, joiSpec)
115
- this._setNumberFieldProperties(schema, joiSpec)
116
- this._setBinaryFieldProperties(schema, joiSpec)
117
- this._setStringFieldProperties(schema, joiSpec)
118
- this._setDateFieldProperties(schema, joiSpec)
119
- this._setArrayFieldProperties(schema, joiSpec, definitions, level)
120
- this._setObjectProperties(schema, joiSpec, definitions, level)
121
- this._setAlternativesProperties(schema, joiSpec, definitions, level)
122
- this._setConditionProperties(schema, joiSpec, definitions, level, 'whens')
123
- this._setMetaProperties(schema, joiSpec)
124
- this._setLinkFieldProperties(schema, joiSpec)
125
- this._setConst(schema, joiSpec)
126
- this._setAnyProperties(schema, joiSpec, definitions, level)
127
- this._addNullTypeIfNullable(schema, joiSpec)
128
-
129
- if (!_.isEmpty(joiSpec.shared)) {
130
- _.forEach(joiSpec.shared, (sharedSchema) => {
131
- this.parse(sharedSchema, definitions, level)
132
- })
133
- }
134
-
135
- this._copyMetasToSchema(joiSpec, schema)
136
-
137
- const schemaId = _.get(joiSpec, 'flags.id')
138
- if (schemaId) {
139
- definitions[schemaId] = schema
140
- schema = {
141
- $ref: `${this._getLocalSchemaBasePath()}/${schemaId}`
142
- }
143
- }
144
- if (level === 0 && !_.isEmpty(definitions)) {
145
- _.set(schema, `${this._getLocalSchemaBasePath().replace('#/', '').replace(/\//, '.')}`, definitions)
146
- }
147
-
148
- if (this.includeSchemaDialect && level === 0 && this.$schema) {
149
- schema.$schema = this.$schema
150
- }
151
-
152
- if (hasPreferences) {
153
- this._popInheritiedPreferences()
154
- }
155
- return schema
156
- }
157
-
158
- _pushInheritiedPreferences(joiSpec) {
159
- if (joiSpec.preferences) {
160
- this.inheritedPreferences.push(_.merge({}, this._getInheritedPreferences(), joiSpec.preferences))
161
- return true
162
- }
163
- return false
164
- }
165
-
166
- _popInheritiedPreferences() {
167
- this.inheritedPreferences.pop()
168
- }
169
-
170
- _getInheritedPreferences() {
171
- return this.inheritedPreferences[this.inheritedPreferences.length - 1] || {}
172
- }
173
-
174
- _isIfThenElseSupported() {
175
- return true
176
- }
177
-
178
- _getChildrenFieldName() {
179
- return 'keys'
180
- }
181
-
182
- _getOptionsFieldName() {
183
- return 'preferences'
184
- }
185
-
186
- _getRuleArgFieldName() {
187
- return 'args'
188
- }
189
-
190
- _getEnumFieldName() {
191
- return 'allow'
192
- }
193
-
194
- _getAllowUnknownFlagName() {
195
- return 'unknown'
196
- }
197
-
198
- _getLocalSchemaBasePath() {
199
- return '#/$defs'
200
- }
201
-
202
- _getFieldDescription(fieldDefn) {
203
- return _.get(fieldDefn, 'flags.description')
204
- }
205
-
206
- _getFieldType(fieldDefn) {
207
- let type = fieldDefn.type
208
- if (type === 'number' && !_.isEmpty(fieldDefn.rules) &&
209
- fieldDefn.rules[0].name === 'integer') {
210
- type = 'integer'
211
- }
212
- return type
213
- }
214
-
215
- _addNullTypeIfNullable(fieldSchema, fieldDefn) {
216
- // This should always be the last call in parse
217
- if (fieldSchema.const === null) {
218
- return
219
- }
220
-
221
- const enums = _.get(fieldDefn, this.enumFieldName)
222
- if (Array.isArray(enums) && enums.includes(null)) {
223
- if (Array.isArray(fieldSchema.type)) {
224
- if (!fieldSchema.type.includes('null')) {
225
- fieldSchema.type.push('null')
226
- }
227
- } else if (fieldSchema.type) {
228
- fieldSchema.type = [fieldSchema.type, 'null']
229
- } else {
230
- fieldSchema.type = 'null'
231
- }
232
- }
233
- }
234
-
235
- _getFieldExample(fieldDefn) {
236
- return _.get(fieldDefn, 'examples')
237
- }
238
-
239
- _getPresence(fieldDefn) {
240
- const presence = _.get(fieldDefn, 'flags.presence')
241
- if (presence !== undefined) {
242
- return presence
243
- }
244
- const preferences = fieldDefn[this.optionsFieldName] || this._getInheritedPreferences()
245
- return _.get(preferences, 'presence')
246
- }
247
-
248
- _isRequired(fieldDefn) {
249
- const presence = this._getPresence(fieldDefn)
250
- return presence === 'required'
251
- }
252
-
253
- _getDefaultValue(fieldDefn) {
254
- return _.get(fieldDefn, 'flags.default')
255
- }
256
-
257
- _getEnum(fieldDefn) {
258
- const enumList = fieldDefn[this.enumFieldName]
259
- const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
260
- if (fieldDefn.flags && fieldDefn.flags.only && _.size(filteredEnumList) > 1) {
261
- return _.uniq(filteredEnumList)
262
- }
263
- }
264
-
265
- _getUnknown(joiSpec) {
266
- const allowUnknown = _.get(joiSpec, `flags.${this.allowUnknownFlagName}`)
267
- if (allowUnknown !== undefined) {
268
- return allowUnknown
269
- }
270
- const preferences = joiSpec[this.optionsFieldName] || this._getInheritedPreferences()
271
- return _.get(preferences, 'allowUnknown') || false
272
- }
273
-
274
- _setIfNotEmpty(schema, field, value) {
275
- if (value !== null && value !== undefined) {
276
- schema[field] = value
277
- }
278
- }
279
-
280
- _setBasicProperties(fieldSchema, fieldDefn) {
281
- this._setIfNotEmpty(fieldSchema, 'type', this._getFieldType(fieldDefn))
282
- this._setIfNotEmpty(fieldSchema, 'examples', this._getFieldExample(fieldDefn))
283
- this._setIfNotEmpty(fieldSchema, 'description', this._getFieldDescription(fieldDefn))
284
- this._setIfNotEmpty(fieldSchema, 'default', this._getDefaultValue(fieldDefn))
285
- this._setIfNotEmpty(fieldSchema, 'enum', this._getEnum(fieldDefn))
286
- if (fieldDefn.invalid && fieldDefn.invalid.length > 0) {
287
- fieldSchema.not = {
288
- enum: fieldDefn.invalid
289
- }
290
- }
291
- }
292
-
293
- _setBinaryFieldProperties(fieldSchema, fieldDefn) {
294
- if (fieldSchema.type !== 'binary') {
295
- return
296
- }
297
- fieldSchema.type = 'string'
298
- if (fieldDefn.flags && fieldDefn.flags.encoding) {
299
- fieldSchema.contentEncoding = fieldDefn.flags.encoding
300
- }
301
- fieldSchema.format = 'binary'
302
- }
303
-
304
- _setObjectProperties(schema, joiSpec, definitions, level) {
305
- if (schema.type !== 'object') {
306
- return
307
- }
308
-
309
- schema.properties = {}
310
- schema.required = []
311
-
312
- schema.additionalProperties = this._getUnknown(joiSpec)
313
-
314
- _.map(joiSpec[this.childrenFieldName], (fieldDefn, key) => {
315
- const fieldSchema = this.parse(fieldDefn, definitions, level + 1)
316
- if (this._isRequired(fieldDefn)) {
317
- schema.required.push(key)
318
- }
319
-
320
- schema.properties[key] = fieldSchema
321
- })
322
-
323
- /**
324
- * For dynamic key scenarios to store the pattern as key
325
- * and have the properties be as with other examples
326
- */
327
- if (joiSpec.patterns) {
328
- _.each(joiSpec.patterns, patternObj => {
329
- if (typeof patternObj.rule !== 'object' || typeof patternObj.regex === 'undefined') {
330
- return
331
- }
332
-
333
- schema.properties[patternObj.regex] = this.parse(patternObj.rule, definitions, level + 1)
334
- schema.patternProperties = schema.patternProperties || {}
335
-
336
- let regexString = patternObj.regex
337
- regexString = regexString.indexOf('/') === 0 ? regexString.substring(1) : regexString
338
- regexString = regexString.lastIndexOf('/') > -1 ? regexString.substring(0, regexString.length - 1) : regexString
339
-
340
- schema.patternProperties[regexString] = schema.properties[patternObj.regex]
341
- })
342
- }
343
-
344
- if (_.isEmpty(schema.required)) {
345
- delete schema.required
346
- }
347
-
348
- this._setObjectDependencies(schema, joiSpec)
349
- }
350
-
351
- _setObjectDependencies(schema, joiSpec) {
352
- if (!_.isArray(joiSpec.dependencies) || joiSpec.dependencies.length === 0) {
353
- return
354
- }
355
-
356
- if (joiSpec.dependencies.length === 1) {
357
- this._setDependencySubSchema(schema, joiSpec.dependencies[0])
358
- } else {
359
- const withDependencies = _.remove(joiSpec.dependencies, (dependency) => {
360
- return dependency.rel === 'with'
361
- })
362
-
363
- schema.allOf = _.compact(_.map(joiSpec.dependencies, (dependency) => {
364
- const subSchema = this._setDependencySubSchema({}, dependency)
365
- if (_.isEmpty(subSchema)) {
366
- return null
367
- }
368
- return subSchema
369
- }))
370
-
371
- _.each(withDependencies, (dependency) => {
372
- this._setDependencySubSchema(schema, dependency)
373
- })
374
-
375
- if (schema.allOf.length === 0) {
376
- // When the logicalOpParser is set to false
377
- delete schema.allOf
378
- }
379
- }
380
- }
381
-
382
- _setDependencySubSchema(schema, dependency) {
383
- const opParser = this.logicalOpParser[dependency.rel]
384
- if (typeof opParser !== 'function') {
385
- return schema
386
- }
387
- opParser(schema, dependency)
388
- return schema
389
- }
390
-
391
- _setNumberFieldProperties(fieldSchema, fieldDefn) {
392
- if (fieldSchema.type !== 'number' && fieldSchema.type !== 'integer') {
393
- return
394
- }
395
-
396
- const ruleArgFieldName = this.ruleArgFieldName
397
-
398
- _.each(fieldDefn.rules, (rule) => {
399
- const value = rule[ruleArgFieldName]
400
- switch (rule.name) {
401
- case 'max':
402
- fieldSchema.maximum = value.limit
403
- break
404
- case 'min':
405
- fieldSchema.minimum = value.limit
406
- break
407
- case 'greater':
408
- fieldSchema.exclusiveMinimum = value.limit
409
- fieldSchema.minimum = value.limit
410
- break
411
- case 'less':
412
- fieldSchema.exclusiveMaximum = value.limit
413
- fieldSchema.maximum = value.limit
414
- break
415
- case 'multiple':
416
- fieldSchema.multipleOf = value.base
417
- break
418
- case 'sign':
419
- if (rule.args.sign === 'positive') {
420
- fieldSchema.exclusiveMinimum = 0
421
- } else if (rule.args.sign === 'negative') {
422
- fieldSchema.exclusiveMaximum = 0
423
- }
424
- break
425
- default:
426
- break
427
- }
428
- })
429
- }
430
-
431
- _setStringFieldProperties(fieldSchema, fieldDefn) {
432
- if (fieldSchema.type !== 'string') {
433
- return
434
- }
435
-
436
- if (fieldDefn.flags && fieldDefn.flags.encoding) {
437
- fieldSchema.contentEncoding = fieldDefn.flags.encoding
438
- }
439
-
440
- const ruleArgFieldName = this.ruleArgFieldName
441
-
442
- _.forEach(fieldDefn.rules, (rule) => {
443
- switch (rule.name) {
444
- case 'min':
445
- fieldSchema.minLength = rule[ruleArgFieldName].limit
446
- break
447
- case 'max':
448
- fieldSchema.maxLength = rule[ruleArgFieldName].limit
449
- break
450
- case 'email':
451
- fieldSchema.format = 'email'
452
- break
453
- case 'hostname':
454
- fieldSchema.format = 'hostname'
455
- break
456
- case 'uri':
457
- fieldSchema.format = 'uri'
458
- break
459
- case 'ip':
460
- const versions = rule[ruleArgFieldName].options.version
461
- if (!_.isEmpty(versions)) {
462
- if (versions.length === 1) {
463
- fieldSchema.format = versions[0]
464
- } else {
465
- fieldSchema.oneOf = _.map(versions, (version) => {
466
- return {
467
- format: version
468
- }
469
- })
470
- }
471
- } else {
472
- fieldSchema.format = 'ipv4'
473
- }
474
- break
475
- case 'pattern':
476
- let regex = rule[ruleArgFieldName].regex
477
- let idx = regex.indexOf('/')
478
- if (idx === 0) {
479
- regex = regex.replace('/', '')
480
- }
481
- idx = regex.lastIndexOf('/') === regex.length - 1
482
- if (idx > -1) {
483
- regex = regex.replace(/\/$/, '')
484
- }
485
- fieldSchema.pattern = regex
486
- break
487
- case 'isoDate':
488
- fieldSchema.format = 'date-time'
489
- break
490
- case 'isoDuration':
491
- fieldSchema.format = 'duration'
492
- break
493
- case 'uuid':
494
- case 'guid':
495
- fieldSchema.format = 'uuid'
496
- break
497
- default:
498
- break
499
- }
500
- })
501
- }
502
-
503
- _setArrayFieldProperties(fieldSchema, fieldDefn, definitions, level) {
504
- if (fieldSchema.type !== 'array') {
505
- return
506
- }
507
-
508
- const ruleArgFieldName = this.ruleArgFieldName
509
-
510
- _.each(fieldDefn.rules, (rule) => {
511
- const value = rule[ruleArgFieldName]
512
- switch (rule.name) {
513
- case 'max':
514
- fieldSchema.maxItems = value.limit
515
- break
516
- case 'min':
517
- fieldSchema.minItems = value.limit
518
- break
519
- case 'length':
520
- fieldSchema.maxItems = value.limit
521
- fieldSchema.minItems = value.limit
522
- break
523
- case 'unique':
524
- fieldSchema.uniqueItems = true
525
- break
526
- case 'has':
527
- fieldSchema.contains = this.parse(value.schema, definitions, level + 1)
528
- break
529
- default:
530
- break
531
- }
532
- })
533
-
534
- if (!fieldDefn.items) {
535
- fieldSchema.items = {}
536
- return
537
- }
538
-
539
- if (fieldDefn.items.length === 1) {
540
- fieldSchema.items = this.parse(fieldDefn.items[0], definitions, level + 1)
541
- } else {
542
- fieldSchema.items = {
543
- anyOf: _.map(fieldDefn.items, (itemSchema) => {
544
- return this.parse(itemSchema, definitions, level + 1)
545
- })
546
- }
547
- }
548
- }
549
-
550
- _setDateFieldProperties(fieldSchema, fieldDefn) {
551
- if (fieldSchema.type !== 'date') {
552
- return
553
- }
554
-
555
- if (fieldDefn.flags && fieldDefn.flags.format !== 'iso') {
556
- fieldSchema.type = 'integer'
557
- } else {
558
- // https://datatracker.ietf.org/doc/draft-handrews-json-schema-validation
559
- // JSON Schema does not have date type, but use string with format.
560
- // However, joi definition cannot clearly tells the date/time/date-time format
561
- fieldSchema.type = 'string'
562
- fieldSchema.format = 'date-time'
563
- }
564
- }
565
-
566
- _setAlternativesProperties(schema, joiSpec, definitions, level) {
567
- if (schema.type !== 'alternatives' || joiSpec.matches.length === 0) {
568
- return
569
- }
570
-
571
- if (joiSpec.matches[0].schema) {
572
- // try style
573
- let mode = 'anyOf'
574
- if (joiSpec.flags && joiSpec.flags.match === 'one') {
575
- mode = 'oneOf'
576
- } else if (joiSpec.flags && joiSpec.flags.match === 'all') {
577
- mode = 'allOf'
578
- }
579
-
580
- schema[mode] = _.map(joiSpec.matches, (match) => {
581
- return this.parse(match.schema, definitions, level + 1)
582
- })
583
-
584
- if (schema[mode].length === 1) {
585
- _.merge(schema, schema[mode][0])
586
- delete schema[mode]
587
- }
588
- } else {
589
- this._setConditionProperties(schema, joiSpec, definitions, level, 'matches', 'anyOf')
590
- }
591
-
592
- if (schema.type === 'alternatives') {
593
- delete schema.type
594
- }
595
- }
596
-
597
- _setConditionProperties(schema, joiSpec, definitions, level, conditionFieldName, logicKeyword = 'allOf') {
598
- if (!joiSpec[conditionFieldName] || joiSpec[conditionFieldName].length === 0) {
599
- return
600
- }
601
-
602
- let ifThenStyle = this._isIfThenElseSupported()
603
- const styleSetting = _.remove(joiSpec.metas, (meta) => {
604
- return typeof meta['if-style'] !== 'undefined'
605
- })
606
-
607
- if (ifThenStyle && styleSetting.length > 0 && styleSetting[0]['if-style'] === false) {
608
- ifThenStyle = false
609
- }
610
-
611
- if (joiSpec[conditionFieldName].length > 1) {
612
- // Multiple case
613
- schema[logicKeyword] = _.map(joiSpec[conditionFieldName], (condition) => {
614
- return this._setConditionSchema(ifThenStyle, {}, condition, definitions, level + 1)
615
- })
616
- } else {
617
- this._setConditionSchema(ifThenStyle, schema, joiSpec[conditionFieldName][0], definitions, level + 1)
618
- }
619
- }
620
-
621
- _setConditionSchema(ifThenStyle, schema, conditionJoiSpec, definitions, level) {
622
- // When "if" is not present, both "then" and "else" MUST be entirely ignored.
623
- // There must be either "is" or "switch"
624
- if (!conditionJoiSpec.is && !conditionJoiSpec.switch) {
625
- return
626
- }
627
-
628
- if (conditionJoiSpec.switch) {
629
- this._parseSwitchCondition(ifThenStyle, schema, conditionJoiSpec, definitions, level)
630
- } else {
631
- if (ifThenStyle) {
632
- this._parseIfThenElseCondition(schema, conditionJoiSpec, definitions, level)
633
- } else {
634
- this._setConditionCompositionStyle(schema, conditionJoiSpec, definitions, level)
635
- }
636
- }
637
-
638
- return schema
639
- }
640
-
641
- _parseIfThenElseCondition(schema, conditionSpec, definitions, level) {
642
- if (conditionSpec.ref) {
643
- // Currently, if there is reference, if-then-else style is not supported
644
- // To use it, the condition must be defined in parent level using schema-style is
645
- return this._setConditionCompositionStyle(schema, conditionSpec, definitions, level)
646
- }
647
-
648
- schema.if = this.parse(conditionSpec.is, definitions, level)
649
-
650
- if (conditionSpec.then) {
651
- schema.then = this.parse(conditionSpec.then, definitions, level)
652
- }
653
- if (conditionSpec.otherwise) {
654
- schema.else = this.parse(conditionSpec.otherwise, definitions, level)
655
- }
656
- return schema
657
- }
658
-
659
- _parseSwitchCondition(ifThenStyle, schema, condition, definitions, level) {
660
- // Switch cannot be used if the joi condition is a schema
661
- // Hence, condition.ref should always exists
662
- const anyOf = []
663
- for (const switchCondition of condition.switch) {
664
- if (ifThenStyle && !condition.ref) {
665
- const innerSchema = this._parseIfThenElseCondition(
666
- {}, switchCondition, definitions, level + 1)
667
-
668
- anyOf.push(innerSchema)
669
- } else {
670
- if (switchCondition.then) {
671
- anyOf.push(this.parse(switchCondition.then, definitions, level + 1))
672
- }
673
-
674
- if (switchCondition.otherwise) {
675
- anyOf.push(this.parse(switchCondition.otherwise, definitions, level + 1))
676
- }
677
- }
678
- }
679
-
680
- if (anyOf.length > 1) {
681
- schema.anyOf = anyOf
682
- } else {
683
- _.merge(schema, oneOf[0])
684
- }
685
- }
686
-
687
- _setConditionCompositionStyle(schema, condition, definitions, level) {
688
- if (condition.ref) {
689
- // If the condition is refering to other field, cannot use the `is` condition for composition
690
- // Simple choise between then / otherwise
691
- const oneOf = schema.oneOf || []
692
- if (condition.then) {
693
- oneOf.push(this.parse(condition.then, definitions, level + 1))
694
- }
695
- if (condition.otherwise) {
696
- oneOf.push(this.parse(condition.otherwise, definitions, level + 1))
697
- }
698
- if (oneOf.length > 1) {
699
- schema.oneOf = oneOf
700
- } else {
701
- _.merge(schema, oneOf[0])
702
- }
703
- } else {
704
- // Before Draft 7, you can express an “if-then” conditional using the Schema Composition keywords
705
- // and a boolean algebra concept called “implication”.
706
- // A -> B(pronounced, A implies B) means that if A is true, then B must also be true.
707
- // It can be expressed as !A || B
708
-
709
- // Variations of implication can express the same things you can express with the if/then/else keywords.
710
- // if/then can be expressed as A -> B, if/else can be expressed as !A -> B,
711
- // and if/then/else can be expressed as A -> B AND !A -> C
712
- if (condition.is && condition.then && condition.otherwise) {
713
- schema.allOf = [
714
- {
715
- anyOf: [
716
- {
717
- not: this.parse(condition.is, definitions, level + 1)
718
- },
719
- this.parse(condition.then, definitions, level + 1)
720
- ]
721
- },
722
- {
723
- anyOf: [
724
- this.parse(condition.is, definitions, level + 1),
725
- this.parse(condition.otherwise, definitions, level + 1)
726
- ]
727
- }
728
- ]
729
- } else if (condition.is && condition.then) {
730
- schema.anyOf = [
731
- {
732
- not: this.parse(condition.is, definitions, level + 1)
733
- },
734
- this.parse(condition.then, definitions, level + 1)
735
- ]
736
- }
737
- }
738
-
739
- return schema
740
- }
741
-
742
- _setConst(schema, fieldDefn) {
743
- const enumList = fieldDefn[this.enumFieldName]
744
- const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
745
- if (fieldDefn.flags && fieldDefn.flags.only && _.size(filteredEnumList) === 1) {
746
- schema.const = filteredEnumList[0]
747
- delete schema.type
748
- }
749
- }
750
-
751
- _setAnyProperties(schema) {
752
- if (schema.type !== 'any') {
753
- return
754
- }
755
-
756
- schema.type = [
757
- 'array',
758
- 'boolean',
759
- 'number',
760
- 'object',
761
- 'string',
762
- 'null'
763
- ]
764
- }
765
-
766
- _setMetaProperties(schema, joiSpec) {
767
- _.forEach(joiSpec.metas, (m) => {
768
- if (m.contentMediaType) {
769
- schema.contentMediaType = m.contentMediaType
770
- }
771
- if (m.format) {
772
- schema.format = m.format
773
- }
774
- if (m.title) {
775
- schema.title = m.title
776
- }
777
- })
778
- }
779
-
780
- _setLinkFieldProperties(schema, joiSpec) {
781
- if (schema.type !== 'link') {
782
- return
783
- }
784
-
785
- if (_.get(joiSpec, 'link.ref.type') === 'local') {
786
- schema.$ref = `${this._getLocalSchemaBasePath()}/${joiSpec.link.ref.path.join('/')}`
787
- delete schema.type
788
- }
789
- }
790
-
791
- _copyMetasToSchema(joiSpec, schema) {
792
- if (!_.isEmpty(joiSpec.metas)) {
793
- _.each(joiSpec.metas, meta => {
794
- _.each(meta, (value, key) => {
795
- if (this._isKnownMetaKey(key)) {
796
- schema[key] = value
797
- }
798
- })
799
- })
800
- }
801
- }
802
-
803
- // Intended to be overridden by child parsers. By default no meta key will be
804
- // is considered "known".
805
- // eslint-disable-next-line no-unused-vars
806
- _isKnownMetaKey(key) {
807
- return false
808
- }
809
- }
810
-
811
- module.exports = JoiJsonSchemaParser
1
+ /* eslint no-use-before-define: 'off' */
2
+ const _ = require('lodash')
3
+ const combinations = require('combinations')
4
+
5
+ /**
6
+ * Default Joi logical operator (or/and/nand/xor/oxor) parsers
7
+ */
8
+ const LOGICAL_OP_PARSER = {
9
+ or: function (schema, dependency) {
10
+ schema.anyOf = _.map(dependency.peers, (peer) => {
11
+ return { required: [peer] }
12
+ })
13
+ },
14
+ and: function (schema, dependency) {
15
+ schema.oneOf = [{ required: dependency.peers }]
16
+ schema.oneOf.push({
17
+ allOf: _.map(dependency.peers, (peer) => {
18
+ return { not: { required: [peer] } }
19
+ })
20
+ })
21
+ },
22
+ nand: function (schema, dependency) {
23
+ schema.not = { required: dependency.peers }
24
+ },
25
+ xor: function (schema, dependency) {
26
+ schema.if = {
27
+ propertyNames: { enum: dependency.peers },
28
+ minProperties: 2
29
+ }
30
+ schema.then = false
31
+ schema.else = {
32
+ oneOf: _.map(dependency.peers, (peer) => {
33
+ return { required: [peer] }
34
+ })
35
+ }
36
+ },
37
+ oxor: function (schema, dependency) {
38
+ schema.oneOf = _.map(dependency.peers, (peer) => {
39
+ return { required: [peer] }
40
+ })
41
+ schema.oneOf.push({
42
+ not: {
43
+ oneOf: _.map(combinations(dependency.peers, 1, 2), (combination) => {
44
+ return { required: combination }
45
+ })
46
+ }
47
+ })
48
+ },
49
+ with: function (schema, dependency) {
50
+ schema.dependentRequired = schema.dependentRequired || {}
51
+ schema.dependentRequired[dependency.key] = dependency.peers
52
+ },
53
+ without: function (schema, dependency) {
54
+ schema.if = { required: [dependency.key] }
55
+ schema.then = {
56
+ not: {
57
+ anyOf: _.map(dependency.peers, (peer) => {
58
+ return { required: [peer] }
59
+ })
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Recognize the `joi.override` representation in `describe()` output.
67
+ *
68
+ * `joi.override` is a Symbol that can be used in `joi.any().valid(…)`
69
+ * statements, to reset the list of valid values. In `describe()` output, it
70
+ * turns up as an object with 1 property:
71
+ *
72
+ * ```
73
+ * { override: true }
74
+ * ```
75
+ */
76
+ function isJoiOverride(e) {
77
+ return typeof e === 'object'
78
+ && e !== null
79
+ && Object.keys(e).length === 1
80
+ && e.override === true
81
+ }
82
+
83
+ class JoiJsonSchemaParser {
84
+ constructor(opts = {}) {
85
+ this.$schema = opts.$schema || 'http://json-schema.org/draft-07/schema#'
86
+ this.includeSchemaDialect = opts.includeSchemaDialect || false
87
+ this.childrenFieldName = this._getChildrenFieldName()
88
+ this.optionsFieldName = this._getOptionsFieldName()
89
+ this.ruleArgFieldName = this._getRuleArgFieldName()
90
+ this.enumFieldName = this._getEnumFieldName()
91
+ this.allowUnknownFlagName = this._getAllowUnknownFlagName()
92
+ this.inheritedPreferences = []
93
+
94
+ if (opts.logicalOpParser === false) {
95
+ this.logicalOpParser = {}
96
+ } else {
97
+ this.logicalOpParser = _.merge({}, LOGICAL_OP_PARSER, opts.logicalOpParser)
98
+ }
99
+ }
100
+
101
+ parse(joiSpec, definitions = {}, level = 0) {
102
+ let schema = {}
103
+
104
+ const hasPreferences = this._pushInheritiedPreferences(joiSpec)
105
+
106
+ if (this._getPresence(joiSpec) === 'forbidden') {
107
+ if (hasPreferences) {
108
+ this._popInheritiedPreferences()
109
+ }
110
+ schema.not = {}
111
+ return schema
112
+ }
113
+
114
+ this._setBasicProperties(schema, joiSpec)
115
+ this._setNumberFieldProperties(schema, joiSpec)
116
+ this._setBinaryFieldProperties(schema, joiSpec)
117
+ this._setStringFieldProperties(schema, joiSpec)
118
+ this._setDateFieldProperties(schema, joiSpec)
119
+ this._setArrayFieldProperties(schema, joiSpec, definitions, level)
120
+ this._setObjectProperties(schema, joiSpec, definitions, level)
121
+ this._setAlternativesProperties(schema, joiSpec, definitions, level)
122
+ this._setConditionProperties(schema, joiSpec, definitions, level, 'whens')
123
+ this._setMetaProperties(schema, joiSpec)
124
+ this._setLinkFieldProperties(schema, joiSpec)
125
+ this._setConst(schema, joiSpec)
126
+ this._setAnyProperties(schema, joiSpec, definitions, level)
127
+ this._addNullTypeIfNullable(schema, joiSpec)
128
+
129
+ if (!_.isEmpty(joiSpec.shared)) {
130
+ _.forEach(joiSpec.shared, (sharedSchema) => {
131
+ this.parse(sharedSchema, definitions, level)
132
+ })
133
+ }
134
+
135
+ this._copyMetasToSchema(joiSpec, schema)
136
+
137
+ // Last step to remove any unknown keys that are not in the JSON Schema specification or known meta keys
138
+ schema = this._retainKnownKeys(schema)
139
+
140
+ const schemaId = _.get(joiSpec, 'flags.id')
141
+ if (schemaId) {
142
+ definitions[schemaId] = schema
143
+ schema = {
144
+ $ref: `${this._getLocalSchemaBasePath()}/${schemaId}`
145
+ }
146
+ }
147
+ if (level === 0 && !_.isEmpty(definitions)) {
148
+ _.set(schema, `${this._getLocalSchemaBasePath().replace('#/', '').replace(/\//, '.')}`, definitions)
149
+ }
150
+
151
+ if (this.includeSchemaDialect && level === 0 && this.$schema) {
152
+ schema.$schema = this.$schema
153
+ }
154
+
155
+ if (hasPreferences) {
156
+ this._popInheritiedPreferences()
157
+ }
158
+ return schema
159
+ }
160
+
161
+ _retainKnownKeys(schema) {
162
+ if (!this.knownKeys) {
163
+ return schema
164
+ }
165
+
166
+ return _.pickBy(schema, (value, key) => this.knownKeys.includes(key) || this._isKnownMetaKey(key))
167
+ }
168
+
169
+ _pushInheritiedPreferences(joiSpec) {
170
+ if (joiSpec.preferences) {
171
+ this.inheritedPreferences.push(_.merge({}, this._getInheritedPreferences(), joiSpec.preferences))
172
+ return true
173
+ }
174
+ return false
175
+ }
176
+
177
+ _popInheritiedPreferences() {
178
+ this.inheritedPreferences.pop()
179
+ }
180
+
181
+ _getInheritedPreferences() {
182
+ return this.inheritedPreferences[this.inheritedPreferences.length - 1] || {}
183
+ }
184
+
185
+ _isIfThenElseSupported() {
186
+ return true
187
+ }
188
+
189
+ _getChildrenFieldName() {
190
+ return 'keys'
191
+ }
192
+
193
+ _getOptionsFieldName() {
194
+ return 'preferences'
195
+ }
196
+
197
+ _getRuleArgFieldName() {
198
+ return 'args'
199
+ }
200
+
201
+ _getEnumFieldName() {
202
+ return 'allow'
203
+ }
204
+
205
+ _getAllowUnknownFlagName() {
206
+ return 'unknown'
207
+ }
208
+
209
+ _getLocalSchemaBasePath() {
210
+ return '#/$defs'
211
+ }
212
+
213
+ _getFieldDescription(fieldDefn) {
214
+ return _.get(fieldDefn, 'flags.description')
215
+ }
216
+
217
+ _getFieldType(fieldDefn) {
218
+ let type = fieldDefn.type
219
+ if (type === 'number' && !_.isEmpty(fieldDefn.rules) &&
220
+ fieldDefn.rules[0].name === 'integer') {
221
+ type = 'integer'
222
+ }
223
+ return type
224
+ }
225
+
226
+ _addNullTypeIfNullable(fieldSchema, fieldDefn) {
227
+ // This should always be the last call in parse
228
+ if (fieldSchema.const === null) {
229
+ return
230
+ }
231
+
232
+ const enums = _.get(fieldDefn, this.enumFieldName)
233
+ if (Array.isArray(enums) && enums.includes(null)) {
234
+ if (Array.isArray(fieldSchema.type)) {
235
+ if (!fieldSchema.type.includes('null')) {
236
+ fieldSchema.type.push('null')
237
+ }
238
+ } else if (fieldSchema.type) {
239
+ fieldSchema.type = [fieldSchema.type, 'null']
240
+ } else {
241
+ fieldSchema.type = 'null'
242
+ }
243
+ }
244
+ }
245
+
246
+ _getFieldExample(fieldDefn) {
247
+ return _.get(fieldDefn, 'examples')
248
+ }
249
+
250
+ _getPresence(fieldDefn) {
251
+ const presence = _.get(fieldDefn, 'flags.presence')
252
+ if (presence !== undefined) {
253
+ return presence
254
+ }
255
+ const preferences = fieldDefn[this.optionsFieldName] || this._getInheritedPreferences()
256
+ return _.get(preferences, 'presence')
257
+ }
258
+
259
+ _isRequired(fieldDefn) {
260
+ const presence = this._getPresence(fieldDefn)
261
+ return presence === 'required'
262
+ }
263
+
264
+ _getDefaultValue(fieldDefn) {
265
+ return _.get(fieldDefn, 'flags.default')
266
+ }
267
+
268
+ _getEnum(fieldDefn) {
269
+ const enumList = fieldDefn[this.enumFieldName]
270
+ const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
271
+ if (fieldDefn.flags && fieldDefn.flags.only && _.size(filteredEnumList) > 1) {
272
+ return _.uniq(filteredEnumList)
273
+ }
274
+ }
275
+
276
+ _getUnknown(joiSpec) {
277
+ const allowUnknown = _.get(joiSpec, `flags.${this.allowUnknownFlagName}`)
278
+ if (allowUnknown !== undefined) {
279
+ return allowUnknown
280
+ }
281
+ const preferences = joiSpec[this.optionsFieldName] || this._getInheritedPreferences()
282
+ return _.get(preferences, 'allowUnknown') || false
283
+ }
284
+
285
+ _setIfNotEmpty(schema, field, value) {
286
+ if (value !== null && value !== undefined) {
287
+ schema[field] = value
288
+ }
289
+ }
290
+
291
+ _setBasicProperties(fieldSchema, fieldDefn) {
292
+ this._setIfNotEmpty(fieldSchema, 'type', this._getFieldType(fieldDefn))
293
+ this._setIfNotEmpty(fieldSchema, 'examples', this._getFieldExample(fieldDefn))
294
+ this._setIfNotEmpty(fieldSchema, 'description', this._getFieldDescription(fieldDefn))
295
+ this._setIfNotEmpty(fieldSchema, 'default', this._getDefaultValue(fieldDefn))
296
+ this._setIfNotEmpty(fieldSchema, 'enum', this._getEnum(fieldDefn))
297
+ if (fieldDefn.invalid && fieldDefn.invalid.length > 0) {
298
+ fieldSchema.not = {
299
+ enum: fieldDefn.invalid
300
+ }
301
+ }
302
+ }
303
+
304
+ _setBinaryFieldProperties(fieldSchema, fieldDefn) {
305
+ if (fieldSchema.type !== 'binary') {
306
+ return
307
+ }
308
+ fieldSchema.type = 'string'
309
+ if (fieldDefn.flags && fieldDefn.flags.encoding) {
310
+ fieldSchema.contentEncoding = fieldDefn.flags.encoding
311
+ }
312
+ fieldSchema.format = 'binary'
313
+ }
314
+
315
+ _setObjectProperties(schema, joiSpec, definitions, level) {
316
+ if (schema.type !== 'object') {
317
+ return
318
+ }
319
+
320
+ schema.properties = {}
321
+ schema.required = []
322
+
323
+ schema.additionalProperties = this._getUnknown(joiSpec)
324
+
325
+ _.map(joiSpec[this.childrenFieldName], (fieldDefn, key) => {
326
+ const fieldSchema = this.parse(fieldDefn, definitions, level + 1)
327
+ if (this._isRequired(fieldDefn)) {
328
+ schema.required.push(key)
329
+ }
330
+
331
+ schema.properties[key] = fieldSchema
332
+ })
333
+
334
+ /**
335
+ * For dynamic key scenarios to store the pattern as key
336
+ * and have the properties be as with other examples
337
+ */
338
+ if (joiSpec.patterns) {
339
+ _.each(joiSpec.patterns, patternObj => {
340
+ if (typeof patternObj.rule !== 'object' || typeof patternObj.regex === 'undefined') {
341
+ return
342
+ }
343
+
344
+ schema.properties[patternObj.regex] = this.parse(patternObj.rule, definitions, level + 1)
345
+ schema.patternProperties = schema.patternProperties || {}
346
+
347
+ let regexString = patternObj.regex
348
+ regexString = regexString.indexOf('/') === 0 ? regexString.substring(1) : regexString
349
+ regexString = regexString.lastIndexOf('/') > -1 ? regexString.substring(0, regexString.length - 1) : regexString
350
+
351
+ schema.patternProperties[regexString] = schema.properties[patternObj.regex]
352
+ })
353
+ }
354
+
355
+ if (_.isEmpty(schema.required)) {
356
+ delete schema.required
357
+ }
358
+
359
+ this._setObjectDependencies(schema, joiSpec)
360
+ }
361
+
362
+ _setObjectDependencies(schema, joiSpec) {
363
+ if (!_.isArray(joiSpec.dependencies) || joiSpec.dependencies.length === 0) {
364
+ return
365
+ }
366
+
367
+ if (joiSpec.dependencies.length === 1) {
368
+ this._setDependencySubSchema(schema, joiSpec.dependencies[0])
369
+ } else {
370
+ const withDependencies = _.remove(joiSpec.dependencies, (dependency) => {
371
+ return dependency.rel === 'with'
372
+ })
373
+
374
+ schema.allOf = _.compact(_.map(joiSpec.dependencies, (dependency) => {
375
+ const subSchema = this._setDependencySubSchema({}, dependency)
376
+ if (_.isEmpty(subSchema)) {
377
+ return null
378
+ }
379
+ return subSchema
380
+ }))
381
+
382
+ _.each(withDependencies, (dependency) => {
383
+ this._setDependencySubSchema(schema, dependency)
384
+ })
385
+
386
+ if (schema.allOf.length === 0) {
387
+ // When the logicalOpParser is set to false
388
+ delete schema.allOf
389
+ }
390
+ }
391
+ }
392
+
393
+ _setDependencySubSchema(schema, dependency) {
394
+ const opParser = this.logicalOpParser[dependency.rel]
395
+ if (typeof opParser !== 'function') {
396
+ return schema
397
+ }
398
+ opParser(schema, dependency)
399
+ return schema
400
+ }
401
+
402
+ _setNumberFieldProperties(fieldSchema, fieldDefn) {
403
+ if (fieldSchema.type !== 'number' && fieldSchema.type !== 'integer') {
404
+ return
405
+ }
406
+
407
+ const ruleArgFieldName = this.ruleArgFieldName
408
+
409
+ _.each(fieldDefn.rules, (rule) => {
410
+ const value = rule[ruleArgFieldName]
411
+ switch (rule.name) {
412
+ case 'max':
413
+ fieldSchema.maximum = value.limit
414
+ break
415
+ case 'min':
416
+ fieldSchema.minimum = value.limit
417
+ break
418
+ case 'greater':
419
+ fieldSchema.exclusiveMinimum = value.limit
420
+ fieldSchema.minimum = value.limit
421
+ break
422
+ case 'less':
423
+ fieldSchema.exclusiveMaximum = value.limit
424
+ fieldSchema.maximum = value.limit
425
+ break
426
+ case 'multiple':
427
+ fieldSchema.multipleOf = value.base
428
+ break
429
+ case 'sign':
430
+ if (rule.args.sign === 'positive') {
431
+ fieldSchema.exclusiveMinimum = 0
432
+ } else if (rule.args.sign === 'negative') {
433
+ fieldSchema.exclusiveMaximum = 0
434
+ }
435
+ break
436
+ default:
437
+ break
438
+ }
439
+ })
440
+ }
441
+
442
+ _setStringFieldProperties(fieldSchema, fieldDefn) {
443
+ if (fieldSchema.type !== 'string') {
444
+ return
445
+ }
446
+
447
+ if (fieldDefn.flags && fieldDefn.flags.encoding) {
448
+ fieldSchema.contentEncoding = fieldDefn.flags.encoding
449
+ }
450
+
451
+ const ruleArgFieldName = this.ruleArgFieldName
452
+
453
+ _.forEach(fieldDefn.rules, (rule) => {
454
+ switch (rule.name) {
455
+ case 'min':
456
+ fieldSchema.minLength = rule[ruleArgFieldName].limit
457
+ break
458
+ case 'max':
459
+ fieldSchema.maxLength = rule[ruleArgFieldName].limit
460
+ break
461
+ case 'email':
462
+ fieldSchema.format = 'email'
463
+ break
464
+ case 'hostname':
465
+ fieldSchema.format = 'hostname'
466
+ break
467
+ case 'uri':
468
+ fieldSchema.format = 'uri'
469
+ break
470
+ case 'ip':
471
+ const versions = rule[ruleArgFieldName].options.version
472
+ if (!_.isEmpty(versions)) {
473
+ if (versions.length === 1) {
474
+ fieldSchema.format = versions[0]
475
+ } else {
476
+ fieldSchema.oneOf = _.map(versions, (version) => {
477
+ return {
478
+ format: version
479
+ }
480
+ })
481
+ }
482
+ } else {
483
+ fieldSchema.format = 'ipv4'
484
+ }
485
+ break
486
+ case 'pattern':
487
+ let regex = rule[ruleArgFieldName].regex
488
+ let idx = regex.indexOf('/')
489
+ if (idx === 0) {
490
+ regex = regex.replace('/', '')
491
+ }
492
+ idx = regex.lastIndexOf('/') === regex.length - 1
493
+ if (idx > -1) {
494
+ regex = regex.replace(/\/$/, '')
495
+ }
496
+ fieldSchema.pattern = regex
497
+ break
498
+ case 'isoDate':
499
+ fieldSchema.format = 'date-time'
500
+ break
501
+ case 'isoDuration':
502
+ fieldSchema.format = 'duration'
503
+ break
504
+ case 'uuid':
505
+ case 'guid':
506
+ fieldSchema.format = 'uuid'
507
+ break
508
+ default:
509
+ break
510
+ }
511
+ })
512
+ }
513
+
514
+ _setArrayFieldProperties(fieldSchema, fieldDefn, definitions, level) {
515
+ if (fieldSchema.type !== 'array') {
516
+ return
517
+ }
518
+
519
+ const ruleArgFieldName = this.ruleArgFieldName
520
+
521
+ _.each(fieldDefn.rules, (rule) => {
522
+ const value = rule[ruleArgFieldName]
523
+ switch (rule.name) {
524
+ case 'max':
525
+ fieldSchema.maxItems = value.limit
526
+ break
527
+ case 'min':
528
+ fieldSchema.minItems = value.limit
529
+ break
530
+ case 'length':
531
+ fieldSchema.maxItems = value.limit
532
+ fieldSchema.minItems = value.limit
533
+ break
534
+ case 'unique':
535
+ fieldSchema.uniqueItems = true
536
+ break
537
+ case 'has':
538
+ fieldSchema.contains = this.parse(value.schema, definitions, level + 1)
539
+ break
540
+ default:
541
+ break
542
+ }
543
+ })
544
+
545
+ if (!fieldDefn.items) {
546
+ fieldSchema.items = {}
547
+ return
548
+ }
549
+
550
+ if (fieldDefn.items.length === 1) {
551
+ fieldSchema.items = this.parse(fieldDefn.items[0], definitions, level + 1)
552
+ } else {
553
+ fieldSchema.items = {
554
+ anyOf: _.map(fieldDefn.items, (itemSchema) => {
555
+ return this.parse(itemSchema, definitions, level + 1)
556
+ })
557
+ }
558
+ }
559
+ }
560
+
561
+ _setDateFieldProperties(fieldSchema, fieldDefn) {
562
+ if (fieldSchema.type !== 'date') {
563
+ return
564
+ }
565
+
566
+ if (fieldDefn.flags && fieldDefn.flags.format !== 'iso') {
567
+ fieldSchema.type = 'integer'
568
+ } else {
569
+ // https://datatracker.ietf.org/doc/draft-handrews-json-schema-validation
570
+ // JSON Schema does not have date type, but use string with format.
571
+ // However, joi definition cannot clearly tells the date/time/date-time format
572
+ fieldSchema.type = 'string'
573
+ fieldSchema.format = 'date-time'
574
+ }
575
+ }
576
+
577
+ _setAlternativesProperties(schema, joiSpec, definitions, level) {
578
+ if (schema.type !== 'alternatives' || joiSpec.matches.length === 0) {
579
+ return
580
+ }
581
+
582
+ if (joiSpec.matches[0].schema) {
583
+ // try style
584
+ let mode = 'anyOf'
585
+ if (joiSpec.flags && joiSpec.flags.match === 'one') {
586
+ mode = 'oneOf'
587
+ } else if (joiSpec.flags && joiSpec.flags.match === 'all') {
588
+ mode = 'allOf'
589
+ }
590
+
591
+ schema[mode] = _.map(joiSpec.matches, (match) => {
592
+ return this.parse(match.schema, definitions, level + 1)
593
+ })
594
+
595
+ if (schema[mode].length === 1) {
596
+ _.merge(schema, schema[mode][0])
597
+ delete schema[mode]
598
+ }
599
+ } else {
600
+ this._setConditionProperties(schema, joiSpec, definitions, level, 'matches', 'anyOf')
601
+ }
602
+
603
+ if (schema.type === 'alternatives') {
604
+ delete schema.type
605
+ }
606
+ }
607
+
608
+ _setConditionProperties(schema, joiSpec, definitions, level, conditionFieldName, logicKeyword = 'allOf') {
609
+ if (!joiSpec[conditionFieldName] || joiSpec[conditionFieldName].length === 0) {
610
+ return
611
+ }
612
+
613
+ let ifThenStyle = this._isIfThenElseSupported()
614
+ const styleSetting = _.remove(joiSpec.metas, (meta) => {
615
+ return typeof meta['if-style'] !== 'undefined'
616
+ })
617
+
618
+ if (ifThenStyle && styleSetting.length > 0 && styleSetting[0]['if-style'] === false) {
619
+ ifThenStyle = false
620
+ }
621
+
622
+ if (joiSpec[conditionFieldName].length > 1) {
623
+ // Multiple case
624
+ schema[logicKeyword] = _.map(joiSpec[conditionFieldName], (condition) => {
625
+ return this._setConditionSchema(ifThenStyle, {}, condition, definitions, level + 1)
626
+ })
627
+ } else {
628
+ this._setConditionSchema(ifThenStyle, schema, joiSpec[conditionFieldName][0], definitions, level + 1)
629
+ }
630
+ }
631
+
632
+ _setConditionSchema(ifThenStyle, schema, conditionJoiSpec, definitions, level) {
633
+ // When "if" is not present, both "then" and "else" MUST be entirely ignored.
634
+ // There must be either "is" or "switch"
635
+ if (!conditionJoiSpec.is && !conditionJoiSpec.switch) {
636
+ return
637
+ }
638
+
639
+ if (conditionJoiSpec.switch) {
640
+ this._parseSwitchCondition(ifThenStyle, schema, conditionJoiSpec, definitions, level)
641
+ } else {
642
+ if (ifThenStyle) {
643
+ this._parseIfThenElseCondition(schema, conditionJoiSpec, definitions, level)
644
+ } else {
645
+ this._setConditionCompositionStyle(schema, conditionJoiSpec, definitions, level)
646
+ }
647
+ }
648
+
649
+ return schema
650
+ }
651
+
652
+ _parseIfThenElseCondition(schema, conditionSpec, definitions, level) {
653
+ if (conditionSpec.ref) {
654
+ // Currently, if there is reference, if-then-else style is not supported
655
+ // To use it, the condition must be defined in parent level using schema-style is
656
+ return this._setConditionCompositionStyle(schema, conditionSpec, definitions, level)
657
+ }
658
+
659
+ schema.if = this.parse(conditionSpec.is, definitions, level)
660
+
661
+ if (conditionSpec.then) {
662
+ schema.then = this.parse(conditionSpec.then, definitions, level)
663
+ }
664
+ if (conditionSpec.otherwise) {
665
+ schema.else = this.parse(conditionSpec.otherwise, definitions, level)
666
+ }
667
+ return schema
668
+ }
669
+
670
+ _parseSwitchCondition(ifThenStyle, schema, condition, definitions, level) {
671
+ // Switch cannot be used if the joi condition is a schema
672
+ // Hence, condition.ref should always exists
673
+ const anyOf = []
674
+ for (const switchCondition of condition.switch) {
675
+ if (ifThenStyle && !condition.ref) {
676
+ const innerSchema = this._parseIfThenElseCondition(
677
+ {}, switchCondition, definitions, level + 1)
678
+
679
+ anyOf.push(innerSchema)
680
+ } else {
681
+ if (switchCondition.then) {
682
+ anyOf.push(this.parse(switchCondition.then, definitions, level + 1))
683
+ }
684
+
685
+ if (switchCondition.otherwise) {
686
+ anyOf.push(this.parse(switchCondition.otherwise, definitions, level + 1))
687
+ }
688
+ }
689
+ }
690
+
691
+ if (anyOf.length > 1) {
692
+ schema.anyOf = anyOf
693
+ } else {
694
+ _.merge(schema, oneOf[0])
695
+ }
696
+ }
697
+
698
+ _setConditionCompositionStyle(schema, condition, definitions, level) {
699
+ if (condition.ref) {
700
+ // If the condition is refering to other field, cannot use the `is` condition for composition
701
+ // Simple choise between then / otherwise
702
+ const oneOf = schema.oneOf || []
703
+ if (condition.then) {
704
+ oneOf.push(this.parse(condition.then, definitions, level + 1))
705
+ }
706
+ if (condition.otherwise) {
707
+ oneOf.push(this.parse(condition.otherwise, definitions, level + 1))
708
+ }
709
+ if (oneOf.length > 1) {
710
+ schema.oneOf = oneOf
711
+ } else {
712
+ _.merge(schema, oneOf[0])
713
+ }
714
+ } else {
715
+ // Before Draft 7, you can express an “if-then” conditional using the Schema Composition keywords
716
+ // and a boolean algebra concept called “implication”.
717
+ // A -> B(pronounced, A implies B) means that if A is true, then B must also be true.
718
+ // It can be expressed as !A || B
719
+
720
+ // Variations of implication can express the same things you can express with the if/then/else keywords.
721
+ // if/then can be expressed as A -> B, if/else can be expressed as !A -> B,
722
+ // and if/then/else can be expressed as A -> B AND !A -> C
723
+ if (condition.is && condition.then && condition.otherwise) {
724
+ schema.allOf = [
725
+ {
726
+ anyOf: [
727
+ {
728
+ not: this.parse(condition.is, definitions, level + 1)
729
+ },
730
+ this.parse(condition.then, definitions, level + 1)
731
+ ]
732
+ },
733
+ {
734
+ anyOf: [
735
+ this.parse(condition.is, definitions, level + 1),
736
+ this.parse(condition.otherwise, definitions, level + 1)
737
+ ]
738
+ }
739
+ ]
740
+ } else if (condition.is && condition.then) {
741
+ schema.anyOf = [
742
+ {
743
+ not: this.parse(condition.is, definitions, level + 1)
744
+ },
745
+ this.parse(condition.then, definitions, level + 1)
746
+ ]
747
+ }
748
+ }
749
+
750
+ return schema
751
+ }
752
+
753
+ _setConst(schema, fieldDefn) {
754
+ const enumList = fieldDefn[this.enumFieldName]
755
+ const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
756
+ if (fieldDefn.flags && fieldDefn.flags.only && _.size(filteredEnumList) === 1) {
757
+ schema.const = filteredEnumList[0]
758
+ delete schema.type
759
+ }
760
+ }
761
+
762
+ _setAnyProperties(schema) {
763
+ if (schema.type !== 'any') {
764
+ return
765
+ }
766
+
767
+ schema.type = [
768
+ 'array',
769
+ 'boolean',
770
+ 'number',
771
+ 'object',
772
+ 'string',
773
+ 'null'
774
+ ]
775
+ }
776
+
777
+ _setMetaProperties(schema, joiSpec) {
778
+ _.forEach(joiSpec.metas, (m) => {
779
+ if (m.contentMediaType) {
780
+ schema.contentMediaType = m.contentMediaType
781
+ }
782
+ if (m.format) {
783
+ schema.format = m.format
784
+ }
785
+ if (m.title) {
786
+ schema.title = m.title
787
+ }
788
+ })
789
+ }
790
+
791
+ _setLinkFieldProperties(schema, joiSpec) {
792
+ if (schema.type !== 'link') {
793
+ return
794
+ }
795
+
796
+ if (_.get(joiSpec, 'link.ref.type') === 'local') {
797
+ schema.$ref = `${this._getLocalSchemaBasePath()}/${joiSpec.link.ref.path.join('/')}`
798
+ delete schema.type
799
+ }
800
+ }
801
+
802
+ _copyMetasToSchema(joiSpec, schema) {
803
+ if (!_.isEmpty(joiSpec.metas)) {
804
+ _.each(joiSpec.metas, meta => {
805
+ _.each(meta, (value, key) => {
806
+ if (this._isKnownMetaKey(key)) {
807
+ schema[key] = value
808
+ }
809
+ })
810
+ })
811
+ }
812
+ }
813
+
814
+ // Intended to be overridden by child parsers. By default no meta key will be
815
+ // is considered "known".
816
+ // eslint-disable-next-line no-unused-vars
817
+ _isKnownMetaKey(key) {
818
+ return false
819
+ }
820
+ }
821
+
822
+ module.exports = JoiJsonSchemaParser