Repository: BRIKEV/express-jsdoc-swagger (js)

Issue:
unsupport method: post,  header:application/x-www-form-urlencoded,  body: name=1&code=2
rt, doc only show body is json.

Pull request description:
Form parameters
### What kind of change does this PR introduce? (check at least one)
 - [X] Feature
 - [X] Test
 - [X] Docs

### Description:
This PR includes the feature of using only `.form` values for a request body instead of creating a model for each endpoint.


The definition is like this:

```
/**
 * POST /api/v1/song
 * @param {string} id.form.required - This is the song id - application/x-www-form-urlencoded
 * @param {string} title.form.required - This is the song title - application/x-www-form-urlencoded
 * @return {object} 200 - song response
 */
```

Where we use

```
* @param {string} [key].form.[!required] - [Description] - [content type]
```

This closes #86 


Patch:
diff --git a/examples/package.json b/examples/package.json
index f411e68..75ab6f4 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -19,6 +19,7 @@
     "requestBody:multiple": "node requestBody/multiple.js",
     "requestBody:components": "node requestBody/components.js",
     "requestBody:formdata": "node requestBody/formdata.js",
+    "requestBody:formParameters": "node requestBody/formParameters.js",
     "requestBody:formUrlencoded": "node requestBody/formUrlencoded.js",
     "requestBody:withExamples": "node requestBody/withExamples.js",
     "security:basic-auth": "node security/basic-auth.js",
