Repository: BRIKEV/express-jsdoc-swagger
Pull request #7 (2020-05-30 00:47:51)

Parameter request body


Patch:
diff --git a/transforms/paths/content.js b/transforms/paths/content.js
new file mode 100644
index 0000000..cadd72e
--- /dev/null
+++ b/transforms/paths/content.js
@@ -0,0 +1,14 @@
+const getSchema = require('./schema');
+
+const DEFAULT_CONTENT_TYPE = 'application/json';
+
+const getContent = (type, contentType) => {
+  const schema = getSchema(type);
+  return {
+    [contentType || DEFAULT_CONTENT_TYPE]: {
+      schema,
+    },
+  };
+};
+
+module.exports = getContent;
diff --git a/transforms/paths/index.js b/transforms/paths/index.js
index 787dcf0..2a6af9c 100644
--- a/transforms/paths/index.js
+++ b/transforms/paths/index.js
@@ -1,7 +1,9 @@
 const debug = require('debug')('express-jsdoc-swagger:transforms:paths');
 const responsesGenerator = require('./responses');
 const parametersGenerator = require('./parameters');
+const requestBody = require('./requestBody');
 const { getTagInfo, getTagsInfo } = require('../utils/tags');
+const validBodyMethods = require('./validRequestBodyMethods');
 
 const formatTags = (tags = []) => tags.map(({ description: name }) => name);
 
@@ -26,6 +28,7 @@ const parsePath = path => {
         responses,
         parameters,
         tags: formatTags(tagsValues),
+        ...(validBodyMethods[lowerCaseMethod] ? { requestBody: requestBody(paramValues) } : {}),
       },
     },
   };
diff --git a/transforms/paths/parameters.js b/transforms/paths/parameters.js
index 7463ab4..3e6444b 100644
--- a/transforms/paths/parameters.js
+++ b/transforms/paths/parameters.js
@@ -3,16 +3,22 @@ const getSchema = require('./schema');
 
 const REQUIRED = 'required';
 const ALLOW_EMPTY_VALUE = 'allowEmptyValue';
