joi-to-json 3.1.2 → 4.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/README.md CHANGED
@@ -8,27 +8,19 @@ 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 uncomfortable 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. Maybe joi did not provide the `describe` api way before, but I always feel uncomfortable of relying on internal api.
12
12
 
13
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
15
+ The implementation of this JOI to JSON conversion tool is simply a pipeline of two components:
16
16
 
17
- It's a breaking change.
17
+ 1. Convertors
18
+ - Each JOI version has one convertor implementation.
19
+ - It converts the `joi.describe()` output to the baseline format (currently the v16 and v17 one)
18
20
 
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.
28
-
29
- ## Installation
30
-
31
- >npm install joi-to-json
21
+ 2. Parsers
22
+ - Each supported output JSON format (e.g. JSON Draft 07, OpenAPI) has one parser implementation.
23
+ - All parsers converts the baseline format into its own format
32
24
 
33
25
 
34
26
  ## Joi Version Support
@@ -42,11 +34,14 @@ It's a breaking change.
42
34
  * 15.1.1
43
35
  * 16.1.8
44
36
  * joi
45
- * 17.4.2
37
+ * 17.9.2
38
+
39
+ Although the versions chosen are the latest one for each major version, It should support other minor versions as well.
46
40
 
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.
48
41
 
49
- Although the versions chosen are the latest one for each major version, I believe it should be supporting other minor version as well.
42
+ ## Installation
43
+
44
+ >npm install joi-to-json
50
45
 
51
46
 
52
47
  ## Usage
@@ -55,11 +50,11 @@ Only one API `parse` is available. It's signature is `parse(joiObj, type = 'jso
55
50
 
56
51
  Currently supported output types:
57
52
  * `json` - Default. Stands for JSON Schema Draft 07
58
- * `open-api` - Stands for OpenAPI Schema
53
+ * `open-api` - Stands for OpenAPI 3.0 Schema - an extended subset of JSON Schema Specification Wright Draft 00 (aka Draft 5)
59
54
  * `json-draft-04` - Stands for JSON Schema Draft 04
60
55
  * `json-draft-2019-09` - Stands for JSON Schema Draft 2019-09
61
56
 
62
- The output schema format are in [outputs](./outputs) under specific folders for different types.
57
+ The output schema format are in [outputs](./outputs-parsers) under specific folders for different types.
63
58
 
64
59
  Sample code is as below:
65
60
 
@@ -95,7 +90,26 @@ const jsonSchema = parse(joiSchema)
95
90
  // const openApiSchema = parse(joiSchema, 'open-api')
