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 +3 -0
- package/README.md +151 -0
- package/index.js +2 -2
- package/lib/convertors/v12.js +258 -247
- package/lib/parsers/json-draft-04.js +25 -21
- package/lib/parsers/json.js +108 -3
- package/lib/parsers/open-api.js +83 -83
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
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
|
package/lib/convertors/v12.js
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (joiObj.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
joiObj.whens
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
rule.name = '
|
|
119
|
-
rule.args
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
rule.args =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
rule.args = {
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
break
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
6
|
-
super.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
package/lib/parsers/json.js
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
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) {
|
package/lib/parsers/open-api.js
CHANGED
|
@@ -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": "
|
|
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
|
},
|