joi-to-json 4.3.0 → 4.3.2

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