joi-to-json 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ### 3.0.0
2
+
3
+ - [BREAKING] Default enable feature: relation operator `and`, `or`, `nand`, `xor`, `oxor`.
package/README.md CHANGED
@@ -95,6 +95,153 @@ const jsonSchema = parse(joiSchema)
95
95
  // const openApiSchema = parse(joiSchema, 'open-api')
96
96
  ```
97
97
 
98
+ ### Joi to OpenAPI
99
+
100
+ Most Joi specifications result in the expected OpenAPI schema.
101
+
102
+ E.g.,
103
+
104
+ ```js
105
+ const joi = require('joi')
106
+ const { dump } = require('js-yaml')
107
+ const { writeFile } = require('fs/promises')
108
+
109
+ const joiSchema = joi.object().keys({
110
+ uuid: joi.string().uuid({ version: ['uuidv3', 'uuidv5'] }),
111
+ nickName: joi.string().required().example('鹄思乱想').description('Hero Nickname').min(3).max(20).pattern(/^[a-z]+$/, { name: 'alpha', invert: true }),
112
+ avatar: joi.string().required().uri(),
113
+ email: joi.string().email(),
114
+ ip: joi.string().ip({ version: ['ipv4', 'ipv6'] }),
115
+ hostname: joi.string().hostname().insensitive(),
116
+ gender: joi.string().valid('Male', 'Female', '', null).default('Male'),
117
+ isoDateString: joi.string().isoDate(),
118
+ isoDurationString: joi.string().isoDuration(),
119
+ birthday: joi.date().iso(),
120
+ certificate: joi.binary().encoding('base64'),
121
+ tags: joi.array().items(joi.string().required()).length(2),
122
+ nested: joi.object().keys({
123
+ key: joi.string()
124
+ }).unknown(true)
125
+ }).unknown(false)
126
+
127
+ async function writeYAML(targetPath) {
128
+ const openApiSchema = parse(joiSchema, 'open-api')
129
+
130
+ const openApiSchemaYAML = dump(openApiSchema, {lineWidth: 120, noCompatMode: true})
131
+ await writeFile(targetPath, openApiSchemaYAML)
132
+ }
133
+ ```
134
+
135
+ results in
136
+
137
+ ```yaml
138
+ type: object
139
+ required:
140
+ - nickName
141
+ - avatar
142
+ properties:
143
+ uuid:
144
+ type: string
145
+ format: uuid
146
+ nickName:
147
+ description: Hero Nickname
148
+ type: string
149
+ pattern: ^[a-z]+$
150
+ minLength: 3,
151
+ maxLength: 20,
152
+ example: 鹄思乱想
153
+ avatar:
154
+ type: string
155
+ format: uri
156
+ email:
157
+ type: string
158
+ format: email
159
+ ip:
160
+ type: string
161
+ oneOf:
162
+ - format: ipv4
163
+ - format: ipv6
164
+ hostname:
165
+ type: string
166
+ format: hostname
167
+ gender:
168
+ type: string
169
+ default: Male
170
+ enum:
171
+ - Male
172
+ - Female
173
+ - ''
174
+ - null
175
+ nullable: true
176
+ isoDateString:
177
+ type: string
178
+ format: date-time
179
+ isoDurationString:
180
+ type: string
181
+ format: duration
182
+ birthday:
183
+ type: string
184
+ format: date-time
185
+ certificate:
186
+ type: string
187
+ format: binary
188
+ tags:
189
+ type: array
190
+ items:
191
+ type: string
192
+ minItems: 2
193
+ maxItems: 2
194
+ nested:
195
+ type: object
196
+ properties:
197
+ key:
198
+ type: string
199
+ additionalProperties: true
200
+ additionalProperties: false
201
+ ```
202
+
203
+ Some OpenAPI features are not supported directly in Joi, but Joi schemas can be annotated with `joi.any().meta({…})`
204
+ to get them in the OpenAPI schema:
205
+
206
+ ```js
207
+
208
+
209
+ const joiSchema = joi.object().keys({
210
+ deprecatedProperty: joi.string().meta({ deprecated: true }).required(),
211
+ readOnlyProperty: joi.string().meta({ readOnly: true }),
212
+ writeOnlyProperty: joi.string().meta({ writeOnly: true }),
213
+ xMeta: joi.string().meta({ 'x-meta': 42 }),
214
+ unknownMetaProperty: joi.string().meta({ unknownMeta: 42 })
215
+ }).unknown(true)
216
+
217
+
218
+ ```
219
+
220
+ begets:
221
+
222
+ ```yaml
223
+ type: object
224
+ required:
225
+ - deprecatedProperty
226
+ properties:
227
+ deprecatedProperty:
228
+ type: string
229
+ deprecated: true
230
+ readOnlyProperty:
231
+ type: string
232
+ readOnly: true
233
+ writeOnlyProperty:
234
+ type: string
235
+ writeOnly: true
236
+ xMeta:
237
+ type: string
238
+ x-meta: 42
239
+ unknownMetaProperty:
240
+ type: string
241
+ # unknownMeta is not exported
242
+ additionalProperties: true
243
+ ```
244
+
98
245
  ## Browser support
99
246
  For generating JSON Schema in a browser you should use below import syntax for `joi` library in order to work because the `joi` browser minimized build does not have `describe` api which the `joi-to-json` relies on.
100
247
 
@@ -102,6 +249,10 @@ For generating JSON Schema in a browser you should use below import syntax for `
102
249
  import Joi from 'joi/lib/index';
