Repository: BRIKEV/express-jsdoc-swagger
Pull request #54 (2020-06-11 22:24:34)

Enum feature
### What kind of change does this PR introduce? (check at least one)
 - [ ] Bugfix
 - [X] Feature
 - [X] Test
 - [ ] Docs
 - [ ] Refactor
 - [ ] Build-related changes
 - [ ] Other, please describe:

### Description:
This PR includes enum feature for parameters and components


Patch:
diff --git a/examples/components/enum.js b/examples/components/enum.js
new file mode 100644
index 0000000..2ce5b0d
--- /dev/null
+++ b/examples/components/enum.js
@@ -0,0 +1,46 @@
+const express = require('express');
+
+const logger = require('../utils/logger');
+const expressJSDocSwagger = require('../..');
+
+const options = {
+  info: {
+    version: '1.0.0',
+    title: 'Albums store',
+    license: {
+      name: 'MIT',
+    },
+  },
+  filesPattern: './enum.js',
+  baseDir: __dirname,
+};
+
+const app = express();
+const port = 3000;
+
+expressJSDocSwagger(app)(options);
+
+/**
+ * A song
+ * @typedef {object} Song
+ * @property {string} title.required - The title
+ * @property {string} artist - The artist
+ * @property {string} license - The year - int64 - enum:ISC,MIT
+ */
+
+/**
+ * Author model
+ * @typedef {object} Author
+ * @property {string} name.required - Author name
+ * @property {string} role - enum:singer,guitarrist - The author role
+ * @property {number} age - Author age - int64
+ */
+
+/**
+ * Album
+ * @typedef {object} Album
+ * @property {Song} firstSong
+ * @property {Author} author
+ */
+
+app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`));
diff --git a/examples/package.json b/examples/package.json
index 5516f81..4d3af73 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -8,6 +8,7 @@
     "parameters:simple": "node parameters/simple.js",
     "parameters:multiple": "node parameters/multiple.js",
     "parameters:components": "node parameters/components.js",
+    "parameters:enum": "node parameters/enum.js",
     "responses:simple": "node responses/simple.js",
     "responses:multiple": "node responses/multiple.js",
     "responses:components": "node responses/components.js",
@@ -18,6 +19,7 @@
     "requestBody:formdata": "node requestBody/formdata.js",
     "security:basic-auth": "node security/basic-auth.js",
     "components:simple": "node components/simple.js",
+    "components:enum": "node components/enum.js",
     "tags:simple": "node tags/simple.js",
     "combineSchemas": "node combineSchemas/index.js",
     "eventEmitter": "node eventEmitter/index.js",
diff --git a/examples/parameters/enum.js b/examples/parameters/enum.js
new file mode 100644
index 0000000..0420e55
--- /dev/null
+++ b/examples/parameters/enum.js
@@ -0,0 +1,44 @@
+const express = require('express');
+
+const logger = require('../utils/logger');
+const expressJSDocSwagger = require('../..');
+
+const options = {
+  info: {
+    version: '1.0.0',
+    title: 'Albums store',
+    license: {
+      name: 'MIT',
+    },
+  },
+  filesPattern: './enum.js',
+  baseDir: __dirname,
+};
+
+const app = express();
+const port = 3000;
+
+expressJSDocSwagger(app)(options);
+
+/**
+ * GET /api/v1
+ * @summary This is the summary or description of the endpoint
+ * @param {string} name.query.required - name param description - enum:type1,type2
+ * @return {string} 200 - success response
+ */
+app.get('/api/v1', (req, res) => res.send('Hello World!'));
+
+/**
+ * GET /api/v1/albums
+ * @summary This is the summary or description of the endpoint
+ * @param {string} name.query.required - name param description - enum:type1,type2
+ * @param {string} license.query - enum:MIT,ISC - name param description
+ * @return {object} 200 - success response - application/json
+ */
+app.get('/api/v1/albums', (req, res) => (
+  res.json([{
+    title: 'abum 1',
+  }])
+));
+
+app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`));
diff --git a/transforms/components/index.js b/transforms/components/index.js
index ae8130b..d2a2138 100644
--- a/transforms/components/index.js
+++ b/transforms/components/index.js
@@ -1,6 +1,8 @@
 const { getTagInfo, getTagsInfo } = require('../utils/tags');
 const mapDescription = require('../utils/mapDescription');
 const refSchema = require('../utils/refSchema');
