Repository: BRIKEV/express-jsdoc-swagger
Pull request #147 (2021-04-14 18:17:24)

fix: multiple data types without swagger keywords
### What kind of change does this PR introduce? (check at least one)
 - [X] Bugfix
 - [ ] Feature
 - [X] Test
 - [X] Docs
 - [ ] Refactor
 - [ ] Build-related changes
 - [ ] Other, please describe:

### Description:

When parsing properties with multiple data types, there was an issue if the user did not add one of the Swagger keywords (`oneOf`, `anyOf` or `allOf`) as the first item in the list, for example:

```
/**
 * @typedef {object} Song
 * @property {string} title.required
 * @property {string} artist
 * @property {number} year
 * @property {object|number} releaseDate
 */
```

In this scenario, the generated schema for the `releaseDate` property would only contain the second type:

```
{
  'description': '',
  'type': 'number',
}
```

As the library assumed the first item to be a keyword and not a real type, the `object` type would be completely ignored and not appear into the generated schema. 

This PR fixes this issue, making the usage of `oneOf`, `anyOf` or `allOf` keywords optional. If the user chooses to write the different data types without specifying a keyword, the **behaviour of `oneOf` will be assumed by default**. 

### Task list:

:bug: **Bug**
- [x] If first item of the data type list is not a keyword, iterate trough the whole `elements` array instead of excluding the first. 

:heavy_check_mark: **Test**
- [x] Fix description for some test in `combineSchema.test.js`.
- [x] Add new test to validate schemas for properties with multiple types written in vanilla JSDoc syntax. 

:arrows_counterclockwise: **Refactor**
- [x] Remove test that forced the first item in data type list to be a Swagger keyword. 

:memo: **Documentation**
- [x] Add JSDoc comments to methods in `combineSchema.js`.


Patch:
diff --git a/examples/nullableFields/index.js b/examples/nullableFields/index.js
index 5214296..ba925ca 100644
--- a/examples/nullableFields/index.js
+++ b/examples/nullableFields/index.js
@@ -27,6 +27,7 @@ expressJSDocSwagger(app)(options);
  * @property {string} artist - The artist
  * @property {integer} year - The year - int64
  * @property {oneOf|string|null} album - The album to which the song belongs (if any)