-const IS_DEPRECATED = 'deprecated';
+const DEPRECATED = 'deprecated';
+const BODY_PARAM = 'body';
+
+const defaultParseParameter = {};
 
 const parseParameter = param => {
   const [name, inOption, ...extraOptions] = param.name.split('.');
   if (!name || !inOption) {
-    return {};
+    return defaultParseParameter;
+  }
+  if (inOption === BODY_PARAM) {
+    return defaultParseParameter;
   }
   const isRequired = extraOptions.includes(REQUIRED);
   const allowEmptyValue = extraOptions.includes(ALLOW_EMPTY_VALUE);
-  const isDeprecated = extraOptions.includes(IS_DEPRECATED);
+  const isDeprecated = extraOptions.includes(DEPRECATED);
   const options = {
     name,
     in: inOption,
diff --git a/transforms/paths/requestBody.js b/transforms/paths/requestBody.js
new file mode 100644
index 0000000..8b80e99
--- /dev/null
+++ b/transforms/paths/requestBody.js
@@ -0,0 +1,49 @@
+const setProperty = require('../utils/setProperty')('parameter');
+const getContent = require('./content');
+const mapDescription = require('../utils/mapDescription');
+
+const REQUIRED = 'required';
+const BODY_PARAM = 'body';
+
+const parseBodyParameter = (currentState, body) => {
+  const [name, inOption, ...extraOptions] = body.name.split('.');
+  if (inOption !== BODY_PARAM) {
+    return {};
+  }
+  const isRequired = extraOptions.includes(REQUIRED);
+  const [description, contentType] = mapDescription(body.description);
+  const options = {
+    name,
+    required: isRequired,
+    description,
+  };
+  return {
+    ...currentState,
+    description: setProperty(options, 'description', {
+      type: 'string',
+    }),
+    required: setProperty(options, 'required', {
+      type: 'boolean',
+      defaultValue: false,
+    }),
+    content: {
+      ...currentState.content,
+      ...getContent(body.type, contentType),
+    },
+  };
+};
+
+const bodyParams = ({ name }) => name.includes('request.body');
+
+const INITIAL_STATE = { content: {} };
+
+const requestBodyGenerator = (params = []) => {
+  if (!params || !Array.isArray(params)) return {};
+  const bodyValues = params.filter(bodyParams);
+  const requestBody = bodyValues.reduce((acc, body) => (
+    { ...acc, ...parseBodyParameter(acc, body) }
+  ), INITIAL_STATE);
+  return requestBody;
+};
+
+module.exports = requestBodyGenerator;
diff --git a/transforms/paths/responses.js b/transforms/paths/responses.js
index 7f30484..a6acea3 100644
--- a/transforms/paths/responses.js
+++ b/transforms/paths/responses.js
@@ -1,9 +1,7 @@
 const chalk = require('chalk');
 
 const STATUS_CODES = require('./validStatusCodes');
-const getSchema = require('./schema');
-
-const DEFAULT_CONTENT_TYPE = 'application/json';
+const getContent = require('./content');
 
 const responsesGenerator = (returnValues = []) => {
   const mapDescription = description => description.split(' - ');
@@ -14,15 +12,14 @@ const responsesGenerator = (returnValues = []) => {
       console.warn(chalk.yellow(`Status ${status} is not valid to create a response`));
       return {};
     }
-    const schema = getSchema(value.type);
+    const hasOldContent = (acc[status] && acc[status].content);
     return {
       ...acc,
       [status]: {
         description: responseDescription,
         content: {
-          [contentType || DEFAULT_CONTENT_TYPE]: {
-            schema,
-          },
+          ...(hasOldContent ? { ...acc[status].content } : {}),
+          ...getContent(value.type, contentType),
         },
       },
     };
diff --git a/transforms/paths/validRequestBodyMethods.js b/transforms/paths/validRequestBodyMethods.js
new file mode 100644
index 0000000..b54a64f
--- /dev/null
+++ b/transforms/paths/validRequestBodyMethods.js
@@ -0,0 +1,14 @@
+
+const validRequestBodyMethods = {
+  get: false,
+  delete: false,
+  head: false,
+  post: true,
+  put: true,
+  connect: true,
+  options: true,
+  trace: true,
+  patch: true,
+};
+
+module.exports = validRequestBodyMethods;
diff --git a/transforms/utils/mapDescription.js b/transforms/utils/mapDescription.js
new file mode 100644
index 0000000..6318005
--- /dev/null
+++ b/transforms/utils/mapDescription.js
@@ -0,0 +1,6 @@
+
+const DECRIPTION_DIVIDER = ' - ';
+
+const mapDescription = description => description.split(DECRIPTION_DIVIDER);
+
+module.exports = mapDescription;


Test patch:
diff --git a/test/transforms/paths/parameters.test.js b/test/transforms/paths/parameters.test.js
index 3d4c6b4..c4c1e12 100644
--- a/test/transforms/paths/parameters.test.js
+++ b/test/transforms/paths/parameters.test.js
@@ -144,4 +144,75 @@ describe('params tests', () => {
     const result = setPaths({}, parsedJSDocs);
     expect(result).toEqual(expected);
   });
+
+  it('should parse jsdoc path reference params', () => {
+    const jsodInput = [`
+      /**
+       * GET /api/v1
+       * @param {Song} name.query.required - name param description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          get: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [{
+              allowEmptyValue: false,
+              deprecated: false,
+              description: 'name param description',
+              in: 'query',
+              name: 'name',
+              required: true,
+              schema: {
+                $ref: '#/components/schemas/Song',
+              },
+            }],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc path params with array of references', () => {
+    const jsodInput = [`
+      /**
+       * GET /api/v1
+       * @param {array<Song>} name.query.required.deprecated - name param description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          get: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [{
+              allowEmptyValue: false,
+              deprecated: true,
+              description: 'name param description',
+              in: 'query',
+              name: 'name',
+              required: true,
+              schema: {
+                type: 'array',
+                items: {
+                  $ref: '#/components/schemas/Song',
+                },
+              },
+            }],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
 });
diff --git a/test/transforms/paths/requestBody.test.js b/test/transforms/paths/requestBody.test.js
new file mode 100644
index 0000000..34473bd
--- /dev/null
+++ b/test/transforms/paths/requestBody.test.js
@@ -0,0 +1,233 @@
+const jsdocInfo = require('../../../consumers/jsdocInfo');
+const setPaths = require('../../../transforms/paths');
+
+describe('request body tests', () => {
+  it('should parse jsdoc request body', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1
+       * @param {string} request.body.required - name body description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          post: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'string',
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc request body with array type', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1
+       * @param {array<string>} request.body.required - name body description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          post: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'array',
+                    items: {
+                      type: 'string',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc path multiple bodys', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1
+       * @param {number} body
+       * @param {array<string>} request.body.required - name body description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          post: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'array',
+                    items: {
+                      type: 'string',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc path reference bodys', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1
+       * @param {Song} request.body.required - name body description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          post: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    $ref: '#/components/schemas/Song',
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc request body with array of references', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1
+       * @param {array<Song>} request.body.required - name body description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          post: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'array',
+                    items: {
+                      $ref: '#/components/schemas/Song',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should parse jsdoc request body with array of references for different content type', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1
+       * @param {array<Song>} request.body.required - name body description
+       * @param {Song} request.body.required - name body description - application/xml
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          post: {
+            summary: '',
+            responses: {},
+            tags: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'array',
+                    items: {
+                      $ref: '#/components/schemas/Song',
+                    },
+                  },
+                },
+                'application/xml': {
+                  schema: {
+                    $ref: '#/components/schemas/Song',
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+});
diff --git a/test/transforms/paths/responses.test.js b/test/transforms/paths/responses.test.js
index 4f93dcc..c970da6 100644
--- a/test/transforms/paths/responses.test.js
+++ b/test/transforms/paths/responses.test.js
@@ -190,4 +190,57 @@ describe('response tests', () => {
     const result = setPaths({}, parsedJSDocs);
     expect(result).toEqual(expected);
   });
+
+  it('should parse jsdoc path spec with more than one response and multiple content types', () => {
+    const jsodInput = [`
+      /**
+       * GET /api/v1
+       * @summary This is the summary or description of the endpoint
+       * @return {object} 200 - success response - application/json
+       * @return {object} 400 - Bad request response
+       * @return {string} 400 - Bad request response - application/xml
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1': {
+          get: {
+            summary: 'This is the summary or description of the endpoint',
+            parameters: [],
+            tags: [],
+            responses: {
+              200: {
+                description: 'success response',
+                content: {
+                  'application/json': {
+                    schema: {
+                      type: 'object',
+                    },
+                  },
+                },
+              },
+              400: {
+                description: 'Bad request response',
+                content: {
+                  'application/json': {
+                    schema: {
+                      type: 'object',
+                    },
+                  },
+                  'application/xml': {
+                    schema: {
+                      type: 'string',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
 });