103
250
  ```
104
251
 
252
+ ## Special Joi Operator Support
253
+
254
+ * [Logical Relation Operator](./docs/logical_rel_support.md)
255
+
105
256
  ## Test
106
257
 
107
258
  >npm run test
package/index.js CHANGED
@@ -22,7 +22,7 @@ const parsers = {
22
22
  'open-api': JoiOpenApiSchemaParser
23
23
  }
24
24
 
25
- function parse(joiObj, type = 'json', definitions = {}) {
25
+ function parse(joiObj, type = 'json', definitions = {}, parserOptions = {}) {
26
26
  if (typeof joiObj.describe !== 'function') {
27
27
  throw new Error('Not an joi object.')
28
28
  }
@@ -59,7 +59,7 @@ function parse(joiObj, type = 'json', definitions = {}) {
59
59
  throw new Error(`No parser is registered for ${type}`)
60
60
  }
61
61
 
62
- return new parser().parse(joiBaseSpec, definitions)
62
+ return new parser(parserOptions).parse(joiBaseSpec, definitions)
63
63
  }
64
64
 
65
65
  module.exports = parse
@@ -1,247 +1,258 @@
1
- const _ = require('lodash')
2
-
3
- class JoiSpecConvertor {
4
- constructor() {}
5
-
6
- static getVersion(joiObj) {
7
- return joiObj._currentJoi.version
8
- }
9
-
10
- static getSupportVersion() {
11
- return '12'
12
- }
13
-
14
- _convertObject(joiObj) {
15
- if (joiObj.children) {
16
- joiObj.keys = joiObj.children
17
- delete joiObj.children
18
- }
19
-
20
- _.each(joiObj.keys, (value, _key) => {
21
- this.toBaseSpec(value)
22
- })
23
-
24
- _.each(joiObj.patterns, (pattern, _idx) => {
25
- this.toBaseSpec(pattern.rule)
26
- })
27
- }
28
-
29
- _convertDate(joiObj) {
30
- if (joiObj.flags.timestamp) {
31
- joiObj.flags.format = joiObj.flags.timestamp
32
- delete joiObj.flags.timestamp
33
- delete joiObj.flags.multiplier
34
- } else if (joiObj.flags.format = {}) {
35
- joiObj.flags.format = 'iso'
36
- }
37
- }
38
-
39
- _convertAlternatives(joiObj) {
40
- let isWhenCase = false
41
- if (joiObj.alternatives.length === 1) {
42
- const condition = joiObj.alternatives[0]
43
- isWhenCase = !!condition.then || !!condition.otherwise
44
- }
45
-
46
- if (isWhenCase) {
47
- // when case
48
- joiObj.type = 'any'
49
- joiObj.whens = joiObj.alternatives
50
-
51
- if (joiObj.flags.presence === 'ignore') {
52
- // FIXME: Not sure when this flag is set.
53
- delete joiObj.flags.presence
54
- }
55
- if (joiObj.whens.peek) {
56
- joiObj.whens.is = joiObj.whens.peek
57
- delete joiObj.whens.peek
58
- }
59
-
60
- joiObj.whens[0] = _.mapValues(joiObj.whens[0], (value, condition) => {
61
- switch (condition) {
62
- case 'is':
63
- this.toBaseSpec(value)
64
- value.type = 'any'
65
- // FIXME: Not sure when this is set.
66
- value.allow.splice(0, 0, { 'override': true })
67
- break
68
- case 'otherwise':
69
- this.toBaseSpec(value)
70
- break
71
- case 'then':
72
- this.toBaseSpec(value)
73
- break
74
- case 'ref':
75
- if (value.indexOf('ref:') === 0) {
76
- value = {
77
- path: value.replace('ref:', '').split('.')
78
- }
79
- }
80
- break
81
- }
82
- return value
83
- })
84
-
85
- delete joiObj.alternatives
86
- delete joiObj.base
87
- } else {
88
- joiObj.matches = _.map(joiObj.alternatives, (alternative) => {
89
- return {
90
- schema: this.toBaseSpec(alternative)
91
- }
92
- })
93
- delete joiObj.alternatives
94
- }
95
- }
96
-
97
- _convertString(joiObj) {
98
- delete joiObj.options
99
-
100
- _.each(joiObj.rules, (rule, _idx) => {
101
- const name = rule.name
102
- if (['min', 'max'].includes(name)) {
103
- rule.args = {
104
- limit: rule.arg
105
- }
106
- } else if (['lowercase', 'uppercase'].includes(name)) {
107
- rule.name = 'case'
108
- rule.args = {
109
- direction: name.replace('case', '')
110
- }
111
- delete joiObj.flags.case
112
- } else if (rule.arg) {
113
- rule.args = {
114
- options: rule.arg
115
- }
116
- }
117
- if (name === 'regex') {
118
- rule.name = 'pattern'
119
- rule.args.regex = rule.arg.pattern.toString()
120
- delete rule.args.options.pattern
121
- }
122
- delete rule.arg
123
- })
124
- }
125
-
126
- _convertNumber(joiObj) {
127
- _.each(joiObj.rules, (rule, _idx) => {
128
- const name = rule.name
129
- if (['positive', 'negative'].includes(name)) {
130
- rule.args = {
131
- sign: name
132
- }
133
- rule.name = 'sign'
134
- }
135
- if (['greater', 'less', 'min', 'max', 'precision'].includes(name)) {
136
- rule.args = {
137
- limit: rule.arg
138
- }
139
- }
140
- if (name === 'multiple') {
141
- rule.args = {
142
- base: rule.arg
143
- }
144
- }
145
- delete rule.arg
146
- })
147
-
148
- if (joiObj.flags.precision) {
149
- delete joiObj.flags.precision
150
- }
151
- }
152
-
153
- _convertBoolean(joiObj) {
154
- if (joiObj.flags.insensitive === false) {
155
- joiObj.flags.sensitive = true
156
- }
157
- delete joiObj.flags.insensitive
158
- }
159
-
160
- _convertArray(joiObj) {
161
- if (joiObj.flags.sparse === false) {
162
- delete joiObj.flags.sparse
163
- }
164
-
165
- _.each(joiObj.rules, (rule, _idx) => {
166
- const name = rule.name
167
- if (['min', 'max', 'length'].includes(name)) {
168
- rule.args = {
169
- limit: rule.arg
170
- }
171
- }
172
- delete rule.arg
173
- })
174
-
175
- _.each(joiObj.items, (item, _idx) => {
176
- this.toBaseSpec(item)
177
- })
178
- }
179
-
180
- _convertExamples(_joiObj) {
181
- }
182
-
183
- toBaseSpec(joiObj) {
184
- joiObj.flags = joiObj.flags || {}
185
-
186
- if (joiObj.flags.allowOnly) {
187
- joiObj.flags.only = joiObj.flags.allowOnly
188
- delete joiObj.flags.allowOnly
189
- }
190
- if (joiObj.flags.allowUnknown) {
191
- joiObj.flags.unknown = joiObj.flags.allowUnknown
192
- delete joiObj.flags.allowUnknown
193
- }
194
- if (joiObj.description) {
195
- joiObj.flags.description = joiObj.description
196
- delete joiObj.description
197
- }
198
- if (joiObj.options) {
199
- joiObj.preferences = joiObj.options
200
- delete joiObj.options
201
- }
202
- if (joiObj.valids) {
203
- joiObj.allow = joiObj.valids
204
- delete joiObj.valids
205
- }
206
- if (joiObj.meta) {
207
- joiObj.metas = joiObj.meta
208
- delete joiObj.meta
209
- }
210
- delete joiObj.invalids
211
-
212
- this._convertExamples(joiObj)
213
-
214
- switch (joiObj.type) {
215
- case 'object':
216
- this._convertObject(joiObj)
217
- break
218
- case 'date':
219
- this._convertDate(joiObj)
220
- break
221
- case 'alternatives':
222
- this._convertAlternatives(joiObj)
223
- break
224
- case 'string':
225
- this._convertString(joiObj)
226
- break
227
- case 'number':
228
- this._convertNumber(joiObj)
229
- break
230
- case 'boolean':
231
- this._convertBoolean(joiObj)
232
- break
233
- case 'array':
234
- this._convertArray(joiObj)
235
- break
236
- default:
237
- break
238
- }
239
- if (_.isEmpty(joiObj.flags)) {
240
- delete joiObj.flags
241
- }
242
-
243
- return joiObj
244
- }
245
- }
246
-
247
- module.exports = JoiSpecConvertor
1
+ const _ = require('lodash')
2
+
3
+ class JoiSpecConvertor {
4
+ constructor() {}
5
+
6
+ static getVersion(joiObj) {
7
+ return joiObj._currentJoi.version
8
+ }
9
+
10
+ static getSupportVersion() {
11
+ return '12'
12
+ }
13
+
14
+ _convertObject(joiObj) {
15
+ if (joiObj.children) {
16
+ joiObj.keys = joiObj.children
17
+ delete joiObj.children
18
+ }
19
+
20
+ _.each(joiObj.keys, (value, _key) => {
21
+ this.toBaseSpec(value)
22
+ })
23
+
24
+ _.each(joiObj.patterns, (pattern, _idx) => {
25
+ this.toBaseSpec(pattern.rule)
26
+ })
27
+
28
+ this._convertDependencies(joiObj)
29
+ }
30
+
31
+ _convertDependencies(joiObj) {
32
+ _.each(joiObj.dependencies, (dependency) => {
33
+ if (dependency.type) {
34
+ dependency.rel = dependency.type
35
+ delete dependency.type
36
+ }
37
+ })
38
+ }
39
+
40
+ _convertDate(joiObj) {
41
+ if (joiObj.flags.timestamp) {
42
+ joiObj.flags.format = joiObj.flags.timestamp
43
+ delete joiObj.flags.timestamp
44
+ delete joiObj.flags.multiplier
45
+ } else if (joiObj.flags.format = {}) {
46
+ joiObj.flags.format = 'iso'
47
+ }
48
+ }
49
+
50
+ _convertAlternatives(joiObj) {
51
+ let isWhenCase = false
52
+ if (joiObj.alternatives.length === 1) {
53
+ const condition = joiObj.alternatives[0]
54
+ isWhenCase = !!condition.then || !!condition.otherwise
55
+ }
56
+
57
+ if (isWhenCase) {
58
+ // when case
59
+ joiObj.type = 'any'
60
+ joiObj.whens = joiObj.alternatives
61
+
62
+ if (joiObj.flags.presence === 'ignore') {
63
+ // FIXME: Not sure when this flag is set.
64
+ delete joiObj.flags.presence
65
+ }
66
+ if (joiObj.whens.peek) {
67
+ joiObj.whens.is = joiObj.whens.peek
68
+ delete joiObj.whens.peek
69
+ }
70
+
71
+ joiObj.whens[0] = _.mapValues(joiObj.whens[0], (value, condition) => {
72
+ switch (condition) {
73
+ case 'is':
74
+ this.toBaseSpec(value)
75
+ value.type = 'any'
76
+ // FIXME: Not sure when this is set.
77
+ value.allow.splice(0, 0, { 'override': true })
78
+ break
79
+ case 'otherwise':
80
+ this.toBaseSpec(value)
81
+ break
82
+ case 'then':
83
+ this.toBaseSpec(value)
84
+ break
85
+ case 'ref':
86
+ if (value.indexOf('ref:') === 0) {
87
+ value = {
88
+ path: value.replace('ref:', '').split('.')
89
+ }
90
+ }
91
+ break
92
+ }
93
+ return value
94
+ })
95
+
96
+ delete joiObj.alternatives
97
+ delete joiObj.base
98
+ } else {
99
+ joiObj.matches = _.map(joiObj.alternatives, (alternative) => {
100
+ return {
101
+ schema: this.toBaseSpec(alternative)
102
+ }
103
+ })
104
+ delete joiObj.alternatives
105
+ }
106
+ }
107
+
108
+ _convertString(joiObj) {
109
+ delete joiObj.options
110
+
111
+ _.each(joiObj.rules, (rule, _idx) => {
112
+ const name = rule.name
113
+ if (['min', 'max'].includes(name)) {
114
+ rule.args = {
115
+ limit: rule.arg
116
+ }
117
+ } else if (['lowercase', 'uppercase'].includes(name)) {
118
+ rule.name = 'case'
119
+ rule.args = {
120
+ direction: name.replace('case', '')
121
+ }
122
+ delete joiObj.flags.case
123
+ } else if (rule.arg) {
124
+ rule.args = {
125
+ options: rule.arg
126
+ }
127
+ }
128
+ if (name === 'regex') {
129
+ rule.name = 'pattern'
130
+ rule.args.regex = rule.arg.pattern.toString()
131
+ delete rule.args.options.pattern
132
+ }
133
+ delete rule.arg
134
+ })
135
+ }
136
+
137
+ _convertNumber(joiObj) {
138
+ _.each(joiObj.rules, (rule, _idx) => {
139
+ const name = rule.name
140
+ if (['positive', 'negative'].includes(name)) {
141
+ rule.args = {
142
+ sign: name
143
+ }
144
+ rule.name = 'sign'
145
+ }
146
+ if (['greater', 'less', 'min', 'max', 'precision'].includes(name)) {
147
+ rule.args = {
148
+ limit: rule.arg
149
+ }
150
+ }
151
+ if (name === 'multiple') {
152
+ rule.args = {
153
+ base: rule.arg
154
+ }
155
+ }
156
+ delete rule.arg
157
+ })
158
+
159
+ if (joiObj.flags.precision) {
160
+ delete joiObj.flags.precision
161
+ }
162
+ }
163
+
164
+ _convertBoolean(joiObj) {
165
+ if (joiObj.flags.insensitive === false) {
166
+ joiObj.flags.sensitive = true
167
+ }
168
+ delete joiObj.flags.insensitive
169
+ }
170
+
171
+ _convertArray(joiObj) {
172
+ if (joiObj.flags.sparse === false) {
173
+ delete joiObj.flags.sparse
174
+ }
175
+
176
+ _.each(joiObj.rules, (rule, _idx) => {
177
+ const name = rule.name
178
+ if (['min', 'max', 'length'].includes(name)) {
179
+ rule.args = {
180
+ limit: rule.arg
181
+ }
182
+ }
183
+ delete rule.arg
184
+ })
185
+
186
+ _.each(joiObj.items, (item, _idx) => {
187
+ this.toBaseSpec(item)
188
+ })
189
+ }
190
+
191
+ _convertExamples(_joiObj) {
192
+ }
193
+
194
+ toBaseSpec(joiObj) {
195
+ joiObj.flags = joiObj.flags || {}
196
+
197
+ if (joiObj.flags.allowOnly) {
198
+ joiObj.flags.only = joiObj.flags.allowOnly
199
+ delete joiObj.flags.allowOnly
200
+ }
201
+ if (joiObj.flags.allowUnknown) {
202
+ joiObj.flags.unknown = joiObj.flags.allowUnknown
203
+ delete joiObj.flags.allowUnknown
204
+ }
205
+ if (joiObj.description) {
206
+ joiObj.flags.description = joiObj.description
207
+ delete joiObj.description
208
+ }
209
+ if (joiObj.options) {
210
+ joiObj.preferences = joiObj.options
211
+ delete joiObj.options
212
+ }
213
+ if (joiObj.valids) {
214
+ joiObj.allow = joiObj.valids
215
+ delete joiObj.valids
216
+ }
217
+ if (joiObj.meta) {
218
+ joiObj.metas = joiObj.meta
219
+ delete joiObj.meta
220
+ }
221
+ delete joiObj.invalids
222
+
223
+ this._convertExamples(joiObj)
224
+
225
+ switch (joiObj.type) {
226
+ case 'object':
227
+ this._convertObject(joiObj)
228
+ break
229
+ case 'date':
230
+ this._convertDate(joiObj)
231
+ break
232
+ case 'alternatives':
233
+ this._convertAlternatives(joiObj)
234
+ break
235
+ case 'string':
236
+ this._convertString(joiObj)
237
+ break
238
+ case 'number':
239
+ this._convertNumber(joiObj)
240
+ break
241
+ case 'boolean':
242
+ this._convertBoolean(joiObj)
243
+ break
244
+ case 'array':
245
+ this._convertArray(joiObj)
246
+ break
247
+ default:
248
+ break
249
+ }
250
+ if (_.isEmpty(joiObj.flags)) {
251
+ delete joiObj.flags
252
+ }
253
+
254
+ return joiObj
255
+ }
256
+ }
257
+
258
+ module.exports = JoiSpecConvertor
@@ -1,21 +1,25 @@
1
- const _ = require('lodash')
2
- const JoiJsonSchemaParser = require('./json')
3
-
4
- class JoiJsonDraftSchemaParser extends JoiJsonSchemaParser {
5
- _setNumberFieldProperties(fieldSchema, fieldDefn) {
6
- super._setNumberFieldProperties(fieldSchema, fieldDefn)
7
-
8
- if (typeof fieldSchema.minimum !== 'undefined' && fieldSchema.minimum === fieldSchema.exclusiveMinimum) {
9
- fieldSchema.exclusiveMinimum = true
10
- }
11
- if (typeof fieldSchema.maximum !== 'undefined' && fieldSchema.maximum === fieldSchema.exclusiveMaximum) {
12
- fieldSchema.exclusiveMaximum = true
13
- }
14
- }
15
-
16
- _getLocalSchemaBasePath() {
17
- return '#/definitions'
18
- }
19
- }
20
-
21
- module.exports = JoiJsonDraftSchemaParser
1
+ const _ = require('lodash')
2
+ const JoiJsonSchemaParser = require('./json')
3
+
4
+ class JoiJsonDraftSchemaParser extends JoiJsonSchemaParser {
5
+ constructor(opts = {}) {
6
+ super(_.merge({ logicalOpParser: { xor: null } }, opts))
7
+ }
8
+
9
+ _setNumberFieldProperties(fieldSchema, fieldDefn) {
10
+ super._setNumberFieldProperties(fieldSchema, fieldDefn)
11
+
12
+ if (typeof fieldSchema.minimum !== 'undefined' && fieldSchema.minimum === fieldSchema.exclusiveMinimum) {
13
+ fieldSchema.exclusiveMinimum = true
14
+ }
15
+ if (typeof fieldSchema.maximum !== 'undefined' && fieldSchema.maximum === fieldSchema.exclusiveMaximum) {
16
+ fieldSchema.exclusiveMaximum = true
17
+ }
18
+ }
19
+
20
+ _getLocalSchemaBasePath() {
21
+ return '#/definitions'
22
+ }
23
+ }
24
+
25
+ module.exports = JoiJsonDraftSchemaParser
@@ -1,13 +1,84 @@
1
1
  /* eslint no-use-before-define: 'off' */
2
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
+ }
50
+
51
+ /**
52
+ * Recognize the `joi.override` representation in `describe()` output.
53
+ *
54
+ * `joi.override` is a Symbol that can be used in `joi.any().valid(…)`
55
+ * statements, to reset the list of valid values. In `describe()` output, it
56
+ * turns up as an object with 1 property:
57
+ *
58
+ * ```
59
+ * { override: true }
60
+ * ```
61
+ */
62
+ function isJoiOverride(e) {
63
+ return typeof e === 'object'
64
+ && e !== null
65
+ && Object.keys(e).length === 1
66
+ && e.override === true
67
+ }
3
68
 
4
69
  class JoiJsonSchemaParser {
5
- constructor() {
70
+ constructor(opts = {}) {
6
71
  this.childrenFieldName = this._getChildrenFieldName()
7
72
  this.optionsFieldName = this._getOptionsFieldName()
8
73
  this.ruleArgFieldName = this._getRuleArgFieldName()
9
74
  this.enumFieldName = this._getEnumFieldName()
10
75
  this.allowUnknownFlagName = this._getAllowUnknownFlagName()
76
+
77
+ if (opts.logicalOpParser === false) {
78
+ this.logicalOpParser = {}
79
+ } else {
80
+ this.logicalOpParser = _.merge({}, LOGICAL_OP_PARSER, opts.logicalOpParser)
81
+ }
11
82
  }
12
83
 
13
84
  parse(joiSpec, definitions = {}, level = 0) {
@@ -124,8 +195,9 @@ class JoiJsonSchemaParser {
124
195
 
125
196
  _getEnum(fieldDefn) {
126
197
  const enumList = fieldDefn[this.enumFieldName]
127
- if (fieldDefn.flags && fieldDefn.flags.only && _.size(enumList) > 1) {
128
- return _.uniq(enumList)
198
+ const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
199
+ if (fieldDefn.flags && fieldDefn.flags.only && _.size(filteredEnumList) > 1) {
200
+ return _.uniq(filteredEnumList)
129
201
  }
130
202
  }
131
203
 
@@ -222,6 +294,39 @@ class JoiJsonSchemaParser {
222
294
  if (_.isEmpty(schema.required)) {
223
295
  delete schema.required
224
296
  }
297
+
298
+ this._setObjectDependencies(schema, joiSpec)
299
+ }
300
+
301
+ _setObjectDependencies(schema, joiSpec) {
302
+ if (!_.isArray(joiSpec.dependencies) || joiSpec.dependencies.length === 0) {
303
+ return
304
+ }
305
+
306
+ if (joiSpec.dependencies.length === 1) {
307
+ this._setDependencySubSchema(schema, joiSpec.dependencies[0])
308
+ } else {
309
+ schema.allOf = _.compact(_.map(joiSpec.dependencies, (dependency) => {
310
+ const subSchema = this._setDependencySubSchema({}, dependency)
311
+ if (_.isEmpty(subSchema)) {
312
+ return null
313
+ }
314
+ return subSchema
315
+ }))
316
+ if (schema.allOf.length === 0) {
317
+ // When the logicalOpParser is set to false
318
+ delete schema.allOf
319
+ }
320
+ }
321
+ }
322
+
323
+ _setDependencySubSchema(schema, dependency) {
324
+ const opParser = this.logicalOpParser[dependency.rel]
325
+ if (typeof opParser !== 'function') {
326
+ return schema
327
+ }
328
+ opParser(schema, dependency)
329
+ return schema
225
330
  }
226
331
 
227
332
  _setNumberFieldProperties(fieldSchema, fieldDefn) {
@@ -1,83 +1,83 @@
1
- const _ = require('lodash')
2
- const JoiJsonSchemaParser = require('./json-draft-04')
3
-
4
- class JoiOpenApiSchemaParser extends JoiJsonSchemaParser {
5
- parse(joiSpec, definitions = {}, level = 0) {
6
- const fullSchema = super.parse(joiSpec, definitions, level)
7
- const schema = _.pick(fullSchema, [
8
- '$ref',
9
- 'title',
10
- 'multipleOf',
11
- 'maximum',
12
- 'exclusiveMaximum',
13
- 'minimum',
14
- 'exclusiveMinimum',
15
- 'maxLength',
16
- 'minLength',
17
- 'pattern',
18
- 'maxItems',
19
- 'minItems',
20
- 'uniqueItems',
21
- 'maxProperties',
22
- 'minProperties',
23
- 'required',
24
- 'enum',
25
- 'description',
26
- 'format',
27
- 'default',
28
- 'type',
29
-
30
- 'allOf',
31
- 'oneOf',
32
- 'anyOf',
33
- 'not',
34
- 'items',
35
- 'properties',
36
- 'additionalProperties',
37
-
38
- 'example',
39
- 'nullable'
40
- ])
41
-
42
- if (!_.isEmpty(joiSpec.metas)) {
43
- _.each(joiSpec.metas, meta => {
44
- _.each(meta, (value, key) => {
45
- if (key.startsWith('x-') || key === 'deprecated' || key === 'readOnly' || key === 'writeOnly') {
46
- schema[key] = value
47
- }
48
- })
49
- })
50
- }
51
-
52
- if (level === 0 && !_.isEmpty(definitions)) {
53
- schema.schemas = definitions
54
- }
55
-
56
- if (fullSchema.const) {
57
- schema.enum = [fullSchema.const]
58
- }
59
-
60
- return schema
61
- }
62
-
63
- _getLocalSchemaBasePath() {
64
- return '#/components/schemas'
65
- }
66
-
67
- _setBasicProperties(fieldSchema, fieldDefn) {
68
- super._setBasicProperties(fieldSchema, fieldDefn)
69
-
70
- if (!_.isEmpty(fieldSchema.examples)) {
71
- fieldSchema.example = fieldSchema.examples[0]
72
- }
73
- }
74
-
75
- _addNullTypeIfNullable(fieldSchema, fieldDefn) {
76
- const enums = _.get(fieldDefn, this.enumFieldName)
77
- if (Array.isArray(enums) && enums.includes(null)) {
78
- fieldSchema.nullable = true
79
- }
80
- }
81
- }
82
-
83
- module.exports = JoiOpenApiSchemaParser
1
+ const _ = require('lodash')
2
+ const JoiJsonSchemaParser = require('./json-draft-04')
3
+
4
+ class JoiOpenApiSchemaParser extends JoiJsonSchemaParser {
5
+ parse(joiSpec, definitions = {}, level = 0) {
6
+ const fullSchema = super.parse(joiSpec, definitions, level)
7
+ const schema = _.pick(fullSchema, [
8
+ '$ref',
9
+ 'title',
10
+ 'multipleOf',
11
+ 'maximum',
12
+ 'exclusiveMaximum',
13
+ 'minimum',
14
+ 'exclusiveMinimum',
15
+ 'maxLength',
16
+ 'minLength',
17
+ 'pattern',
18
+ 'maxItems',
19
+ 'minItems',
20
+ 'uniqueItems',
21
+ 'maxProperties',
22
+ 'minProperties',
23
+ 'required',
24
+ 'enum',
25
+ 'description',
26
+ 'format',
27
+ 'default',
28
+ 'type',
29
+
30
+ 'allOf',
31
+ 'oneOf',
32
+ 'anyOf',
33
+ 'not',
34
+ 'items',
35
+ 'properties',
36
+ 'additionalProperties',
37
+
38
+ 'example',
39
+ 'nullable'
40
+ ])
41
+
42
+ if (!_.isEmpty(joiSpec.metas)) {
43
+ _.each(joiSpec.metas, meta => {
44
+ _.each(meta, (value, key) => {
45
+ if (key.startsWith('x-') || key === 'deprecated' || key === 'readOnly' || key === 'writeOnly') {
46
+ schema[key] = value
47
+ }
48
+ })
49
+ })
50
+ }
51
+
52
+ if (level === 0 && !_.isEmpty(definitions)) {
53
+ schema.schemas = definitions
54
+ }
55
+
56
+ if (fullSchema.const) {
57
+ schema.enum = [fullSchema.const]
58
+ }
59
+
60
+ return schema
61
+ }
62
+
63
+ _getLocalSchemaBasePath() {
64
+ return '#/components/schemas'
65
+ }
66
+
67
+ _setBasicProperties(fieldSchema, fieldDefn) {
68
+ super._setBasicProperties(fieldSchema, fieldDefn)
69
+
70
+ if (!_.isEmpty(fieldSchema.examples)) {
71
+ fieldSchema.example = fieldSchema.examples[0]
72
+ }
73
+ }
74
+
75
+ _addNullTypeIfNullable(fieldSchema, fieldDefn) {
76
+ const enums = _.get(fieldDefn, this.enumFieldName)
77
+ if (Array.isArray(enums) && enums.includes(null)) {
78
+ fieldSchema.nullable = true
79
+ }
80
+ }
81
+ }
82
+
83
+ module.exports = JoiOpenApiSchemaParser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joi-to-json",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "joi to JSON / OpenAPI Schema Converter",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -23,6 +23,7 @@
23
23
  "oas"
24
24
  ],
25
25
  "dependencies": {
26
+ "combinations": "^1.0.0",
26
27
  "lodash": "^4.17.21",
27
28
  "semver-compare": "^1.0.0"
28
29
  },