+ * @property {(string[]|null)} tags - Associate tags (if any)
  */
 
 /**
diff --git a/transforms/utils/combineSchema.js b/transforms/utils/combineSchema.js
index d6c19e2..7182bf6 100644
--- a/transforms/utils/combineSchema.js
+++ b/transforms/utils/combineSchema.js
@@ -3,37 +3,64 @@ const { refSchema } = require('./refSchema');
 
 const VALID_TYPES = ['oneOf', 'anyOf', 'allOf'];
 
-const validType = type => VALID_TYPES.includes(type);
+/**
+ *  This method checks the first item of the data type list to validate if
+ * it contains any of the keywords representing the different schema types
+ * in Swagger: 'oneOf', 'anyOf' or 'allOf'.
+ *
+ * @param {object[]} elements - List of data types for the property
+ * @returns {string|null} Name of the schema type (if any)
+ */
+const getSchemaType = elements => {
+  const schemaType = elements[0].name;
 
+  if (!VALID_TYPES.includes(schemaType)) {
+    errorMessage(`SchemaType ${schemaType} invalid, it should be one of these ${VALID_TYPES.join(', ')}`);
+    return null;
+  }
+
+  return schemaType;
+};
+
+/**
+ *  This method receives an array of data types passed down to the
+ * 'property' annotation, for example:
+ *
+ *    > '{oneOf|string|null}'.
+ *
+ *  The aim of this method is to process this array and generate
+ * the schema for the property, including a list of its types.
+ *
+ * @param {object[]} elements - List of data types for the property
+ * @returns Swagger schema for the property
+ */
 const combineSchema = elements => {
   let schema = {};
   if (!elements || elements.length === 0) return schema;
 
-  // If param has multiple types, check if null is part of them and remove it from array
+  // Check if 'null' is part of the listed types and remove it from the array
   const nullIndex = elements.findIndex(el => el.type === 'NullLiteral');
   if (nullIndex > 0) {
     elements.splice(nullIndex, 1);
     schema = { nullable: true };
   }
 
-  const schemaType = elements[0].name;
-  const [, ...types] = elements;
-
-  if (validType(schemaType)) {
-    if (types.length > 1 || schemaType === 'allOf') {
-      schema = {
-        ...schema,
-        [schemaType]: types.map(type => refSchema(type)),
-      };
-    } else {
-      // If there's only a type in the list, don't wrap it in 'oneOf' or 'anyOf'
-      schema = {
-        ...schema,
-        ...refSchema(types[0]),
-      };
-    }
+  const schemaType = getSchemaType(elements);
+  const types = !schemaType ? elements : elements.slice(1);
+
+  // If there are multiple types in the list, wrap them into a schema type
+  // ('oneOf' will be used by default if none is specified)
+  if (types.length > 1 || schemaType === 'allOf') {
+    schema = {
+      ...schema,
+      [schemaType || 'oneOf']: types.map(type => refSchema(type)),
+    };
   } else {
-    errorMessage(`SchemaType ${schemaType} invalid, it should be one of these ${VALID_TYPES.join(', ')}`);
+    // If there's only a type in the list, don't wrap it in 'oneOf' or 'anyOf' blocks
+    schema = {
+      ...schema,
+      ...refSchema(types[0]),
+    };
   }
 
   return schema;


Test patch:
diff --git a/test/transforms/paths/combineSchemas.test.js b/test/transforms/paths/combineSchemas.test.js
index 3871f31..92eed3e 100644
--- a/test/transforms/paths/combineSchemas.test.js
+++ b/test/transforms/paths/combineSchemas.test.js
@@ -47,46 +47,7 @@ test('should parse jsdoc path response with oneOf keyword', () => {
   expect(result).toEqual(expected);
 });
 
-test('should not parse when type is invalid', () => {
-  global.console = { ...global.console, warn: jest.fn() };
-  const jsodInput = [`
-      /**
-       * GET /api/v1
-       * @summary This is the summary of the endpoint
-       * @return {invalid|Song|Album} 200 - success response - application/json
-       */
-    `];
-  const expected = {
-    paths: {
-      '/api/v1': {
-        get: {
-          deprecated: false,
-          summary: 'This is the summary of the endpoint',
-          parameters: [],
-          tags: [],
-          security: [],
-          responses: {
-            200: {
-              description: 'success response',
-              content: {
-                'application/json': {
-                  schema: {},
-                },
-              },
-            },
-          },
-        },
-      },
-    },
-  };
-  const parsedJSDocs = jsdocInfo()(jsodInput);
-  const result = setPaths({}, parsedJSDocs);
-  expect(result).toEqual(expected);
-  // eslint-disable-next-line
-  expect(console.warn).toHaveBeenCalled();
-});
-
-test('should not parse component with anyOf keyword', () => {
+test('should parse component with anyOf keyword', () => {
   const jsodInput = [`
     /**
      * A song
@@ -193,7 +154,8 @@ test('should parse jsdoc path reference params with allOf keyword', () => {
   const result = setPaths({}, parsedJSDocs);
   expect(result).toEqual(expected);
 });
-test('should not parse component with anyOf array keyword', () => {
+
+test('should parse component with anyOf array keyword', () => {
   const jsodInput = [`
     /**
      * A song
@@ -271,3 +233,63 @@ test('should not parse component with anyOf array keyword', () => {
   const result = parseComponents({}, parsedJSDocs);
   expect(result).toEqual(expected);
 });
+
+test('should parse component with jsdoc syntax for multiple data types', () => {
+  const jsodInput = [`
+    /**
+     * A song
+     * @typedef {object} Song
+     * @property {string} title.required
+     * @property {string} artist
+     * @property {number} year
+     * @property {(string|null)} album
+     * @property {object|number} releaseDate
+     */
+  `];
+  const expected = {
+    components: {
+      schemas: {
+        Song: {
+          type: 'object',
+          required: [
+            'title',
+          ],
+          description: 'A song',
+          properties: {
+            title: {
+              type: 'string',
+              description: '',
+            },
+            artist: {
+              type: 'string',
+              description: '',
+            },
+            year: {
+              type: 'number',
+              description: '',
+            },
+            album: {
+              type: 'string',
+              description: '',
+              nullable: true,
+            },
+            releaseDate: {
+              description: '',
+              oneOf: [
+                {
+                  type: 'object',
+                },
+                {
+                  type: 'number',
+                },
+              ],
+            },
+          },
+        },
+      },
+    },
+  };
+  const parsedJSDocs = jsdocInfo()(jsodInput);
+  const result = parseComponents({}, parsedJSDocs);
+  expect(result).toEqual(expected);
+});