joi-to-json 1.5.0 → 2.2.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
@@ -8,10 +8,23 @@ It's **The most powerful schema description language and data validator for Java
8
8
  Many times, we need to utilize this schema description to produce other output, such as Swagger OpenAPI doc.
9
9
  That is why I build [joi-route-to-swagger](https://github.com/kenspirit/joi-route-to-swagger) in the first place.
10
10
 
11
- At the beginning, `joi-route-to-swagger` relies on [joi-to-json-schema](https://github.com/lightsofapollo/joi-to-json-schema/) which utilizes many joi internal api or properties. I believed there was reason. Maybe joi did not provide the `describe` api way before. But I always feel not comfortable and think it's time to move on.
11
+ At the beginning, `joi-route-to-swagger` relies on [joi-to-json-schema](https://github.com/lightsofapollo/joi-to-json-schema/) which utilizes many joi internal api or properties. I believed there was reason. Maybe joi did not provide the `describe` api way before. But I always feel uncomfortable and think it's time to move on.
12
12
 
13
- The intention of `joi-to-json` is to support converting different version's joi schema to [JSON Schema (draft-04)](https://json-schema.org/specification-links.html#draft-4) using `describe` api.
13
+ The intention of `joi-to-json` is to support converting different version's joi schema to [JSON Schema](https://json-schema.org) using `describe` api.
14
14
 
15
+ ## 2.0.0 is out
16
+
17
+ It's a breaking change.
18
+
19
+ * Functionally, output format supports OpenAPI Schema other than purely JSON Schema.
20
+
21
+ * Technically, implementation theory has a big change:
22
+ - In v1.0.0, it directly converts `joi.describe()` to JSON schema using different parser implementations.
23
+ - In v2.0.0, `joi.describe()` of different versions are first converted to one base format, the latest version of `joi.describe()` output. Then different parsers (JSON, OpenAPI) all refer to this base format.
24
+
25
+ * The benefits of the change are:
26
+ - Easier to retire old version of joi.
27
+ - Easier to support more output formats.
15
28
 
16
29
  ## Installation
17
30
 
@@ -28,21 +41,30 @@ The intention of `joi-to-json` is to support converting different version's joi
28
41
  * @hapi/joi
29
42
  * 15.1.1
30
43
  * 16.1.8
31
- * 17.1.0
44
+ * joi
45
+ * 17.4.2
32
46
 
33
- For all above version, I have tested one complex joi object [fixtures](./fixtures) which covers most of the JSON schema attributes that can be described in joi schema.
47
+ For all above versions, I have tested one complex joi object [fixtures](./fixtures) which covers most of the JSON schema attributes that can be described in joi schema.
34
48
 
35
49
  Although the versions chosen are the latest one for each major version, I believe it should be supporting other minor version as well.
36
50
 
37
51
 
38
52
  ## Usage
39
53
 
40
- Only one API `convert` is available.
54
+ Only one API `parse` is available. It's signature is `parse(joiObj, type = 'json')`
41
55
 
42
- You can optionally provide debug flag `true` as the second argument to check which version of the parser is chosen and the joi schema describe output
56
+ Currently supported output types:
57
+ * `json` - Default. Stands for JSON Schema Draft 07
58
+ * `open-api` - Stands for OpenAPI Schema
59
+ * `json-draft-04` - Stands for JSON Schema Draft 04
60
+ * `json-draft-2019-09` - Stands for JSON Schema Draft 2019-09
61
+
62
+ The output schema format are in [outputs](./outputs) under specific folders for different types.
63
+
64
+ Sample code is as below:
43
65
 
44
66
  ```javascript
45
- const convert = require('joi-to-json')
67
+ const parse = require('joi-to-json')
46
68
 
47
69
  const joiSchema = joi.object().keys({
48
70
  nickName: joi.string().required().min(3).max(20).example('鹄思乱想').description('Hero Nickname')
@@ -64,14 +86,15 @@ const joiSchema = joi.object().keys({
64
86
  ).required()).min(1).max(3).unique().description('Skills'),
65
87
  tags: joi.array().items(joi.string().required()).length(2),
66
88
  retired: joi.boolean().truthy('yes').falsy('no').insensitive(false),
67
- certificate: joi.binary().encoding('base64')
89
+ certificate: joi.binary().encoding('base64'),
90
+ notes: joi.any().meta({ 'x-supported-lang': ['zh-CN', 'en-US'], deprecated: true })
68
91
  })
69
92
 
70
- const jsonSchema = convert(joiSchema, true)
93
+ const jsonSchema = parse(joiSchema)
94
+ // Or parsing to OpenAPI schema through:
95
+ // const openApiSchema = parse(joiSchema, 'open-api')
71
96
  ```
72
97
 
73
- The output json format is [here](./output.json)
74
-
75
98
  ## Test
76
99
 
77
100
  >npm run test
@@ -79,8 +102,6 @@ The output json format is [here](./output.json)
79
102
  You can optionally set below environment variables:
80
103
 
81
104
  * `CASE_PATTERN=joi-obj-12` to control which version of joi obj to test
82
- * `DEBUG=true` to check which version of the parser is chosen and the joi schema describe output
83
-
84
105
 
85
106
  ## License
86
107
 
package/index.js CHANGED
@@ -1,23 +1,41 @@
1
1
  const cmp = require('semver-compare')
2
- const DEFAULT_PARSER = require('./lib/parser_base')
2
+ const fs = require('fs')
3
+ const path = require('path')
3
4
 
4
- const parsers = [
5
- require('./lib/parser_v16'),
6
- require('./lib/parser_v14'),
7
- DEFAULT_PARSER
8
- ]
5
+ const convertorsDir = path.resolve(__dirname, './lib/convertors')
6
+ const parsersDir = path.resolve(__dirname, './lib/parsers')
7
+ const convertors = []
8
+ const parsers = {}
9
9
 
10
- function convert(joiObj, debug) {
11
- let parser
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
+ })
16
+
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
+ })
23
+
24
+ function parse(joiObj, type = 'json') {
25
+ if (typeof joiObj.describe !== 'function') {
26
+ throw new Error('Not an joi object.')
27
+ }
12
28
 
13
- for (let i = 0; i < parsers.length; i++) {
14
- const tmpParser = parsers[i]
29
+ let convertor
30
+
31
+ for (let i = 0; i < convertors.length; i++) {
32
+ const tmpConvertor = convertors[i]
15
33
  try {
16
- let version = tmpParser.getVersion(joiObj)
17
- let result = cmp(tmpParser.getSupportVersion(), version)
34
+ let version = tmpConvertor.getVersion(joiObj)
35
+ let result = cmp(tmpConvertor.getSupportVersion(), version)
18
36
  if (result <= 0) {
19
37
  // The first parser has smaller or equal version
20
- parser = tmpParser
38
+ convertor = tmpConvertor
21
39
  break
22
40
  }
23
41
  } catch (e) {
@@ -27,17 +45,18 @@ function convert(joiObj, debug) {
27
45
  }
28
46
  }
29
47
 
30
- if (!parser) {
31
- console.warn('No parser available, using the default one')
32
- parser = DEFAULT_PARSER
48
+ if (!convertor) {
49
+ console.warn('No matched joi version convertor found, using the latest version')
50
+ convertor = convertors[0]
33
51
  }
34
52
 
35
- const parserObj = new parser(joiObj)
36
- if (debug) {
37
- console.debug(`Parser of verison ${parser.getSupportVersion()} is chosen.\n`)
38
- console.debug(`Joi Object Describe Result as below:\n${JSON.stringify(parserObj.joiDescribe, null, 2)}\n`)
53
+ const joiBaseSpec = new convertor().toBaseSpec(joiObj.describe())
54
+ const parser = parsers[type]
55
+ if (!parser) {
56
+ throw new Error(`No parser is registered for ${type}`)
39
57
  }
40
- return parserObj.jsonSchema
58
+
59
+ return new parser().parse(joiBaseSpec)
41
60
  }
42
61
 
43
- module.exports = convert
62
+ module.exports = parse
@@ -0,0 +1,241 @@
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
+ if (joiObj.base && joiObj.base.type === 'any') {
41
+ // when case
42
+ joiObj.type = 'any'
43
+ joiObj.whens = joiObj.alternatives
44
+
45
+ if (joiObj.flags.presence === 'ignore') {
46
+ // FIXME: Not sure when this flag is set.
47
+ delete joiObj.flags.presence
48
+ }
49
+ if (joiObj.whens.peek) {
50
+ joiObj.whens.is = joiObj.whens.peek
51
+ delete joiObj.whens.peek
52
+ }
53
+
54
+ joiObj.whens[0] = _.mapValues(joiObj.whens[0], (value, condition) => {
55
+ switch (condition) {
56
+ case 'is':
57
+ this.toBaseSpec(value)
58
+ value.type = 'any'
59
+ // FIXME: Not sure when this is set.
60
+ value.allow.splice(0, 0, { 'override': true })
61
+ break
62
+ case 'otherwise':
63
+ this.toBaseSpec(value)
64
+ break
65
+ case 'then':
66
+ this.toBaseSpec(value)
67
+ break
68
+ case 'ref':
69
+ if (value.indexOf('ref:') === 0) {
70
+ value = {
71
+ path: value.replace('ref:', '').split('.')
72
+ }
73
+ }
74
+ break
75
+ }
76
+ return value
77
+ })
78
+
79
+ delete joiObj.alternatives
80
+ delete joiObj.base
81
+ } else {
82
+ joiObj.matches = _.map(joiObj.alternatives, (alternative) => {
83
+ return {
84
+ schema: this.toBaseSpec(alternative)
85
+ }
86
+ })
87
+ delete joiObj.alternatives
88
+ }
89
+ }
90
+
91
+ _convertString(joiObj) {
92
+ delete joiObj.options
93
+
94
+ _.each(joiObj.rules, (rule, _idx) => {
95
+ const name = rule.name
96
+ if (['min', 'max'].includes(name)) {
97
+ rule.args = {
98
+ limit: rule.arg
99
+ }
100
+ } else if (['lowercase', 'uppercase'].includes(name)) {
101
+ rule.name = 'case'
102
+ rule.args = {
103
+ direction: name.replace('case', '')
104
+ }
105
+ delete joiObj.flags.case
106
+ } else if (rule.arg) {
107
+ rule.args = {
108
+ options: rule.arg
109
+ }
110
+ }
111
+ if (name === 'regex') {
112
+ rule.name = 'pattern'
113
+ rule.args.regex = rule.arg.pattern.toString()
114
+ delete rule.args.options.pattern
115
+ }
116
+ delete rule.arg
117
+ })
118
+ }
119
+
120
+ _convertNumber(joiObj) {
121
+ _.each(joiObj.rules, (rule, _idx) => {
122
+ const name = rule.name
123
+ if (['positive', 'negative'].includes(name)) {
124
+ rule.args = {
125
+ sign: name
126
+ }
127
+ rule.name = 'sign'
128
+ }
129
+ if (['greater', 'less', 'min', 'max', 'precision'].includes(name)) {
130
+ rule.args = {
131
+ limit: rule.arg
132
+ }
133
+ }
134
+ if (name === 'multiple') {
135
+ rule.args = {
136
+ base: rule.arg
137
+ }
138
+ }
139
+ delete rule.arg
140
+ })
141
+
142
+ if (joiObj.flags.precision) {
143
+ delete joiObj.flags.precision
144
+ }
145
+ }
146
+
147
+ _convertBoolean(joiObj) {
148
+ if (joiObj.flags.insensitive === false) {
149
+ joiObj.flags.sensitive = true
150
+ }
151
+ delete joiObj.flags.insensitive
152
+ }
153
+
154
+ _convertArray(joiObj) {
155
+ if (joiObj.flags.sparse === false) {
156
+ delete joiObj.flags.sparse
157
+ }
158
+
159
+ _.each(joiObj.rules, (rule, _idx) => {
160
+ const name = rule.name
161
+ if (['min', 'max', 'length'].includes(name)) {
162
+ rule.args = {
163
+ limit: rule.arg
164
+ }
165
+ }
166
+ delete rule.arg
167
+ })
168
+
169
+ _.each(joiObj.items, (item, _idx) => {
170
+ this.toBaseSpec(item)
171
+ })
172
+ }
173
+
174
+ _convertExamples(_joiObj) {
175
+ }
176
+
177
+ toBaseSpec(joiObj) {
178
+ joiObj.flags = joiObj.flags || {}
179
+
180
+ if (joiObj.flags.allowOnly) {
181
+ joiObj.flags.only = joiObj.flags.allowOnly
182
+ delete joiObj.flags.allowOnly
183
+ }
184
+ if (joiObj.flags.allowUnknown) {
185
+ joiObj.flags.unknown = joiObj.flags.allowUnknown
186
+ delete joiObj.flags.allowUnknown
187
+ }
188
+ if (joiObj.description) {
189
+ joiObj.flags.description = joiObj.description
190
+ delete joiObj.description
191
+ }
192
+ if (joiObj.options) {
193
+ joiObj.preferences = joiObj.options
194
+ delete joiObj.options
195
+ }
196
+ if (joiObj.valids) {
197
+ joiObj.allow = joiObj.valids
198
+ delete joiObj.valids
199
+ }
200
+ if (joiObj.meta) {
201
+ joiObj.metas = joiObj.meta
202
+ delete joiObj.meta
203
+ }
204
+ delete joiObj.invalids
205
+
206
+ this._convertExamples(joiObj)
207
+
208
+ switch (joiObj.type) {
209
+ case 'object':
210
+ this._convertObject(joiObj)
211
+ break
212
+ case 'date':
213
+ this._convertDate(joiObj)
214
+ break
215
+ case 'alternatives':
216
+ this._convertAlternatives(joiObj)
217
+ break
218
+ case 'string':
219
+ this._convertString(joiObj)
220
+ break
221
+ case 'number':
222
+ this._convertNumber(joiObj)
223
+ break
224
+ case 'boolean':
225
+ this._convertBoolean(joiObj)
226
+ break
227
+ case 'array':
228
+ this._convertArray(joiObj)
229
+ break
230
+ default:
231
+ break
232
+ }
233
+ if (_.isEmpty(joiObj.flags)) {
234
+ delete joiObj.flags
235
+ }
236
+
237
+ return joiObj
238
+ }
239
+ }
240
+
241
+ module.exports = JoiSpecConvertor
@@ -0,0 +1,9 @@
1
+ const BaseConverter = require('./v12')
2
+
3
+ class JoiSpecConvertor extends BaseConverter {
4
+ static getSupportVersion() {
5
+ return '13'
6
+ }
7
+ }
8
+
9
+ module.exports = JoiSpecConvertor
@@ -0,0 +1,24 @@
1
+ const BaseConverter = require('./v13')
2
+
3
+ class JoiSpecConvertor extends BaseConverter {
4
+ static getSupportVersion() {
5
+ return '14'
6
+ }
7
+
8
+ _convertExamples(joiObj) {
9
+ if (joiObj.examples && joiObj.examples.length === 1) {
10
+ const example = joiObj.examples[0]
11
+ joiObj.examples = [example.value]
12
+ }
13
+ }
14
+
15
+ _convertNumber(joiObj) {
16
+ super._convertNumber(joiObj)
17
+
18
+ if (joiObj.flags) {
19
+ delete joiObj.flags.unsafe
20
+ }
21
+ }
22
+ }
23
+
24
+ module.exports = JoiSpecConvertor
@@ -0,0 +1,9 @@
1
+ const BaseConverter = require('./v14')
2
+
3
+ class JoiSpecConvertor extends BaseConverter {
4
+ static getSupportVersion() {
5
+ return '15'
6
+ }
7
+ }
8
+
9
+ module.exports = JoiSpecConvertor
@@ -0,0 +1,30 @@
1
+ const BaseConverter = require('./v15')
2
+
3
+ class JoiSpecConvertor extends BaseConverter {
4
+ static getVersion(joiObj) {
5
+ return joiObj.$_root.version
6
+ }
7
+
8
+ static getSupportVersion() {
9
+ return '16'
10
+ }
11
+
12
+ // All methods are overridden because the Joi v16.1.8 spec is closed to the latest version
13
+ _convertExamples(_joiObj) { }
14
+
15
+ _convertObject(_joiObj) { }
16
+
17
+ _convertDate(_joiObj) { }
18
+
19
+ _convertAlternatives(_joiObj) { }
20
+
21
+ _convertString(_joiObj) { }
22
+
23
+ _convertNumber(_joiObj) { }
24
+
25
+ _convertBoolean(_joiObj) { }
26
+
27
+ _convertArray(_joiObj) {}
28
+ }
29
+
30
+ module.exports = JoiSpecConvertor
@@ -0,0 +1,9 @@
1
+ const BaseConverter = require('./v16')
2
+
3
+ class JoiSpecConvertor extends BaseConverter {
4
+ static getSupportVersion() {
5
+ return '17'
6
+ }
7
+ }
8
+
9
+ module.exports = JoiSpecConvertor
@@ -0,0 +1,17 @@
1
+ const _ = require('lodash')
2
+ const JoiJsonSchemaParser = require('./json')
3
+
4
+ class JoiOpenApiSchemaParser 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
+
17
+ module.exports = JoiOpenApiSchemaParser
@@ -0,0 +1,22 @@
1
+ const _ = require('lodash')
2
+ const JoiJsonSchemaParser = require('./json')
3
+
4
+ class JoiOpenApiSchemaParser extends JoiJsonSchemaParser {
5
+ parse(joiSpec) {
6
+ const schema = super.parse(joiSpec)
7
+
8
+ if (!_.isEmpty(joiSpec.metas)) {
9
+ _.each(joiSpec.metas, meta => {
10
+ _.each(meta, (value, key) => {
11
+ if (key === 'deprecated') {
12
+ schema[key] = value
13
+ }
14
+ })
15
+ })
16
+ }
17
+
18
+ return schema
19
+ }
20
+ }
21
+
22
+ module.exports = JoiOpenApiSchemaParser