+const addEnumValues = require('../utils/enumValues');
+const formatDescription = require('../utils/formatDescription');
 const combineSchema = require('../utils/combineSchema');
 
 const REQUIRED = 'required';
@@ -15,7 +17,8 @@ const formatProperties = properties => {
   return properties.reduce((acum, property) => {
     const name = getPropertyName(property);
     const type = property.type.name;
-    const [description, format] = mapDescription(property.description);
+    const [descriptionValue, enumValues] = formatDescription(property.description);
+    const [description, format] = mapDescription(descriptionValue);
     return {
       ...acum,
       [name]: {
@@ -23,6 +26,7 @@ const formatProperties = properties => {
         ...refSchema(type),
         ...combineSchema(property.type.elements),
         ...(format ? { format } : {}),
+        ...addEnumValues(enumValues),
       },
     };
   }, {});
diff --git a/transforms/paths/parameters.js b/transforms/paths/parameters.js
index 9ea7ed0..3d4fcb5 100644
--- a/transforms/paths/parameters.js
+++ b/transforms/paths/parameters.js
@@ -1,4 +1,5 @@
 const setProperty = require('../utils/setProperty')('parameter');
+const formatDescription = require('../utils/formatDescription');
 const getSchema = require('./schema');
 
 const REQUIRED = 'required';
@@ -47,15 +48,16 @@ const parseParameter = param => {
   const isRequired = extraOptions.includes(REQUIRED);
   const allowEmptyValue = extraOptions.includes(ALLOW_EMPTY_VALUE);
   const isDeprecated = extraOptions.includes(DEPRECATED);
+  const [description, enumValues] = formatDescription(param.description);
   const options = {
     name,
     in: inOption,
     required: isRequired,
     allowEmptyValue,
     deprecated: isDeprecated,
-    description: param.description,
+    description,
   };
-  const schema = getSchema(param.type);
+  const schema = getSchema(param.type, enumValues);
   return parameterPayload(options, schema);
 };
 
diff --git a/transforms/paths/responses.js b/transforms/paths/responses.js
index e8f51e3..94d76a4 100644
--- a/transforms/paths/responses.js
+++ b/transforms/paths/responses.js
@@ -1,10 +1,9 @@
 const chalk = require('chalk');
 
 const STATUS_CODES = require('./validStatusCodes');
+const mapDescription = require('../utils/mapDescription');
 const getContent = require('./content');
 
-const mapDescription = description => description.split(' - ');
-
 const hasOldContent = (value, status) => (value[status] && value[status].content);
 
 const formatResponses = values => values.reduce((acc, value) => {
diff --git a/transforms/paths/schema.js b/transforms/paths/schema.js
index 73e0418..2e6305d 100644
--- a/transforms/paths/schema.js
+++ b/transforms/paths/schema.js
@@ -1,7 +1,8 @@
 const combineSchema = require('../utils/combineSchema');
+const addEnumValues = require('../utils/enumValues');
 const refSchema = require('../utils/refSchema');
 
-const getSchema = type => {
+const getSchema = (type, enumValues = []) => {
   const nameType = type.name;
   let schema = {
     ...refSchema(nameType),
@@ -9,6 +10,7 @@ const getSchema = type => {
   schema = {
     ...schema,
     ...combineSchema(type.elements),
+    ...addEnumValues(enumValues),
   };
   const notPrimitiveType = !nameType;
   if (notPrimitiveType && !type.elements) {
diff --git a/transforms/utils/enumValues.js b/transforms/utils/enumValues.js
new file mode 100644
index 0000000..f54c1d2
--- /dev/null
+++ b/transforms/utils/enumValues.js
@@ -0,0 +1,9 @@
+
+const addEnumValues = (values = []) => {
+  if (values.length === 0) {
+    return {};
+  }
+  return { enum: values };
+};
+
+module.exports = addEnumValues;
diff --git a/transforms/utils/formatDescription.js b/transforms/utils/formatDescription.js
new file mode 100644
index 0000000..55909a5
--- /dev/null
+++ b/transforms/utils/formatDescription.js
@@ -0,0 +1,20 @@
+const mapDescription = require('./mapDescription');
+
+const ENUM_IDENTIFIER = 'enum:';
+const DESCRIPTION_DIVIDER = ' - ';
+
+const formatDescription = description => {
+  const descriptionTypes = mapDescription(description);
+  const descriptionValue = descriptionTypes.filter(value => (
+    !value.includes(ENUM_IDENTIFIER)
+  )).join(DESCRIPTION_DIVIDER);
+  const enumOption = descriptionTypes.find(value => value.includes(ENUM_IDENTIFIER));
+  if (!enumOption) {
+    return [descriptionValue];
+  }
+  const [, enumOptions] = enumOption.split('enum:');
+  const enumValues = enumOptions.split(',');
+  return [descriptionValue, enumValues];
+};
+
+module.exports = formatDescription;
diff --git a/transforms/utils/tags.js b/transforms/utils/tags.js
index 8341688..436f5be 100644
--- a/transforms/utils/tags.js
+++ b/transforms/utils/tags.js
@@ -1,9 +1,11 @@
+const mapDescription = require('./mapDescription');
+
 const getTagInfo = (tags, key) => tags.find(({ title }) => title === key);
 
 const getTagsInfo = (tags, key) => tags.filter(({ title }) => title === key);
 
 const formatDescriptionTag = desc => {
-  const [name, description] = desc.split(' - ');
+  const [name, description] = mapDescription(desc);
   return { name, description };
 };
 


Test patch:
diff --git a/test/transforms/components/index.test.js b/test/transforms/components/index.test.js
index 6c58678..72e659c 100644
--- a/test/transforms/components/index.test.js
+++ b/test/transforms/components/index.test.js
@@ -85,6 +85,96 @@ describe('parseComponents method', () => {
     expect(result).toEqual(expected);
   });
 
+  it('Should parse jsdoc component spec with enum properties', () => {
+    const jsodInput = [`
+      /**
+       * A song
+       * @typedef {object} Song
+       * @property {string} title - The title
+       * @property {string} artist - The artist - enum:value1,value2
+       * @property {number} year - The year - int64
+       */
+    `];
+    const expected = {
+      components: {
+        schemas: {
+          Song: {
+            type: 'object',
+            required: [],
+            description: 'A song',
+            properties: {
+              title: {
+                type: 'string',
+                description: 'The title',
+              },
+              artist: {
+                type: 'string',
+                description: 'The artist',
+                enum: [
+                  'value1',
+                  'value2',
+                ],
+              },
+              year: {
+                type: 'number',
+                description: 'The year',
+                format: 'int64',
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = parseComponents({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('Should parse jsdoc component spec with enum properties in different order', () => {
+    const jsodInput = [`
+      /**
+       * A song
+       * @typedef {object} Song
+       * @property {string} title - The title
+       * @property {string} artist - enum:value1,value2 - The artist
+       * @property {number} year - The year - int64
+       */
+    `];
+    const expected = {
+      components: {
+        schemas: {
+          Song: {
+            type: 'object',
+            required: [],
+            description: 'A song',
+            properties: {
+              title: {
+                type: 'string',
+                description: 'The title',
+              },
+              artist: {
+                type: 'string',
+                description: 'The artist',
+                enum: [
+                  'value1',
+                  'value2',
+                ],
+              },
+              year: {
+                type: 'number',
+                description: 'The year',
+                format: 'int64',
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = parseComponents({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
   it('Should parse jsdoc component spec with require and format properties', () => {
     const jsodInput = [`
       /**
diff --git a/test/transforms/paths/parameters.test.js b/test/transforms/paths/parameters.test.js
index 5d3a4cc..e6d46c1 100644
--- a/test/transforms/paths/parameters.test.js
+++ b/test/transforms/paths/parameters.test.js
@@ -227,4 +227,84 @@ describe('params tests', () => {
     const result = setPaths({}, parsedJSDocs);
     expect(result).toEqual(expected);
   });
+
+  it('should parse jsdoc path params with enum values', () => {
+    const jsodInput = [`
+      /**
+       * GET /api/v1
+       * @param {string} name.query.required - name param description - enum:value1,value2
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          get: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [{
+              allowEmptyValue: false,
+              deprecated: false,
+              description: 'name param description',
+              in: 'query',
+              name: 'name',
+              required: true,
+              schema: {
+                type: 'string',
+                enum: [
+                  'value1',
+                  'value2',
+                ],
+              },
+            }],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc path params with enum values in diferent order', () => {
+    const jsodInput = [`
+      /**
+       * GET /api/v1
+       * @param {string} name.query.required - enum:value1,value2 - name param description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          get: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [{
+              allowEmptyValue: false,
+              deprecated: false,
+              description: 'name param description',
+              in: 'query',
+              name: 'name',
+              required: true,
+              schema: {
+                type: 'string',
+                enum: [
+                  'value1',
+                  'value2',
+                ],
+              },
+            }],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
 });