96
91
  ```
97
92
 
98
- ### Joi to OpenAPI
93
+ ## Features
94
+
95
+ ### Special Joi Operator Support
96
+
97
+ * [Logical Relation Operator](./docs/logical_rel_support.md)
98
+
99
+ ### Named Link
100
+
101
+ Supports named link for schema reuse, such as `.link('#person')`. **For `open-api` conversion**, as the shared schemas are located in `#/components/schemas` which is not self-contained, the conversion result contains an **extra `schemas`** field so that you can extract it when required.
102
+
103
+ ### Conditional Expression
104
+
105
+ Starting from Draft 7, JSON Specification supports `If-Then-Else` style expression. Before that, we can also use something called [Implication](http://json-schema.org/understanding-json-schema/reference/conditionals.html#implication) using Schema Composition Approach to simulate that.
106
+
107
+ By default, the `If-Then-Else` approach is used if the output spec supports it. However, if the joi conditional expression (`alternatives` or `when`) is annotated using Meta `.meta({ 'if-style': true })`, the JSON schema conversion will use the Composition approach using `allOf` and/or `anyOf` instead.
108
+
109
+ **Limitation**: Currently, if the joi condition definition is referring to another field, the `If-Then-Else` style output is not supported. Instead, it simply uses the `anyOf` composing the `then` and `otherwise` on the defined field.
110
+
111
+
112
+ ## YAML File Generation
99
113
 
100
114
  Most Joi specifications result in the expected OpenAPI schema.
101
115
 
@@ -196,7 +210,7 @@ properties:
196
210
  properties:
197
211
  key:
198
212
  type: string
199
- additionalProperties: true
213
+ additionalProperties: true
200
214
  additionalProperties: false
201
215
  ```
202
216
 
@@ -242,10 +256,6 @@ properties:
242
256
  additionalProperties: true
243
257
  ```
244
258
 
245
- ## Special Joi Operator Support
246
-
247
- * [Logical Relation Operator](./docs/logical_rel_support.md)
248
-
249
259
  ## Browser support
250
260
  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.
251
261
 
@@ -265,7 +275,7 @@ const logicalOpParser: Joi2Json.LogicalOpParserOpts = {
265
275
  };
266
276
 
267
277
  parse(joi.string()); // Default call
268
- parse(joi.string(), 'json', {}, false); // Completely disable Logical Relation Operator
278
+ parse(joi.string(), 'json', {}, { logicalOpParser: false }); // Completely disable Logical Relation Operator
269
279
  parse(joi.string(), 'open-api', {}, { logicalOpParser }); // Partially override Logical Relation Operator
270
280
  ```
271
281
 
@@ -273,19 +283,50 @@ parse(joi.string(), 'open-api', {}, { logicalOpParser }); // Partially override
273
283
 
274
284
  >npm run test
275
285
 
276
- You can optionally set below environment variables:
286
+ ### Categories of Test Cases
277
287
 
278
- * `CASE_PATTERN=joi-obj-17` to control which version of joi obj to test
288
+ * JOI Standard Representation Conversion
279
289
 
280
- ## Known Limitation
290
+ `fixtures-conversion` folder stores each JOI version's supported keyword for different data types.
291
+ In case any data type or keyword is not supported in historical JOI version, we can just create customized file to override the `base` version, such as `v15/link.js`.
281
292
 
282
- * For `object.pattern` usage in Joi, `pattern` parameter can only be a regular expression now as I cannot convert Joi object to regex yet.
293
+ Standard converted results are stored in `outputs-conversion` folder.
294
+
295
+ `test/conversion.spec.js` Test Spec handles all supported JOI versions' conversion verificaiton.
296
+
297
+ * JSON output format Conversion
298
+
299
+ `outputs-parsers` folder stores different output formats base on the JOI Standard Representation in `outputs-conversion` folder.
300
+ The Test Spec under `test/parser/` are responsible for these area.
301
+
302
+ * JSON schema (Draft 07) Validity Unit Test
283
303
 
284
- ## Updates
304
+ For special **Logical Relation Operator** and **Conditional Expression**, some Unit Tests are created to verify the JOI Spec and corresponding JSON Spec are valid of the same verification intention.
285
305
 
286
- **Version 2.3.0**
287
306
 
288
- * Supports named link for schema resuse, such as `.link('#person')`. **For `open-api` conversion**, as the shared schemas are located in `#/components/schemas` which is not self-contained, the conversion result contains an **extra `schemas`** field so that you can extract it when required.
307
+ ### Test Debug Approach
308
+
309
+ When running `conversion.spec.js`, below environment variables can be set:
310
+
311
+ * `TEST_CONVERTOR`: control which version of joi to test.
312
+ Example: `TEST_CONVERTOR=v17`
313
+ * `TEST_CASE`: control which test cases to verify. Name of the test cases is the key of the return object in `fixtures-conversion`.
314
+ Example: `TEST_CASE=conditional,match_all` verifies the case in `alternatives.js`
315
+ * `TEST_UPDATE_CONVERSION_BASELINE`: control whether writes the baseline file generated from the latest-version convertor (Currently `v17`).
316
+ It is activated when setting to `true`.
317
+
318
+ When runninng Test Spec under `test/parser`, below environment variables can be set:
319
+
320
+ * `TEST_CASE`: control which test cases to verify.
321
+ For example, when running `json.spec.js`, and set `TEST_CASE=conditional,match_all`, it verifies the corresponding JSON files in `outputs-parsers/json/alternatives`.
322
+ * `TEST_UPDATE_PARSER_BASELINE`: control whether writes the baseline file for the corresponding parser.
323
+ It is activated when setting to `true`. For example, when running `json.spec.js`, it writes the baseline files under `outputs-parsers/json`.
324
+
325
+
326
+ ## Known Limitation
327
+
328
+ * For `object.pattern` usage in Joi, `pattern` parameter can only be a regular expression now as I cannot convert Joi object to regex yet.
329
+ * `If-Then-Else` style output is not applicable for the condition referring to the other field.
289
330
 
290
331
  ## License
291
332
 
@@ -12,6 +12,16 @@ class JoiSpecConvertor {
12
12
  }
13
13
 
14
14
  _convertObject(joiObj) {
15
+ _.each(joiObj.rules, (rule, _idx) => {
16
+ const name = rule.name
17
+ if (['min', 'max', 'length'].includes(name)) {
18
+ rule.args = {
19
+ limit: rule.arg
20
+ }
21
+ }
22
+ delete rule.arg
23
+ })
24
+
15
25
  if (joiObj.children) {
16
26
  joiObj.keys = joiObj.children
17
27
  delete joiObj.children
@@ -30,6 +40,9 @@ class JoiSpecConvertor {
30
40
 
31
41
  _convertDependencies(joiObj) {
32
42
  _.each(joiObj.dependencies, (dependency) => {
43
+ if (dependency.key === null) {
44
+ delete dependency.key
45
+ }
33
46
  if (dependency.type) {
34
47
  dependency.rel = dependency.type
35
48
  delete dependency.type
@@ -48,61 +61,92 @@ class JoiSpecConvertor {
48
61
  }
49
62
 
50
63
  _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
- }
64
+ // backup alternatives setting
65
+ const alternatives = joiObj.alternatives
66
+ let fieldName = 'matches'
56
67
 
57
- if (isWhenCase) {
58
- // when case
59
- joiObj.type = 'any'
60
- joiObj.whens = joiObj.alternatives
68
+ if (joiObj.base) {
69
+ // When case is based on one schema type
70
+ fieldName = 'whens'
61
71
 
62
- if (joiObj.flags.presence === 'ignore') {
63
- // FIXME: Not sure when this flag is set.
72
+ const baseType = joiObj.base
73
+ delete joiObj.base
74
+ delete joiObj.alternatives
75
+ if (joiObj.flags && joiObj.flags.presence) {
64
76
  delete joiObj.flags.presence
65
77
  }
66
- if (joiObj.whens.peek) {
67
- joiObj.whens.is = joiObj.whens.peek
68
- delete joiObj.whens.peek
69
- }
70
78
 
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
79
+ this.toBaseSpec(baseType)
80
+
81
+ _.merge(joiObj, baseType)
82
+ }
83
+
84
+ // Convert alternatives
85
+ joiObj[fieldName] = _.map(alternatives, (alternative) => {
86
+ if (alternative.peek || alternative.ref) {
87
+ if (alternative.peek) {
88
+ alternative.is = alternative.peek
89
+ delete alternative.peek
92
90
  }
93
- return value
94
- })
95
91
 
96
- delete joiObj.alternatives
97
- delete joiObj.base
98
- } else {
99
- joiObj.matches = _.map(joiObj.alternatives, (alternative) => {
92
+ return _.mapValues(alternative, (value, condition) => {
93
+ switch (condition) {
94
+ case 'is':
95
+ this.toBaseSpec(value)
96
+ if (alternative.ref) {
97
+ // alternative field reference case
98
+ value.type = 'any'
99
+ value.allow.splice(0, 0, { 'override': true })
100
+ }
101
+ break
102
+ case 'otherwise':
103
+ this.toBaseSpec(value)
104
+ break
105
+ case 'then':
106
+ this.toBaseSpec(value)
107
+ break
108
+ case 'ref':
109
+ if (value.indexOf('ref:') === 0) {
110
+ value = {
111
+ path: value.replace('ref:', '').split('.')
112
+ }
113
+ }
114
+ break
115
+ }
116
+ return value
117
+ })
118
+ } else {
100
119
  return {
101
120
  schema: this.toBaseSpec(alternative)
102
121
  }
103
- })
104
- delete joiObj.alternatives
105
- }
122
+ }
123
+ })
124
+
125
+ // FIXME: Joi seems merging the `is` to `then` schema
126
+ _.forEach(joiObj[fieldName], (alternative) => {
127
+ if (alternative.is && alternative.then) {
128
+ if (alternative.is.type === 'object') {
129
+ const isKeys = Object.keys(alternative.is.keys)
130
+ _.forEach(isKeys, (key) => {
131
+ delete alternative.then.keys[key]
132
+ })
133
+ }
134
+ }
135
+ })
136
+
137
+ delete joiObj.alternatives
138
+ }
139
+
140
+ _convertBinary(joiObj) {
141
+ _.each(joiObj.rules, (rule, _idx) => {
142
+ const name = rule.name
143
+ if (['min', 'max', 'length'].includes(name)) {
144
+ rule.args = {
145
+ limit: rule.arg
146
+ }
147
+ }
148
+ delete rule.arg
149
+ })
106
150
  }
107
151
 
108
152
  _convertString(joiObj) {
@@ -162,6 +206,18 @@ class JoiSpecConvertor {
162
206
  }
163
207
 
164
208
  _convertBoolean(joiObj) {
209
+ if (_.isArray(joiObj.truthy)) {
210
+ _.remove(joiObj.truthy, (i) => i === true)
211
+ if (joiObj.truthy.length === 0) {
212
+ delete joiObj.truthy
213
+ }
214
+ }
215
+ if (_.isArray(joiObj.falsy)) {
216
+ _.remove(joiObj.falsy, (i) => i === false)
217
+ if (joiObj.falsy.length === 0) {
218
+ delete joiObj.falsy
219
+ }
220
+ }
165
221
  if (joiObj.flags.insensitive === false) {
166
222
  joiObj.flags.sensitive = true
167
223
  }
@@ -180,6 +236,12 @@ class JoiSpecConvertor {
180
236
  limit: rule.arg
181
237
  }
182
238
  }
239
+ if (name === 'has') {
240
+ this.toBaseSpec(rule.arg)
241
+ rule.args = {
242
+ schema: rule.arg
243
+ }
244
+ }
183
245
  delete rule.arg
184
246
  })
185
247
 
@@ -196,12 +258,12 @@ class JoiSpecConvertor {
196
258
 
197
259
  if (joiObj.flags.allowOnly) {
198
260
  joiObj.flags.only = joiObj.flags.allowOnly
199
- delete joiObj.flags.allowOnly
200
261
  }
262
+ delete joiObj.flags.allowOnly
201
263
  if (joiObj.flags.allowUnknown) {
202
264
  joiObj.flags.unknown = joiObj.flags.allowUnknown
203
- delete joiObj.flags.allowUnknown
204
265
  }
266
+ delete joiObj.flags.allowUnknown
205
267
  if (joiObj.description) {
206
268
  joiObj.flags.description = joiObj.description
207
269
  delete joiObj.description
@@ -218,6 +280,13 @@ class JoiSpecConvertor {
218
280
  joiObj.metas = joiObj.meta
219
281
  delete joiObj.meta
220
282
  }
283
+ if (joiObj.invalids) {
284
+ // FIXME Looks like before v16, joi spec default add [''] for string and [Infinity,-Infinity] for number
285
+ if (!(joiObj.invalids.length === 1 && joiObj.invalids[0] === '') &&
286
+ !(joiObj.invalids.length === 2 && joiObj.invalids[0] === Infinity && joiObj.invalids[1] === -Infinity)) {
287
+ joiObj.invalid = joiObj.invalids
288
+ }
289
+ }
221
290
  delete joiObj.invalids
222
291
 
223
292
  this._convertExamples(joiObj)
@@ -232,6 +301,9 @@ class JoiSpecConvertor {
232
301
  case 'alternatives':
233
302
  this._convertAlternatives(joiObj)
234
303
  break
304
+ case 'binary':
305
+ this._convertBinary(joiObj)
306
+ break
235
307
  case 'string':
236
308
  this._convertString(joiObj)
237
309
  break
@@ -10,6 +10,9 @@ class JoiSpecConvertor extends BaseConverter {
10
10
  const example = joiObj.examples[0]
11
11
  joiObj.examples = [example.value]
12
12
  }
13
+ if (joiObj.examples && joiObj.allow && joiObj.flags && joiObj.flags.only) {
14
+ joiObj.examples = joiObj.allow
15
+ }
13
16
  }
14
17
 
15
18
  _convertNumber(joiObj) {
@@ -18,6 +18,8 @@ class JoiSpecConvertor extends BaseConverter {
18
18
 
19
19
  _convertAlternatives(_joiObj) { }
20
20
 
21
+ _convertBinary(_joiObj) { }
22
+
21
23
  _convertString(_joiObj) { }
22
24
 
23
25
  _convertNumber(_joiObj) { }
@@ -15,6 +15,10 @@ class JoiJsonDraftSchemaParser extends JoiJsonSchemaParser {
15
15
  }, opts))
16
16
  }
17
17
 
18
+ _isIfThenElseSupported() {
19
+ return false
20
+ }
21
+
18
22
  _setNumberFieldProperties(fieldSchema, fieldDefn) {
19
23
  super._setNumberFieldProperties(fieldSchema, fieldDefn)
20
24
 
@@ -29,6 +33,29 @@ class JoiJsonDraftSchemaParser extends JoiJsonSchemaParser {
29
33
  _getLocalSchemaBasePath() {
30
34
  return '#/definitions'
31
35
  }
36
+
37
+ _setArrayFieldProperties(fieldSchema, fieldDefn, definitions, level) {
38
+ super._setArrayFieldProperties(fieldSchema, fieldDefn, definitions, level)
39
+
40
+ delete fieldSchema.contains
41
+ }
42
+
43
+ _setConst(fieldSchema, fieldDefn) {
44
+ super._setConst(fieldSchema, fieldDefn)
45
+
46
+ if (typeof fieldSchema.const !== 'undefined') {
47
+ if (fieldSchema.const === null) {
48
+ fieldSchema.type = 'null'
49
+ } else if (_.isArray(fieldSchema.const)) {
50
+ fieldSchema.type = 'array'
51
+ } else {
52
+ // boolean / number / string / object
53
+ fieldSchema.type = typeof fieldSchema.const
54
+ }
55
+ fieldSchema.enum = [fieldSchema.const]
56
+ delete fieldSchema.const
57
+ }
58
+ }
32
59
  }
33
60
 
34
61
  module.exports = JoiJsonDraftSchemaParser
@@ -8,7 +8,7 @@ class JoiJsonDraftSchemaParser extends JoiJsonSchemaParser {
8
8
  if (!_.isEmpty(joiSpec.metas)) {
9
9
  _.each(joiSpec.metas, meta => {
10
10
  _.each(meta, (value, key) => {
11
- if (key === 'deprecated') {
11
+ if (key === 'deprecated' || key === 'readOnly' || key === 'writeOnly') {
12
12
  schema[key] = value
13
13
  }
14
14
  })
@@ -111,10 +111,12 @@ class JoiJsonSchemaParser {
111
111
  this._setArrayFieldProperties(schema, joiSpec, definitions, level)
112
112
  this._setObjectProperties(schema, joiSpec, definitions, level)
113
113
  this._setAlternativesProperties(schema, joiSpec, definitions, level)
114
- this._setAnyProperties(schema, joiSpec, definitions, level)
115
- this._addNullTypeIfNullable(schema, joiSpec)
114
+ this._setConditionProperties(schema, joiSpec, definitions, level, 'whens')
116
115
  this._setMetaProperties(schema, joiSpec)
117
116
  this._setLinkFieldProperties(schema, joiSpec)
117
+ this._setConst(schema, joiSpec)
118
+ this._setAnyProperties(schema, joiSpec, definitions, level)
119
+ this._addNullTypeIfNullable(schema, joiSpec)
118
120
 
119
121
  if (!_.isEmpty(joiSpec.shared)) {
120
122
  this.parse(joiSpec.shared[0], definitions, level)
@@ -134,6 +136,10 @@ class JoiJsonSchemaParser {
134
136
  return schema
135
137
  }
136
138
 
139
+ _isIfThenElseSupported() {
140
+ return true
141
+ }
142
+
137
143
  _getChildrenFieldName() {
138
144
  return 'keys'
139
145
  }
@@ -173,9 +179,21 @@ class JoiJsonSchemaParser {
173
179
 
174
180
  _addNullTypeIfNullable(fieldSchema, fieldDefn) {
175
181
  // This should always be the last call in parse
182
+ if (fieldSchema.const === null) {
183
+ return
184
+ }
185
+
176
186
  const enums = _.get(fieldDefn, this.enumFieldName)
177
187
  if (Array.isArray(enums) && enums.includes(null)) {
178
- fieldSchema.type = [fieldSchema.type, 'null']
188
+ if (Array.isArray(fieldSchema.type)) {
189
+ if (!fieldSchema.type.includes('null')) {
190
+ fieldSchema.type.push('null')
191
+ }
192
+ } else if (fieldSchema.type) {
193
+ fieldSchema.type = [fieldSchema.type, 'null']
194
+ } else {
195
+ fieldSchema.type = 'null'
196
+ }
179
197
  }
180
198
  }
181
199
 
@@ -200,13 +218,6 @@ class JoiJsonSchemaParser {
200
218
  return _.get(fieldDefn, 'flags.default')
201
219
  }
202
220
 
203
- _getConst(fieldDefn) {
204
- const enumList = fieldDefn[this.enumFieldName]
205
- if (fieldDefn.flags && fieldDefn.flags.only && _.size(enumList) === 1) {
206
- return enumList[0]
207
- }
208
- }
209
-
210
221
  _getEnum(fieldDefn) {
211
222
  const enumList = fieldDefn[this.enumFieldName]
212
223
  const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
@@ -234,8 +245,13 @@ class JoiJsonSchemaParser {
234
245
  this._setIfNotEmpty(fieldSchema, 'examples', this._getFieldExample(fieldDefn))
235
246
  this._setIfNotEmpty(fieldSchema, 'description', this._getFieldDescription(fieldDefn))
236
247
  this._setIfNotEmpty(fieldSchema, 'default', this._getDefaultValue(fieldDefn))
237
- this._setIfNotEmpty(fieldSchema, 'const', this._getConst(fieldDefn))
238
248
  this._setIfNotEmpty(fieldSchema, 'enum', this._getEnum(fieldDefn))
249
+ if (fieldDefn.invalid && fieldDefn.invalid.length > 0) {
250
+ fieldSchema.not = {
251
+ type: fieldSchema.type,
252
+ enum: fieldDefn.invalid
253
+ }
254
+ }
239
255
  }
240
256
 
241
257
  _setBinaryFieldProperties(fieldSchema, fieldDefn) {
@@ -480,6 +496,9 @@ class JoiJsonSchemaParser {
480
496
  case 'unique':
481
497
  fieldSchema.uniqueItems = true
482
498
  break
499
+ case 'has':
500
+ fieldSchema.contains = this.parse(value.schema, definitions, level + 1)
501
+ break
483
502
  default:
484
503
  break
485
504
  }
@@ -518,61 +537,192 @@ class JoiJsonSchemaParser {
518
537
  }
519
538
 
520
539
  _setAlternativesProperties(schema, joiSpec, definitions, level) {
521
- if (schema.type !== 'alternatives') {
540
+ if (schema.type !== 'alternatives' || joiSpec.matches.length === 0) {
522
541
  return
523
542
  }
524
543
 
525
- if (joiSpec.matches.length === 1) {
526
- const match = joiSpec.matches[0]
527
- if (match.switch) {
528
- schema.oneOf = _.map(match.switch, (condition) => {
529
- return this.parse(condition.then || condition.otherwise, definitions, level + 1)
530
- })
531
- } else if (match.then || match.otherwise) {
532
- schema.oneOf = []
533
- if (match.then) schema.oneOf.push(this.parse(match.then, definitions, level + 1))
534
- if (match.otherwise) schema.oneOf.push(this.parse(match.otherwise, definitions, level + 1))
544
+ if (joiSpec.matches[0].schema) {
545
+ // try style
546
+ let mode = 'anyOf'
547
+ if (joiSpec.flags && joiSpec.flags.match === 'one') {
548
+ mode = 'oneOf'
549
+ } else if (joiSpec.flags && joiSpec.flags.match === 'all') {
550
+ mode = 'allOf'
535
551
  }
536
- } else {
537
- schema.oneOf = _.map(joiSpec.matches, (match) => {
552
+
553
+ schema[mode] = _.map(joiSpec.matches, (match) => {
538
554
  return this.parse(match.schema, definitions, level + 1)
539
555
  })
556
+
557
+ if (schema[mode].length === 1) {
558
+ _.merge(schema, schema[mode][0])
559
+ delete schema[mode]
560
+ }
561
+ } else {
562
+ this._setConditionProperties(schema, joiSpec, definitions, level, 'matches', 'anyOf')
540
563
  }
541
564
 
542
- delete schema.type
565
+ if (schema.type === 'alternatives') {
566
+ delete schema.type
567
+ }
543
568
  }
544
569
 
545
- _setAnyProperties(schema, joiSpec, definitions, level) {
546
- if (schema.type !== 'any') {
570
+ _setConditionProperties(schema, joiSpec, definitions, level, conditionFieldName, logicKeyword = 'allOf') {
571
+ if (!joiSpec[conditionFieldName] || joiSpec[conditionFieldName].length === 0) {
547
572
  return
548
573
  }
549
574
 
550
- if (joiSpec.whens) {
551
- schema.oneOf = []
575
+ let ifThenStyle = this._isIfThenElseSupported()
576
+ const styleSetting = _.remove(joiSpec.metas, (meta) => {
577
+ return typeof meta['if-style'] !== undefined
578
+ })
552
579
 
553
- const condition = joiSpec.whens[0]
580
+ if (ifThenStyle && styleSetting.length > 0 && styleSetting[0]['if-style'] === false) {
581
+ ifThenStyle = false
582
+ }
554
583
 
555
- if (condition.switch) {
556
- for (const switchCondition of condition.switch) {
557
- if (switchCondition.then) {
558
- schema.oneOf.push(this.parse(switchCondition.then, definitions, level + 1))
559
- }
584
+ if (joiSpec[conditionFieldName].length > 1) {
585
+ // Multiple case
586
+ schema[logicKeyword] = _.map(joiSpec[conditionFieldName], (condition) => {
587
+ return this._setConditionSchema(ifThenStyle, {}, condition, definitions, level + 1)
588
+ })
589
+ } else {
590
+ this._setConditionSchema(ifThenStyle, schema, joiSpec[conditionFieldName][0], definitions, level + 1)
591
+ }
592
+ }
560
593
 
561
- if (switchCondition.otherwise) {
562
- schema.oneOf.push(this.parse(switchCondition.otherwise, definitions, level + 1))
563
- }
594
+ _setConditionSchema(ifThenStyle, schema, conditionJoiSpec, definitions, level) {
595
+ // When "if" is not present, both "then" and "else" MUST be entirely ignored.
596
+ // There must be either "is" or "switch"
597
+ if (!conditionJoiSpec.is && !conditionJoiSpec.switch) {
598
+ return
599
+ }
600
+
601
+ if (conditionJoiSpec.switch) {
602
+ this._parseSwitchCondition(ifThenStyle, schema, conditionJoiSpec, definitions, level)
603
+ } else {
604
+ if (ifThenStyle) {
605
+ this._parseIfThenElseCondition(schema, conditionJoiSpec, definitions, level)
606
+ } else {
607
+ this._setConditionCompositionStyle(schema, conditionJoiSpec, definitions, level)
608
+ }
609
+ }
610
+
611
+ return schema
612
+ }
613
+
614
+ _parseIfThenElseCondition(schema, conditionSpec, definitions, level) {
615
+ if (conditionSpec.ref) {
616
+ // Currently, if there is reference, if-then-else style is not supported
617
+ // To use it, the condition must be defined in parent level using schema-style is
618
+ return this._setConditionCompositionStyle(schema, conditionSpec, definitions, level)
619
+ }
620
+
621
+ schema.if = this.parse(conditionSpec.is, definitions, level)
622
+
623
+ if (conditionSpec.then) {
624
+ schema.then = this.parse(conditionSpec.then, definitions, level)
625
+ }
626
+ if (conditionSpec.otherwise) {
627
+ schema.else = this.parse(conditionSpec.otherwise, definitions, level)
628
+ }
629
+ return schema
630
+ }
631
+
632
+ _parseSwitchCondition(ifThenStyle, schema, condition, definitions, level) {
633
+ // Switch cannot be used if the joi condition is a schema
634
+ // Hence, condition.ref should always exists
635
+ const anyOf = []
636
+ for (const switchCondition of condition.switch) {
637
+ if (ifThenStyle && !condition.ref) {
638
+ const innerSchema = this._parseIfThenElseCondition(
639
+ {}, switchCondition, definitions, level + 1)
640
+
641
+ anyOf.push(innerSchema)
642
+ } else {
643
+ if (switchCondition.then) {
644
+ anyOf.push(this.parse(switchCondition.then, definitions, level + 1))
645
+ }
646
+
647
+ if (switchCondition.otherwise) {
648
+ anyOf.push(this.parse(switchCondition.otherwise, definitions, level + 1))
564
649
  }
565
650
  }
651
+ }
566
652
 
653
+ if (anyOf.length > 1) {
654
+ schema.anyOf = anyOf
655
+ } else {
656
+ _.merge(schema, oneOf[0])
657
+ }
658
+ }
659
+
660
+ _setConditionCompositionStyle(schema, condition, definitions, level) {
661
+ if (condition.ref) {
662
+ // If the condition is refering to other field, cannot use the `is` condition for composition
663
+ // Simple choise between then / otherwise
664
+ const oneOf = schema.oneOf || []
567
665
  if (condition.then) {
568
- schema.oneOf.push(this.parse(condition.then, definitions, level + 1))
666
+ oneOf.push(this.parse(condition.then, definitions, level + 1))
569
667
  }
570
-
571
668
  if (condition.otherwise) {
572
- schema.oneOf.push(this.parse(condition.otherwise, definitions, level + 1))
669
+ oneOf.push(this.parse(condition.otherwise, definitions, level + 1))
573
670
  }
671
+ if (oneOf.length > 1) {
672
+ schema.oneOf = oneOf
673
+ } else {
674
+ _.merge(schema, oneOf[0])
675
+ }
676
+ } else {
677
+ // Before Draft 7, you can express an “if-then” conditional using the Schema Composition keywords
678
+ // and a boolean algebra concept called “implication”.
679
+ // A -> B(pronounced, A implies B) means that if A is true, then B must also be true.
680
+ // It can be expressed as !A || B
681
+
682
+ // Variations of implication can express the same things you can express with the if/then/else keywords.
683
+ // if/then can be expressed as A -> B, if/else can be expressed as !A -> B,
684
+ // and if/then/else can be expressed as A -> B AND !A -> C
685
+ if (condition.is && condition.then && condition.otherwise) {
686
+ schema.allOf = [
687
+ {
688
+ anyOf: [
689
+ {
690
+ not: this.parse(condition.is, definitions, level + 1)
691
+ },
692
+ this.parse(condition.then, definitions, level + 1)
693
+ ]
694
+ },
695
+ {
696
+ anyOf: [
697
+ this.parse(condition.is, definitions, level + 1),
698
+ this.parse(condition.otherwise, definitions, level + 1)
699
+ ]
700
+ }
701
+ ]
702
+ } else if (condition.is && condition.then) {
703
+ schema.anyOf = [
704
+ {
705
+ not: this.parse(condition.is, definitions, level + 1)
706
+ },
707
+ this.parse(condition.then, definitions, level + 1)
708
+ ]
709
+ }
710
+ }
574
711
 
712
+ return schema
713
+ }
714
+
715
+ _setConst(schema, fieldDefn) {
716
+ const enumList = fieldDefn[this.enumFieldName]
717
+ const filteredEnumList = enumList ? _.filter(enumList, e => !isJoiOverride(e)) : enumList
718
+ if (fieldDefn.flags && fieldDefn.flags.only && _.size(filteredEnumList) === 1) {
719
+ schema.const = filteredEnumList[0]
575
720
  delete schema.type
721
+ }
722
+ }
723
+
724
+ _setAnyProperties(schema) {
725
+ if (schema.type !== 'any') {
576
726
  return
577
727
  }
578
728
 
@@ -6,7 +6,8 @@ class JoiOpenApiSchemaParser extends JoiJsonSchemaParser {
6
6
  super(_.merge({
7
7
  logicalOpParser: {
8
8
  xor: null,
9
- with: null
9
+ with: null,
10
+ without: null
10
11
  }
11
12
  }, opts))
12
13
  }
@@ -69,6 +70,10 @@ class JoiOpenApiSchemaParser extends JoiJsonSchemaParser {
69
70
  return schema
70
71
  }
71
72
 
73
+ _isIfThenElseSupported() {
74
+ return false
75
+ }
76
+
72
77
  _getLocalSchemaBasePath() {
73
78
  return '#/components/schemas'
74
79
  }
@@ -82,9 +87,41 @@ class JoiOpenApiSchemaParser extends JoiJsonSchemaParser {
82
87
  }
83
88
 
84
89
  _addNullTypeIfNullable(fieldSchema, fieldDefn) {
85
- const enums = _.get(fieldDefn, this.enumFieldName)
86
- if (Array.isArray(enums) && enums.includes(null)) {
90
+ super._addNullTypeIfNullable(fieldSchema, fieldDefn)
91
+
92
+ if (fieldSchema.type === 'null') {
93
+ delete fieldSchema.type
94
+ fieldSchema.nullable = true
95
+ } else if (_.isArray(fieldSchema.type) && fieldSchema.type.includes('null')) {
96
+ _.remove(fieldSchema.type, (i) => {
97
+ return i === 'null'
98
+ })
87
99
  fieldSchema.nullable = true
100
+
101
+ if (fieldSchema.type.length === 1) {
102
+ fieldSchema.type = fieldSchema.type[0]
103
+ }
104
+ }
105
+
106
+ if (_.isArray(fieldSchema.type)) {
107
+ // anyOf might exist for When / Alternative case
108
+ fieldSchema.anyOf = fieldSchema.anyOf || []
109
+
110
+ _.forEach(fieldSchema.type, (t) => {
111
+ const typeExisted = _.some(fieldSchema.anyOf, (condition) => {
112
+ return condition.type === t
113
+ })
114
+
115
+ if (!typeExisted) {
116
+ const def = { type: t }
117
+ if (t === 'array') {
118
+ def.items = {}
119
+ }
120
+ fieldSchema.anyOf.push(def)
121
+ }
122
+ })
123
+
124
+ delete fieldSchema.type
88
125
  }
89
126
  }
90
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joi-to-json",
3
- "version": "3.1.2",
3
+ "version": "4.0.0",
4
4
  "description": "joi to JSON / OpenAPI Schema Converter",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -28,16 +28,17 @@
28
28
  "semver-compare": "^1.0.0"
29
29
  },
30
30
  "devDependencies": {
31
+ "@jest/transform": "^29.6.2",
31
32
  "ajv": "^8.12.0",
32
33
  "ajv-draft-04": "^1.0.0",
33
- "eslint": "^8.31.0",
34
- "jest": "^29.3.1",
34
+ "eslint": "^8.47.0",
35
+ "jest": "^29.6.2",
35
36
  "joi-12": "npm:@commercial/joi@^12.1.0",
36
37
  "joi-13": "npm:joi@^13.7.0",
37
38
  "joi-14": "npm:joi@^14.3.1",
38
39
  "joi-15": "npm:@hapi/joi@^15.1.1",
39
40
  "joi-16": "npm:@hapi/joi@^16.1.8",
40
- "joi-17": "npm:joi@^17.6.0"
41
+ "joi-17": "npm:joi@^17.9.2"
41
42
  },
42
43
  "types": "index.d.ts",
43
44
  "files": [
package/CHANGELOG.md DELETED
@@ -1,11 +0,0 @@
1
- ### 3.1.1
2
-
3
- - Fix Typescript definition.
4
-
5
- ### 3.1.0
6
-
7
- - Add support for relation operator `with` and `without`.
8
-
9
- ### 3.0.0
10
-
11
- - [BREAKING] Default enable feature: relation operator `and`, `or`, `nand`, `xor`, `oxor`.