joi-to-json 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,13 @@ const jsonSchema = parse(joiSchema)
95
95
  // const openApiSchema = parse(joiSchema, 'open-api')
96
96
  ```
97
97
 
98
+ ## Browser support
99
+ 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
+
101
+ ```typescript
102
+ import Joi from 'joi/lib/index';
103
+ ```
104
+
98
105
  ## Test
99
106
 
100
107
  >npm run test
package/index.d.ts CHANGED
@@ -1,18 +1,18 @@
1
- import Joi from 'joi-17';
2
-
3
- declare module Joi2Json {
4
- /**
5
- * @type {string}
6
- */
7
- export type Mode = 'json' | 'open-api' | 'json-draft-2019-09' | 'json-draft-04';
8
-
9
- /**
10
- * @param {string} joi - A Joi schema.
11
- * @param {string} [mode='json'] - json / open-api / json-draft-2019-09 / json-draft-04
12
- * @param {Record} [sharedSchema={}] - Passed-in object storing shared schemas
13
- * @returns {any} Converted JSON schema object.
14
- */
15
- export function parse(joi: Joi.Schema, mode?: Mode, sharedSchema?: Record<string, any>): any;
16
- }
17
-
18
- export default Joi2Json.parse;
1
+ import Joi from 'joi-17';
2
+
3
+ declare module Joi2Json {
4
+ /**
5
+ * @type {string}
6
+ */
7
+ export type Mode = 'json' | 'open-api' | 'json-draft-2019-09' | 'json-draft-04';
8
+
9
+ /**
10
+ * @param {string} joi - A Joi schema.
11
+ * @param {string} [mode='json'] - json / open-api / json-draft-2019-09 / json-draft-04
12
+ * @param {Record} [sharedSchema={}] - Passed-in object storing shared schemas
13
+ * @returns {any} Converted JSON schema object.
14
+ */
15
+ export function parse(joi: Joi.Schema, mode?: Mode, sharedSchema?: Record<string, any>): any;
16
+ }
17
+
18
+ export default Joi2Json.parse;
package/index.js CHANGED
@@ -1,25 +1,26 @@
1
1
  const cmp = require('semver-compare')
2
- const fs = require('fs')
3
- const path = require('path')
4
2
 
5
- const convertorsDir = path.resolve(__dirname, './lib/convertors')
6
- const parsersDir = path.resolve(__dirname, './lib/parsers')
7
- const convertors = []
8
- const parsers = {}
3
+ const c17 = require('./lib/convertors/v17')
4
+ const c16 = require('./lib/convertors/v16')
5
+ const c15 = require('./lib/convertors/v15')
6
+ const c14 = require('./lib/convertors/v14')
7
+ const c13 = require('./lib/convertors/v13')
8
+ const c12 = require('./lib/convertors/v12')
9
9
 
10
- fs.readdirSync(convertorsDir).sort().reverse().forEach(file => {
11
- if (file.endsWith('.js')) {
12
- const convertor = require(`${convertorsDir}/${file}`)
13
- convertors.push(convertor)
14
- }
15
- })
10
+ const JoiJsonSchemaParser = require('./lib/parsers/json')
11
+ const JoiOpenApiSchemaParser = require('./lib/parsers/open-api')
12
+ const JoiJsonDraftSchemaParser19 = require('./lib/parsers/json-draft-2019-09')
13
+ const JoiJsonDraftSchemaParser = require('./lib/parsers/json-draft-04')
16
14
 
17
- fs.readdirSync(parsersDir).forEach(file => {
18
- if (file.endsWith('.js')) {
19
- const parser = require(`${parsersDir}/${file}`)
20
- parsers[file.split('.')[0]] = parser
21
- }
22
- })
15
+ const convertors = [
16
+ c17, c16, c15, c14, c13, c12
17
+ ]
18
+ const parsers = {
19
+ 'json-draft-2019-09': JoiJsonDraftSchemaParser19,
20
+ 'json-draft-4': JoiJsonDraftSchemaParser,
21
+ json: JoiJsonSchemaParser,
22
+ 'open-api': JoiOpenApiSchemaParser
23
+ }
23
24
 
24
25
  function parse(joiObj, type = 'json', definitions = {}) {
25
26
  if (typeof joiObj.describe !== 'function') {
@@ -1,484 +1,484 @@
1
- /* eslint no-use-before-define: 'off' */
2
- const _ = require('lodash')
3
-
4
- class JoiJsonSchemaParser {
5
- constructor() {
6
- this.childrenFieldName = this._getChildrenFieldName()
7
- this.optionsFieldName = this._getOptionsFieldName()
8
- this.ruleArgFieldName = this._getRuleArgFieldName()
9
- this.enumFieldName = this._getEnumFieldName()
10
- this.allowUnknownFlagName = this._getAllowUnknownFlagName()
11
- }
12
-
13
- parse(joiSpec, definitions = {}, level = 0) {
14
- let schema = {}
15
-
16
- if (this._getPresence(joiSpec) === 'forbidden') {
17
- schema.not = {}
18
- return schema
19
- }
20
-
21
- this._setBasicProperties(schema, joiSpec)
22
- this._setNumberFieldProperties(schema, joiSpec)
23
- this._setBinaryFieldProperties(schema, joiSpec)
24
- this._setStringFieldProperties(schema, joiSpec)
25
- this._setDateFieldProperties(schema, joiSpec)
26
- this._setArrayFieldProperties(schema, joiSpec, definitions, level)
27
- this._setObjectProperties(schema, joiSpec, definitions, level)
28
- this._setAlternativesProperties(schema, joiSpec, definitions, level)
29
- this._setAnyProperties(schema, joiSpec, definitions, level)
30
- this._addNullTypeIfNullable(schema, joiSpec)
31
- this._setMetaProperties(schema, joiSpec)
32
- this._setLinkFieldProperties(schema, joiSpec)
33
-
34
- if (!_.isEmpty(joiSpec.shared)) {
35
- this.parse(joiSpec.shared[0], definitions, level)
36
- }
37
-
38
- const schemaId = _.get(joiSpec, 'flags.id')
39
- if (schemaId) {
40
- definitions[schemaId] = schema
41
- schema = {
42
- $ref: `${this._getLocalSchemaBasePath()}/${schemaId}`
43
- }
44
- }
45
- if (level === 0 && !_.isEmpty(definitions)) {
46
- _.set(schema, `${this._getLocalSchemaBasePath().replace('#/', '').replace(/\//, '.')}`, definitions)
47
- }
48
-
49
- return schema
50
- }
51
-
52
- _getChildrenFieldName() {
53
- return 'keys'
54
- }
55
-
56
- _getOptionsFieldName() {
57
- return 'preferences'
58
- }
59
-
60
- _getRuleArgFieldName() {
61
- return 'args'
62
- }
63
-
64
- _getEnumFieldName() {
65
- return 'allow'
66
- }
67
-
68
- _getAllowUnknownFlagName() {
69
- return 'unknown'
70
- }
71
-
72
- _getLocalSchemaBasePath() {
73
- return '#/$defs'
74
- }
75
-
76
- _getFieldDescription(fieldDefn) {
77
- return _.get(fieldDefn, 'flags.description')
78
- }
79
-
80
- _getFieldType(fieldDefn) {
81
- let type = fieldDefn.type
82
- if (type === 'number' && !_.isEmpty(fieldDefn.rules) &&
83
- fieldDefn.rules[0].name === 'integer') {
84
- type = 'integer'
85
- }
86
- return type
87
- }
88
-
89
- _addNullTypeIfNullable(fieldSchema, fieldDefn) {
90
- // This should always be the last call in parse
91
- const enums = _.get(fieldDefn, this.enumFieldName)
92
- if (Array.isArray(enums) && enums.includes(null)) {
93
- fieldSchema.type = [fieldSchema.type, 'null']
94
- }
95
- }
96
-
97
- _getFieldExample(fieldDefn) {
98
- return _.get(fieldDefn, 'examples')
99
- }
100
-
101
- _getPresence(fieldDefn) {
102
- const presence = _.get(fieldDefn, 'flags.presence')
103
- if (presence !== undefined) {
104
- return presence
105
- }
106
- return _.get(fieldDefn, `${this.optionsFieldName}.presence`)
107
- }
108
-
109
- _isRequired(fieldDefn) {
110
- const presence = this._getPresence(fieldDefn)
111
- return presence === 'required'
112
- }
113
-
114
- _getDefaultValue(fieldDefn) {
115
- return _.get(fieldDefn, 'flags.default')
116
- }
117
-
118
- _getConst(fieldDefn) {
119
- const enumList = fieldDefn[this.enumFieldName]
120
- if (fieldDefn.flags && fieldDefn.flags.only && _.size(enumList) === 1) {
121
- return enumList[0]
122
- }
123
- }
124
-
125
- _getEnum(fieldDefn) {
126
- const enumList = fieldDefn[this.enumFieldName]
127
- if (fieldDefn.flags && fieldDefn.flags.only && _.size(enumList) > 1) {
128
- return _.uniq(enumList)
129
- }
130
- }
131
-
132
- _getUnknown(joiSpec) {
133
- let allowUnknown = _.get(joiSpec, `${this.optionsFieldName}.allowUnknown`, false)
134
- if (joiSpec.flags && typeof joiSpec.flags[this.allowUnknownFlagName] !== 'undefined') {
135
- allowUnknown = joiSpec.flags[this.allowUnknownFlagName]
136
- }
137
- return allowUnknown
138
- }
139
-
140
- _setIfNotEmpty(schema, field, value) {
141
- if (value !== null && value !== undefined) {
142
- schema[field] = value
143
- }
144
- }
145
-
146
- _setBasicProperties(fieldSchema, fieldDefn) {
147
- this._setIfNotEmpty(fieldSchema, 'type', this._getFieldType(fieldDefn))
148
- this._setIfNotEmpty(fieldSchema, 'examples', this._getFieldExample(fieldDefn))
149
- this._setIfNotEmpty(fieldSchema, 'description', this._getFieldDescription(fieldDefn))
150
- this._setIfNotEmpty(fieldSchema, 'default', this._getDefaultValue(fieldDefn))
151
- this._setIfNotEmpty(fieldSchema, 'const', this._getConst(fieldDefn))
152
- this._setIfNotEmpty(fieldSchema, 'enum', this._getEnum(fieldDefn))
153
- }
154
-
155
- _setBinaryFieldProperties(fieldSchema, fieldDefn) {
156
- if (fieldSchema.type !== 'binary') {
157
- return
158
- }
159
- fieldSchema.type = 'string'
160
- if (fieldDefn.flags && fieldDefn.flags.encoding) {
161
- fieldSchema.contentEncoding = fieldDefn.flags.encoding
162
- }
163
- fieldSchema.format = 'binary'
164
- }
165
-
166
- _setObjectProperties(schema, joiSpec, definitions, level) {
167
- if (schema.type !== 'object') {
168
- return
169
- }
170
-
171
- schema.properties = {}
172
- schema.required = []
173
-
174
- schema.additionalProperties = this._getUnknown(joiSpec)
175
-
176
- _.map(joiSpec[this.childrenFieldName], (fieldDefn, key) => {
177
- const fieldSchema = this.parse(fieldDefn, definitions, level + 1)
178
- if (this._isRequired(fieldDefn)) {
179
- schema.required.push(key)
180
- }
181
-
182
- schema.properties[key] = fieldSchema
183
- })
184
-
185
- /**
186
- * For dynamic key scenarios to store the pattern as key
187
- * and have the properties be as with other examples
188
- */
189
- if (joiSpec.patterns) {
190
- _.each(joiSpec.patterns, patternObj => {
191
- if (typeof patternObj.rule !== 'object' || typeof patternObj.regex === 'undefined') {
192
- return
193
- }
194
-
195
- schema.properties[patternObj.regex] = {
196
- type: patternObj.rule.type,
197
- properties: {}
198
- }
199
- schema.properties[patternObj.regex].required = []
200
-
201
- const childKeys = patternObj.rule.keys || patternObj.rule.children
202
- schema.properties[patternObj.regex].additionalProperties = this._getUnknown(patternObj.rule)
203
-
204
- _.each(childKeys, (ruleObj, key) => {
205
- schema.properties[patternObj.regex].properties[key] = this.parse(ruleObj, definitions, level + 1)
206
-
207
- if (this._isRequired(ruleObj)) {
208
- schema.properties[patternObj.regex].required.push(key)
209
- }
210
- })
211
-
212
- schema.patternProperties = schema.patternProperties || {}
213
-
214
- let regexString = patternObj.regex
215
- regexString = regexString.indexOf('/') === 0 ? regexString.substring(1) : regexString
216
- regexString = regexString.lastIndexOf('/') > -1 ? regexString.substring(0, regexString.length - 1) : regexString
217
-
218
- schema.patternProperties[regexString] = schema.properties[patternObj.regex]
219
- })
220
- }
221
-
222
- if (_.isEmpty(schema.required)) {
223
- delete schema.required
224
- }
225
- }
226
-
227
- _setNumberFieldProperties(fieldSchema, fieldDefn) {
228
- if (fieldSchema.type !== 'number' && fieldSchema.type !== 'integer') {
229
- return
230
- }
231
-
232
- const ruleArgFieldName = this.ruleArgFieldName
233
-
234
- _.each(fieldDefn.rules, (rule) => {
235
- const value = rule[ruleArgFieldName]
236
- switch (rule.name) {
237
- case 'max':
238
- fieldSchema.maximum = value.limit
239
- break
240
- case 'min':
241
- fieldSchema.minimum = value.limit
242
- break
243
- case 'greater':
244
- fieldSchema.exclusiveMinimum = value.limit
245
- fieldSchema.minimum = value.limit
246
- break
247
- case 'less':
248
- fieldSchema.exclusiveMaximum = value.limit
249
- fieldSchema.maximum = value.limit
250
- break
251
- case 'multiple':
252
- fieldSchema.multipleOf = value.base
253
- break
254
- default:
255
- break
256
- }
257
- })
258
- }
259
-
260
- _setStringFieldProperties(fieldSchema, fieldDefn) {
261
- if (fieldSchema.type !== 'string') {
262
- return
263
- }
264
-
265
- if (fieldDefn.flags && fieldDefn.flags.encoding) {
266
- fieldSchema.contentEncoding = fieldDefn.flags.encoding
267
- }
268
-
269
- const ruleArgFieldName = this.ruleArgFieldName
270
-
271
- _.forEach(fieldDefn.rules, (rule) => {
272
- switch (rule.name) {
273
- case 'min':
274
- fieldSchema.minLength = rule[ruleArgFieldName].limit
275
- break
276
- case 'max':
277
- fieldSchema.maxLength = rule[ruleArgFieldName].limit
278
- break
279
- case 'email':
280
- fieldSchema.format = 'email'
281
- break
282
- case 'hostname':
283
- fieldSchema.format = 'hostname'
284
- break
285
- case 'uri':
286
- fieldSchema.format = 'uri'
287
- break
288
- case 'ip':
289
- const versions = rule[ruleArgFieldName].options.version
290
- if (!_.isEmpty(versions)) {
291
- if (versions.length === 1) {
292
- fieldSchema.format = versions[0]
293
- } else {
294
- fieldSchema.oneOf = _.map(versions, (version) => {
295
- return {
296
- format: version
297
- }
298
- })
299
- }
300
- } else {
301
- fieldSchema.format = 'ipv4'
302
- }
303
- break
304
- case 'pattern':
305
- let regex = rule[ruleArgFieldName].regex
306
- let idx = regex.indexOf('/')
307
- if (idx === 0) {
308
- regex = regex.replace('/', '')
309
- }
310
- idx = regex.lastIndexOf('/') === regex.length - 1
311
- if (idx > -1) {
312
- regex = regex.replace(/\/$/, '')
313
- }
314
- fieldSchema.pattern = regex
315
- break
316
- case 'isoDate':
317
- fieldSchema.format = 'date-time'
318
- break
319
- case 'isoDuration':
320
- fieldSchema.format = 'duration'
321
- break
322
- case 'uuid':
323
- case 'guid':
324
- fieldSchema.format = 'uuid'
325
- break
326
- default:
327
- break
328
- }
329
- })
330
- }
331
-
332
- _setArrayFieldProperties(fieldSchema, fieldDefn, definitions, level) {
333
- if (fieldSchema.type !== 'array') {
334
- return
335
- }
336
-
337
- const ruleArgFieldName = this.ruleArgFieldName
338
-
339
- _.each(fieldDefn.rules, (rule) => {
340
- const value = rule[ruleArgFieldName]
341
- switch (rule.name) {
342
- case 'max':
343
- fieldSchema.maxItems = value.limit
344
- break
345
- case 'min':
346
- fieldSchema.minItems = value.limit
347
- break
348
- case 'length':
349
- fieldSchema.maxItems = value.limit
350
- fieldSchema.minItems = value.limit
351
- break
352
- case 'unique':
353
- fieldSchema.uniqueItems = true
354
- break
355
- default:
356
- break
357
- }
358
- })
359
-
360
- if (!fieldDefn.items) {
361
- fieldSchema.items = {}
362
- return
363
- }
364
-
365
- if (fieldDefn.items.length === 1) {
366
- fieldSchema.items = this.parse(fieldDefn.items[0], definitions, level + 1)
367
- } else {
368
- fieldSchema.items = {
369
- anyOf: _.map(fieldDefn.items, (itemSchema) => {
370
- return this.parse(itemSchema, definitions, level + 1)
371
- })
372
- }
373
- }
374
- }
375
-
376
- _setDateFieldProperties(fieldSchema, fieldDefn) {
377
- if (fieldSchema.type !== 'date') {
378
- return
379
- }
380
-
381
- if (fieldDefn.flags && fieldDefn.flags.format !== 'iso') {
382
- fieldSchema.type = 'integer'
383
- } else {
384
- // https://datatracker.ietf.org/doc/draft-handrews-json-schema-validation
385
- // JSON Schema does not have date type, but use string with format.
386
- // However, joi definition cannot clearly tells the date/time/date-time format
387
- fieldSchema.type = 'string'
388
- fieldSchema.format = 'date-time'
389
- }
390
- }
391
-
392
- _setAlternativesProperties(schema, joiSpec, definitions, level) {
393
- if (schema.type !== 'alternatives') {
394
- return
395
- }
396
-
397
- if (joiSpec.matches.length === 1) {
398
- const match = joiSpec.matches[0]
399
- if (match.switch) {
400
- schema.oneOf = _.map(match.switch, (condition) => {
401
- return this.parse(condition.then || condition.otherwise, definitions, level + 1)
402
- })
403
- } else if (match.then || match.otherwise) {
404
- schema.oneOf = []
405
- if (match.then) schema.oneOf.push(this.parse(match.then, definitions, level + 1))
406
- if (match.otherwise) schema.oneOf.push(this.parse(match.otherwise, definitions, level + 1))
407
- }
408
- } else {
409
- schema.oneOf = _.map(joiSpec.matches, (match) => {
410
- return this.parse(match.schema, definitions, level + 1)
411
- })
412
- }
413
-
414
- delete schema.type
415
- }
416
-
417
- _setAnyProperties(schema, joiSpec, definitions, level) {
418
- if (schema.type !== 'any') {
419
- return
420
- }
421
-
422
- if (joiSpec.whens) {
423
- schema.oneOf = []
424
-
425
- const condition = joiSpec.whens[0]
426
-
427
- if (condition.switch) {
428
- for (const switchCondition of condition.switch) {
429
- if (switchCondition.then) {
430
- schema.oneOf.push(this.parse(switchCondition.then, definitions, level + 1))
431
- }
432
-
433
- if (switchCondition.otherwise) {
434
- schema.oneOf.push(this.parse(switchCondition.otherwise, definitions, level + 1))
435
- }
436
- }
437
- }
438
-
439
- if (condition.then) {
440
- schema.oneOf.push(this.parse(condition.then, definitions, level + 1))
441
- }
442
-
443
- if (condition.otherwise) {
444
- schema.oneOf.push(this.parse(condition.otherwise, definitions, level + 1))
445
- }
446
-
447
- delete schema.type
448
- return
449
- }
450
-
451
- schema.type = [
452
- 'array',
453
- 'boolean',
454
- 'number',
455
- 'object',
456
- 'string',
457
- 'null'
458
- ]
459
- }
460
-
461
- _setMetaProperties(schema, joiSpec) {
462
- _.forEach(joiSpec.metas, (m) => {
463
- if (m.contentMediaType) {
464
- schema.contentMediaType = m.contentMediaType
465
- }
466
- if (m.format) {
467
- schema.format = m.format
468
- }
469
- })
470
- }
471
-
472
- _setLinkFieldProperties(schema, joiSpec) {
473
- if (schema.type !== 'link') {
474
- return
475
- }
476
-
477
- if (_.get(joiSpec, 'link.ref.type') === 'local') {
478
- schema.$ref = `${this._getLocalSchemaBasePath()}/${joiSpec.link.ref.path.join('/')}`
479
- delete schema.type
480
- }
481
- }
482
- }
483
-
484
- module.exports = JoiJsonSchemaParser
1
+ /* eslint no-use-before-define: 'off' */
2
+ const _ = require('lodash')
3
+
4
+ class JoiJsonSchemaParser {
5
+ constructor() {
6
+ this.childrenFieldName = this._getChildrenFieldName()
7
+ this.optionsFieldName = this._getOptionsFieldName()
8
+ this.ruleArgFieldName = this._getRuleArgFieldName()
9
+ this.enumFieldName = this._getEnumFieldName()
10
+ this.allowUnknownFlagName = this._getAllowUnknownFlagName()
11
+ }
12
+
13
+ parse(joiSpec, definitions = {}, level = 0) {
14
+ let schema = {}
15
+
16
+ if (this._getPresence(joiSpec) === 'forbidden') {
17
+ schema.not = {}
18
+ return schema
19
+ }
20
+
21
+ this._setBasicProperties(schema, joiSpec)
22
+ this._setNumberFieldProperties(schema, joiSpec)
23
+ this._setBinaryFieldProperties(schema, joiSpec)
24
+ this._setStringFieldProperties(schema, joiSpec)
25
+ this._setDateFieldProperties(schema, joiSpec)
26
+ this._setArrayFieldProperties(schema, joiSpec, definitions, level)
27
+ this._setObjectProperties(schema, joiSpec, definitions, level)
28
+ this._setAlternativesProperties(schema, joiSpec, definitions, level)
29
+ this._setAnyProperties(schema, joiSpec, definitions, level)
30
+ this._addNullTypeIfNullable(schema, joiSpec)
31
+ this._setMetaProperties(schema, joiSpec)
32
+ this._setLinkFieldProperties(schema, joiSpec)
33
+
34
+ if (!_.isEmpty(joiSpec.shared)) {
35
+ this.parse(joiSpec.shared[0], definitions, level)
36
+ }
37
+
38
+ const schemaId = _.get(joiSpec, 'flags.id')
39
+ if (schemaId) {
40
+ definitions[schemaId] = schema
41
+ schema = {
42
+ $ref: `${this._getLocalSchemaBasePath()}/${schemaId}`
43
+ }
44
+ }
45
+ if (level === 0 && !_.isEmpty(definitions)) {
46
+ _.set(schema, `${this._getLocalSchemaBasePath().replace('#/', '').replace(/\//, '.')}`, definitions)
47
+ }
48
+
49
+ return schema
50
+ }
51
+
52
+ _getChildrenFieldName() {
53
+ return 'keys'
54
+ }
55
+
56
+ _getOptionsFieldName() {
57
+ return 'preferences'
58
+ }
59
+
60
+ _getRuleArgFieldName() {
61
+ return 'args'
62
+ }
63
+
64
+ _getEnumFieldName() {
65
+ return 'allow'
66
+ }
67
+
68
+ _getAllowUnknownFlagName() {
69
+ return 'unknown'
70
+ }
71
+
72
+ _getLocalSchemaBasePath() {
73
+ return '#/$defs'
74
+ }
75
+
76
+ _getFieldDescription(fieldDefn) {
77
+ return _.get(fieldDefn, 'flags.description')
78
+ }
79
+
80
+ _getFieldType(fieldDefn) {
81
+ let type = fieldDefn.type
82
+ if (type === 'number' && !_.isEmpty(fieldDefn.rules) &&
83
+ fieldDefn.rules[0].name === 'integer') {
84
+ type = 'integer'
85
+ }
86
+ return type
87
+ }
88
+
89
+ _addNullTypeIfNullable(fieldSchema, fieldDefn) {
90
+ // This should always be the last call in parse
91
+ const enums = _.get(fieldDefn, this.enumFieldName)
92
+ if (Array.isArray(enums) && enums.includes(null)) {
93
+ fieldSchema.type = [fieldSchema.type, 'null']
94
+ }
95
+ }
96
+
97
+ _getFieldExample(fieldDefn) {
98
+ return _.get(fieldDefn, 'examples')
99
+ }
100
+
101
+ _getPresence(fieldDefn) {
102
+ const presence = _.get(fieldDefn, 'flags.presence')
103
+ if (presence !== undefined) {
104
+ return presence
105
+ }
106
+ return _.get(fieldDefn, `${this.optionsFieldName}.presence`)
107
+ }
108
+
109
+ _isRequired(fieldDefn) {
110
+ const presence = this._getPresence(fieldDefn)
111
+ return presence === 'required'
112
+ }
113
+
114
+ _getDefaultValue(fieldDefn) {
115
+ return _.get(fieldDefn, 'flags.default')
116
+ }
117
+
118
+ _getConst(fieldDefn) {
119
+ const enumList = fieldDefn[this.enumFieldName]
120
+ if (fieldDefn.flags && fieldDefn.flags.only && _.size(enumList) === 1) {
121
+ return enumList[0]
122
+ }
123
+ }
124
+
125
+ _getEnum(fieldDefn) {
126
+ const enumList = fieldDefn[this.enumFieldName]
127
+ if (fieldDefn.flags && fieldDefn.flags.only && _.size(enumList) > 1) {
128
+ return _.uniq(enumList)
129
+ }
130
+ }
131
+
132
+ _getUnknown(joiSpec) {
133
+ let allowUnknown = _.get(joiSpec, `${this.optionsFieldName}.allowUnknown`, false)
134
+ if (joiSpec.flags && typeof joiSpec.flags[this.allowUnknownFlagName] !== 'undefined') {
135
+ allowUnknown = joiSpec.flags[this.allowUnknownFlagName]
136
+ }
137
+ return allowUnknown
138
+ }
139
+
140
+ _setIfNotEmpty(schema, field, value) {
141
+ if (value !== null && value !== undefined) {
142
+ schema[field] = value
143
+ }
144
+ }
145
+
146
+ _setBasicProperties(fieldSchema, fieldDefn) {
147
+ this._setIfNotEmpty(fieldSchema, 'type', this._getFieldType(fieldDefn))
148
+ this._setIfNotEmpty(fieldSchema, 'examples', this._getFieldExample(fieldDefn))
149
+ this._setIfNotEmpty(fieldSchema, 'description', this._getFieldDescription(fieldDefn))
150
+ this._setIfNotEmpty(fieldSchema, 'default', this._getDefaultValue(fieldDefn))
151
+ this._setIfNotEmpty(fieldSchema, 'const', this._getConst(fieldDefn))
152
+ this._setIfNotEmpty(fieldSchema, 'enum', this._getEnum(fieldDefn))
153
+ }
154
+
155
+ _setBinaryFieldProperties(fieldSchema, fieldDefn) {
156
+ if (fieldSchema.type !== 'binary') {
157
+ return
158
+ }
159
+ fieldSchema.type = 'string'
160
+ if (fieldDefn.flags && fieldDefn.flags.encoding) {
161
+ fieldSchema.contentEncoding = fieldDefn.flags.encoding
162
+ }
163
+ fieldSchema.format = 'binary'
164
+ }
165
+
166
+ _setObjectProperties(schema, joiSpec, definitions, level) {
167
+ if (schema.type !== 'object') {
168
+ return
169
+ }
170
+
171
+ schema.properties = {}
172
+ schema.required = []
173
+
174
+ schema.additionalProperties = this._getUnknown(joiSpec)
175
+
176
+ _.map(joiSpec[this.childrenFieldName], (fieldDefn, key) => {
177
+ const fieldSchema = this.parse(fieldDefn, definitions, level + 1)
178
+ if (this._isRequired(fieldDefn)) {
179
+ schema.required.push(key)
180
+ }
181
+
182
+ schema.properties[key] = fieldSchema
183
+ })
184
+
185
+ /**
186
+ * For dynamic key scenarios to store the pattern as key
187
+ * and have the properties be as with other examples
188
+ */
189
+ if (joiSpec.patterns) {
190
+ _.each(joiSpec.patterns, patternObj => {
191
+ if (typeof patternObj.rule !== 'object' || typeof patternObj.regex === 'undefined') {
192
+ return
193
+ }
194
+
195
+ schema.properties[patternObj.regex] = {
196
+ type: patternObj.rule.type,
197
+ properties: {}
198
+ }
199
+ schema.properties[patternObj.regex].required = []
200
+
201
+ const childKeys = patternObj.rule.keys || patternObj.rule.children
202
+ schema.properties[patternObj.regex].additionalProperties = this._getUnknown(patternObj.rule)
203
+
204
+ _.each(childKeys, (ruleObj, key) => {
205
+ schema.properties[patternObj.regex].properties[key] = this.parse(ruleObj, definitions, level + 1)
206
+
207
+ if (this._isRequired(ruleObj)) {
208
+ schema.properties[patternObj.regex].required.push(key)
209
+ }
210
+ })
211
+
212
+ schema.patternProperties = schema.patternProperties || {}
213
+
214
+ let regexString = patternObj.regex
215
+ regexString = regexString.indexOf('/') === 0 ? regexString.substring(1) : regexString
216
+ regexString = regexString.lastIndexOf('/') > -1 ? regexString.substring(0, regexString.length - 1) : regexString
217
+
218
+ schema.patternProperties[regexString] = schema.properties[patternObj.regex]
219
+ })
220
+ }
221
+
222
+ if (_.isEmpty(schema.required)) {
223
+ delete schema.required
224
+ }
225
+ }
226
+
227
+ _setNumberFieldProperties(fieldSchema, fieldDefn) {
228
+ if (fieldSchema.type !== 'number' && fieldSchema.type !== 'integer') {
229
+ return
230
+ }
231
+
232
+ const ruleArgFieldName = this.ruleArgFieldName
233
+
234
+ _.each(fieldDefn.rules, (rule) => {
235
+ const value = rule[ruleArgFieldName]
236
+ switch (rule.name) {
237
+ case 'max':
238
+ fieldSchema.maximum = value.limit
239
+ break
240
+ case 'min':
241
+ fieldSchema.minimum = value.limit
242
+ break
243
+ case 'greater':
244
+ fieldSchema.exclusiveMinimum = value.limit
245
+ fieldSchema.minimum = value.limit
246
+ break
247
+ case 'less':
248
+ fieldSchema.exclusiveMaximum = value.limit
249
+ fieldSchema.maximum = value.limit
250
+ break
251
+ case 'multiple':
252
+ fieldSchema.multipleOf = value.base
253
+ break
254
+ default:
255
+ break
256
+ }
257
+ })
258
+ }
259
+
260
+ _setStringFieldProperties(fieldSchema, fieldDefn) {
261
+ if (fieldSchema.type !== 'string') {
262
+ return
263
+ }
264
+
265
+ if (fieldDefn.flags && fieldDefn.flags.encoding) {
266
+ fieldSchema.contentEncoding = fieldDefn.flags.encoding
267
+ }
268
+
269
+ const ruleArgFieldName = this.ruleArgFieldName
270
+
271
+ _.forEach(fieldDefn.rules, (rule) => {
272
+ switch (rule.name) {
273
+ case 'min':
274
+ fieldSchema.minLength = rule[ruleArgFieldName].limit
275
+ break
276
+ case 'max':
277
+ fieldSchema.maxLength = rule[ruleArgFieldName].limit
278
+ break
279
+ case 'email':
280
+ fieldSchema.format = 'email'
281
+ break
282
+ case 'hostname':
283
+ fieldSchema.format = 'hostname'
284
+ break
285
+ case 'uri':
286
+ fieldSchema.format = 'uri'
287
+ break
288
+ case 'ip':
289
+ const versions = rule[ruleArgFieldName].options.version
290
+ if (!_.isEmpty(versions)) {
291
+ if (versions.length === 1) {
292
+ fieldSchema.format = versions[0]
293
+ } else {
294
+ fieldSchema.oneOf = _.map(versions, (version) => {
295
+ return {
296
+ format: version
297
+ }
298
+ })
299
+ }
300
+ } else {
301
+ fieldSchema.format = 'ipv4'
302
+ }
303
+ break
304
+ case 'pattern':
305
+ let regex = rule[ruleArgFieldName].regex
306
+ let idx = regex.indexOf('/')
307
+ if (idx === 0) {
308
+ regex = regex.replace('/', '')
309
+ }
310
+ idx = regex.lastIndexOf('/') === regex.length - 1
311
+ if (idx > -1) {
312
+ regex = regex.replace(/\/$/, '')
313
+ }
314
+ fieldSchema.pattern = regex
315
+ break
316
+ case 'isoDate':
317
+ fieldSchema.format = 'date-time'
318
+ break
319
+ case 'isoDuration':
320
+ fieldSchema.format = 'duration'
321
+ break
322
+ case 'uuid':
323
+ case 'guid':
324
+ fieldSchema.format = 'uuid'
325
+ break
326
+ default:
327
+ break
328
+ }
329
+ })
330
+ }
331
+
332
+ _setArrayFieldProperties(fieldSchema, fieldDefn, definitions, level) {
333
+ if (fieldSchema.type !== 'array') {
334
+ return
335
+ }
336
+
337
+ const ruleArgFieldName = this.ruleArgFieldName
338
+
339
+ _.each(fieldDefn.rules, (rule) => {
340
+ const value = rule[ruleArgFieldName]
341
+ switch (rule.name) {
342
+ case 'max':
343
+ fieldSchema.maxItems = value.limit
344
+ break
345
+ case 'min':
346
+ fieldSchema.minItems = value.limit
347
+ break
348
+ case 'length':
349
+ fieldSchema.maxItems = value.limit
350
+ fieldSchema.minItems = value.limit
351
+ break
352
+ case 'unique':
353
+ fieldSchema.uniqueItems = true
354
+ break
355
+ default:
356
+ break
357
+ }
358
+ })
359
+
360
+ if (!fieldDefn.items) {
361
+ fieldSchema.items = {}
362
+ return
363
+ }
364
+
365
+ if (fieldDefn.items.length === 1) {
366
+ fieldSchema.items = this.parse(fieldDefn.items[0], definitions, level + 1)
367
+ } else {
368
+ fieldSchema.items = {
369
+ anyOf: _.map(fieldDefn.items, (itemSchema) => {
370
+ return this.parse(itemSchema, definitions, level + 1)
371
+ })
372
+ }
373
+ }
374
+ }
375
+
376
+ _setDateFieldProperties(fieldSchema, fieldDefn) {
377
+ if (fieldSchema.type !== 'date') {
378
+ return
379
+ }
380
+
381
+ if (fieldDefn.flags && fieldDefn.flags.format !== 'iso') {
382
+ fieldSchema.type = 'integer'
383
+ } else {
384
+ // https://datatracker.ietf.org/doc/draft-handrews-json-schema-validation
385
+ // JSON Schema does not have date type, but use string with format.
386
+ // However, joi definition cannot clearly tells the date/time/date-time format
387
+ fieldSchema.type = 'string'
388
+ fieldSchema.format = 'date-time'
389
+ }
390
+ }
391
+
392
+ _setAlternativesProperties(schema, joiSpec, definitions, level) {
393
+ if (schema.type !== 'alternatives') {
394
+ return
395
+ }
396
+
397
+ if (joiSpec.matches.length === 1) {
398
+ const match = joiSpec.matches[0]
399
+ if (match.switch) {
400
+ schema.oneOf = _.map(match.switch, (condition) => {
401
+ return this.parse(condition.then || condition.otherwise, definitions, level + 1)
402
+ })
403
+ } else if (match.then || match.otherwise) {
404
+ schema.oneOf = []
405
+ if (match.then) schema.oneOf.push(this.parse(match.then, definitions, level + 1))
406
+ if (match.otherwise) schema.oneOf.push(this.parse(match.otherwise, definitions, level + 1))
407
+ }
408
+ } else {
409
+ schema.oneOf = _.map(joiSpec.matches, (match) => {
410
+ return this.parse(match.schema, definitions, level + 1)
411
+ })
412
+ }
413
+
414
+ delete schema.type
415
+ }
416
+
417
+ _setAnyProperties(schema, joiSpec, definitions, level) {
418
+ if (schema.type !== 'any') {
419
+ return
420
+ }
421
+
422
+ if (joiSpec.whens) {
423
+ schema.oneOf = []
424
+
425
+ const condition = joiSpec.whens[0]
426
+
427
+ if (condition.switch) {
428
+ for (const switchCondition of condition.switch) {
429
+ if (switchCondition.then) {
430
+ schema.oneOf.push(this.parse(switchCondition.then, definitions, level + 1))
431
+ }
432
+
433
+ if (switchCondition.otherwise) {
434
+ schema.oneOf.push(this.parse(switchCondition.otherwise, definitions, level + 1))
435
+ }
436
+ }
437
+ }
438
+
439
+ if (condition.then) {
440
+ schema.oneOf.push(this.parse(condition.then, definitions, level + 1))
441
+ }
442
+
443
+ if (condition.otherwise) {
444
+ schema.oneOf.push(this.parse(condition.otherwise, definitions, level + 1))
445
+ }
446
+
447
+ delete schema.type
448
+ return
449
+ }
450
+
451
+ schema.type = [
452
+ 'array',
453
+ 'boolean',
454
+ 'number',
455
+ 'object',
456
+ 'string',
457
+ 'null'
458
+ ]
459
+ }
460
+
461
+ _setMetaProperties(schema, joiSpec) {
462
+ _.forEach(joiSpec.metas, (m) => {
463
+ if (m.contentMediaType) {
464
+ schema.contentMediaType = m.contentMediaType
465
+ }
466
+ if (m.format) {
467
+ schema.format = m.format
468
+ }
469
+ })
470
+ }
471
+
472
+ _setLinkFieldProperties(schema, joiSpec) {
473
+ if (schema.type !== 'link') {
474
+ return
475
+ }
476
+
477
+ if (_.get(joiSpec, 'link.ref.type') === 'local') {
478
+ schema.$ref = `${this._getLocalSchemaBasePath()}/${joiSpec.link.ref.path.join('/')}`
479
+ delete schema.type
480
+ }
481
+ }
482
+ }
483
+
484
+ module.exports = JoiJsonSchemaParser
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "joi-to-json",
3
- "version": "2.4.0",
4
- "description": "joi to JSON / OpenAPI Schema Converter",
5
- "main": "index.js",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/kenspirit/joi-to-json.git"
9
- },
10
- "scripts": {
11
- "lint": "./node_modules/.bin/eslint .",
12
- "test": "jest"
13
- },
14
- "author": "Ken Chen",
15
- "license": "MIT",
16
- "keywords": [
17
- "joi",
18
- "json-schema",
19
- "json",
20
- "schema",
21
- "openapi",
22
- "swagger",
23
- "oas"
24
- ],
25
- "dependencies": {
26
- "lodash": "^4.17.21",
27
- "semver-compare": "^1.0.0"
28
- },
29
- "devDependencies": {
30
- "ajv": "^8.11.0",
31
- "ajv-draft-04": "^1.0.0",
32
- "eslint": "^8.12.0",
33
- "jest": "^27.5.1",
34
- "joi-12": "npm:@commercial/joi@^12.1.0",
35
- "joi-13": "npm:joi@^13.7.0",
36
- "joi-14": "npm:joi@^14.3.1",
37
- "joi-15": "npm:@hapi/joi@^15.1.1",
38
- "joi-16": "npm:@hapi/joi@^16.1.8",
39
- "joi-17": "npm:joi@^17.4.2"
40
- },
41
- "types": "index.d.ts",
42
- "files": [
43
- "lib/**/*.js",
44
- "index.js",
45
- "index.d.ts"
46
- ]
47
- }
1
+ {
2
+ "name": "joi-to-json",
3
+ "version": "2.5.0",
4
+ "description": "joi to JSON / OpenAPI Schema Converter",
5
+ "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/kenspirit/joi-to-json.git"
9
+ },
10
+ "scripts": {
11
+ "lint": "./node_modules/.bin/eslint .",
12
+ "test": "jest"
13
+ },
14
+ "author": "Ken Chen",
15
+ "license": "MIT",
16
+ "keywords": [
17
+ "joi",
18
+ "json-schema",
19
+ "json",
20
+ "schema",
21
+ "openapi",
22
+ "swagger",
23
+ "oas"
24
+ ],
25
+ "dependencies": {
26
+ "lodash": "^4.17.21",
27
+ "semver-compare": "^1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "ajv": "^8.11.0",
31
+ "ajv-draft-04": "^1.0.0",
32
+ "eslint": "^8.12.0",
33
+ "jest": "^27.5.1",
34
+ "joi-12": "npm:@commercial/joi@^12.1.0",
35
+ "joi-13": "npm:joi@^13.7.0",
36
+ "joi-14": "npm:joi@^14.3.1",
37
+ "joi-15": "npm:@hapi/joi@^15.1.1",
38
+ "joi-16": "npm:@hapi/joi@^16.1.8",
39
+ "joi-17": "npm:joi@^17.6.0"
40
+ },
41
+ "types": "index.d.ts",
42
+ "files": [
43
+ "lib/**/*.js",
44
+ "index.js",
45
+ "index.d.ts"
46
+ ]
47
+ }