diff --git a/examples/requestBody/formParameters.js b/examples/requestBody/formParameters.js
new file mode 100644
index 0000000..2fbbd51
--- /dev/null
+++ b/examples/requestBody/formParameters.js
@@ -0,0 +1,34 @@
+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: './formParameters.js',
+  baseDir: __dirname,
+};
+
+const app = express();
+const port = 3000;
+
+expressJSDocSwagger(app)(options);
+
+// To use form params it is neccesary to provide a description
+
+/**
+ * POST /api/v1/song
+ * @param {string} id.form.required - This is the song id - application/x-www-form-urlencoded
+ * @param {string} title.form.required - This is the song title - application/x-www-form-urlencoded
+ * @return {object} 200 - song response
+ */
+app.post('/api/v1/songs', (req, res) => res.json({}));
+
+
+app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`));
diff --git a/package.json b/package.json
index 26b15a5..e7990dc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "express-jsdoc-swagger",
-  "version": "1.0.5",
+  "version": "1.0.6",
   "description": "Swagger OpenAPI 3.x generator",
   "main": "index.js",
   "dependencies": {
diff --git a/transforms/paths/content.js b/transforms/paths/content.js
index 62ef3d9..af88466 100644
--- a/transforms/paths/content.js
+++ b/transforms/paths/content.js
@@ -2,11 +2,14 @@ const getSchema = require('./schema');
 
 const DEFAULT_CONTENT_TYPE = 'application/json';
 
-const getContent = entity => (type, contentType, originalValue, examples) => {
+const getContent = entity => (type, contentType, originalValue, examples, extendSchema = {}) => {
   const schema = getSchema(entity, originalValue)(type);
   return {
     [contentType || DEFAULT_CONTENT_TYPE]: {
-      schema,
+      schema: {
+        ...schema,
+        ...extendSchema,
+      },
       examples,
     },
   };
diff --git a/transforms/paths/formParams.js b/transforms/paths/formParams.js
new file mode 100644
index 0000000..44a2191
--- /dev/null
+++ b/transforms/paths/formParams.js
@@ -0,0 +1,38 @@
+const merge = require('merge');
+const getContent = require('./content')('@paramFormBody');
+const mapDescription = require('../utils/mapDescription');
+
+const DEFAULT_CONTENT_TYPE = 'application/json';
+
+const formParams = (currentState, key, body, isRequired, requestExamples) => {
+  const bodyType = { name: 'object' };
+  const [description, contentType] = mapDescription(body.description);
+  let requiredValues = isRequired ? [key] : [];
+  const paramContentType = contentType || DEFAULT_CONTENT_TYPE;
+  if (currentState.content[paramContentType]) {
+    requiredValues = [
+      ...(currentState.content[paramContentType].schema.required || []),
+      key,
+    ];
+  }
+  const schema = {
+    properties: {
+      [key]: {
+        type: body.type.name,
+        description,
+      },
+    },
+    required: requiredValues,
+  };
+  return {
+    content: {
+      ...merge.recursive(
+        true,
+        currentState.content,
+        getContent(bodyType, contentType, description, requestExamples, schema),
+      ),
+    },
+  };
+};
+
+module.exports = formParams;
diff --git a/transforms/paths/index.js b/transforms/paths/index.js
index 400336f..0dd8de0 100644
--- a/transforms/paths/index.js
+++ b/transforms/paths/index.js
@@ -25,7 +25,7 @@ const setRequestBody = (lowerCaseMethod, bodyValues, requestExamples) => {
   return bodyMethods[lowerCaseMethod] && hasBodyValues ? { requestBody } : {};
 };
 
-const bodyParams = ({ name }) => name.includes('request.body');
+const bodyParams = ({ name }) => name.includes('request.body') || name.includes('.form');
 
 const pathValues = tags => {
   const examplesValues = getTagsInfo(tags, 'example');
diff --git a/transforms/paths/parameters.js b/transforms/paths/parameters.js
index 80fee61..aaec499 100644
--- a/transforms/paths/parameters.js
+++ b/transforms/paths/parameters.js
@@ -6,6 +6,7 @@ const getSchema = require('./schema');
 const REQUIRED = 'required';
 const DEPRECATED = 'deprecated';
 const BODY_PARAM = 'body';
+const FORM_TYPE = 'form';
 
 const defaultParseParameter = {};
 
@@ -42,7 +43,7 @@ const wrongInOption = paramValue => {
 
 const parseParameter = param => {
   const [name, inOption, ...extraOptions] = param.name.split('.');
-  if (!name || !inOption) {
+  if (!name || !inOption || inOption === FORM_TYPE) {
     return defaultParseParameter;
   }
   if (inOption === BODY_PARAM) {
diff --git a/transforms/paths/requestBody.js b/transforms/paths/requestBody.js
index a1dee76..91e61ee 100644
--- a/transforms/paths/requestBody.js
+++ b/transforms/paths/requestBody.js
@@ -1,9 +1,10 @@
 const setProperty = require('../utils/setProperty')('parameter');
 const getContent = require('./content')('@paramBody');
 const mapDescription = require('../utils/mapDescription');
+const formParams = require('./formParams');
 
 const REQUIRED = 'required';
-const BODY_PARAM = 'body';
+const FORM_TYPE = 'form';
 
 const formatExamples = (exampleValues = []) => exampleValues
   .reduce((exampleMap, example, i) => ({
@@ -15,11 +16,9 @@ const formatExamples = (exampleValues = []) => exampleValues
   }), {});
 
 const parseBodyParameter = (currentState, body, examples) => {
-  const [name, inOption, ...extraOptions] = body.name.split('.');
-  if (inOption !== BODY_PARAM) {
-    return {};
-  }
+  const [name, ...extraOptions] = body.name.split('.');
   const isRequired = extraOptions.includes(REQUIRED);
+  const hasForm = extraOptions.includes(FORM_TYPE);
   const [description, contentType] = mapDescription(body.description);
   const options = {
     name,
@@ -32,6 +31,10 @@ const parseBodyParameter = (currentState, body, examples) => {
     requestExamples = formatExamples(examples);
   }
 
+  if (hasForm) {
+    return formParams(currentState, name, body, isRequired, requestExamples);
+  }
+
   return {
     ...currentState,
     description: setProperty(options, 'description', {


Test patch:
diff --git a/test/transforms/paths/formParameters.test.js b/test/transforms/paths/formParameters.test.js
new file mode 100644
index 0000000..b88c1d2
--- /dev/null
+++ b/test/transforms/paths/formParameters.test.js
@@ -0,0 +1,308 @@
+const jsdocInfo = require('../../../consumers/jsdocInfo');
+const setPaths = require('../../../transforms/paths');
+
+describe('form requestBody tests', () => {
+  it('should add request body using form examples', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/
+       * @param {string} id.form.required - id description
+       * @param {string} title.form.required - title description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/': {
+          post: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [],
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      id: {
+                        type: 'string',
+                        description: 'id description',
+                      },
+                      title: {
+                        type: 'string',
+                        description: 'title description',
+                      },
+                    },
+                    required: ['id', 'title'],
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should add request body using form examples with params', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/
+       * @param {string} name.path.required - name param description
+       * @param {string} id.form.required - id description
+       * @param {string} title.form.required - title description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/': {
+          post: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [
+              {
+                deprecated: false,
+                description: 'name param description',
+                in: 'path',
+                name: 'name',
+                required: true,
+                schema: {
+                  type: 'string',
+                },
+              },
+            ],
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      id: {
+                        type: 'string',
+                        description: 'id description',
+                      },
+                      title: {
+                        type: 'string',
+                        description: 'title description',
+                      },
+                    },
+                    required: ['id', 'title'],
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should not fail when a request.body is sent', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/
+       * @param {string} request.body.required - name body description
+       * @param {string} id.form.required - id description
+       * @param {string} title.form.required - title description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/': {
+          post: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [],
+            requestBody: {
+              description: 'name body description',
+              required: true,
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      id: {
+                        type: 'string',
+                        description: 'id description',
+                      },
+                      title: {
+                        type: 'string',
+                        description: 'title description',
+                      },
+                    },
+                    required: ['id', 'title'],
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should add request body for different application content type', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/
+       * @param {string} name.path.required - name param description
+       * @param {string} id.form.required - id description - application/x-www-form-urlencoded
+       * @param {string} title.form.required - title description
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/': {
+          post: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [
+              {
+                deprecated: false,
+                description: 'name param description',
+                in: 'path',
+                name: 'name',
+                required: true,
+                schema: {
+                  type: 'string',
+                },
+              },
+            ],
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      title: {
+                        type: 'string',
+                        description: 'title description',
+                      },
+                    },
+                    required: ['title'],
+                  },
+                },
+                'application/x-www-form-urlencoded': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      id: {
+                        type: 'string',
+                        description: 'id description',
+                      },
+                    },
+                    required: ['id'],
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('should add request body for different application content type with an example', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/
+       * @param {string} name.path.required - name param description
+       * @param {string} id.form.required - id description - application/x-www-form-urlencoded
+       * @param {string} title.form.required - title description
+       * @example request - example payload
+       * sample input string
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/': {
+          post: {
+            deprecated: false,
+            summary: '',
+            responses: {},
+            tags: [],
+            security: [],
+            parameters: [
+              {
+                deprecated: false,
+                description: 'name param description',
+                in: 'path',
+                name: 'name',
+                required: true,
+                schema: {
+                  type: 'string',
+                },
+              },
+            ],
+            requestBody: {
+              content: {
+                'application/json': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      title: {
+                        type: 'string',
+                        description: 'title description',
+                      },
+                    },
+                    required: ['title'],
+                  },
+                  examples: {
+                    example1: {
+                      summary: 'example payload',
+                      value: 'sample input string',
+                    },
+                  },
+                },
+                'application/x-www-form-urlencoded': {
+                  schema: {
+                    type: 'object',
+                    properties: {
+                      id: {
+                        type: 'string',
+                        description: 'id description',
+                      },
+                    },
+                    required: ['id'],
+                  },
+                  examples: {
+                    example1: {
+                      summary: 'example payload',
+                      value: 'sample input string',
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+});