From 41c410fa8daeaed1d5a5cb45dda7aaee43b51021 Mon Sep 17 00:00:00 2001
From: Kevin Martin <kvn.p.mrtn@gmail.com>
Date: Tue, 13 Oct 2020 06:47:55 -0700
Subject: [PATCH] feat: add coffeescript support (#52)

It's now possible to include your `.coffee` files into the result swagger specification.

Example:

```sh
swagger-jsdoc.js -d example/v2/swaggerDef.js example/v2/route.coffee
```
---
 example/v2/route.coffee            |  26 ++++++
 lib/helpers/parseApiFile.js        |   1 +
 lib/helpers/parseApiFileContent.js |  40 ++++++---
 test/helpers.spec.js               | 132 ++++++++++++++++++++++++++---
 4 files changed, 178 insertions(+), 21 deletions(-)
 create mode 100644 example/v2/route.coffee

diff --git a/example/v2/route.coffee b/example/v2/route.coffee
new file mode 100644
index 00000000..b11c1dea
--- /dev/null
+++ b/example/v2/route.coffee
@@ -0,0 +1,26 @@
+# Coffeescript Example
+
+###
+* @swagger
+* /login:
+*   post:
+*     description: Login to the application
+*     produces:
+*       - application/json
+*     parameters:
+*       - name: username
+*         description: Username to use for login.
+*         in: formData
+*         required: true
+*         type: string
+*       - name: password
+*         description: User's password.
+*         in: formData
+*         required: true
+*         type: string
+*     responses:
+*       200:
+*         description: login
+###
+app.post '/login', (req, res) ->
+  res.json req.body
\ No newline at end of file
diff --git a/lib/helpers/parseApiFile.js b/lib/helpers/parseApiFile.js
index ca30d70b..8876f55e 100644
--- a/lib/helpers/parseApiFile.js
+++ b/lib/helpers/parseApiFile.js
@@ -1,6 +1,7 @@
 const fs = require('fs');
 const path = require('path');
 const parseApiFileContent = require('./parseApiFileContent');
+
 /**
  * Parses the provided API file for JSDoc comments.
  * @function
diff --git a/lib/helpers/parseApiFileContent.js b/lib/helpers/parseApiFileContent.js
index 90feaa55..9c7833dd 100644
--- a/lib/helpers/parseApiFileContent.js
+++ b/lib/helpers/parseApiFileContent.js
@@ -12,24 +12,44 @@ const jsYaml = require('js-yaml');
  */
 function parseApiFileContent(fileContent, ext) {
   const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm;
+  const csDocRegex = /###([\s\S]*?)###/gm;
   const yaml = [];
-  const jsDocComments = [];
+  const jsdoc = [];
+  let regexResults = null;
 
-  if (ext === '.yaml' || ext === '.yml') {
-    yaml.push(jsYaml.safeLoad(fileContent));
-  } else {
-    const regexResults = fileContent.match(jsDocRegex);
-    if (regexResults) {
-      for (let i = 0; i < regexResults.length; i += 1) {
-        const jsDocComment = doctrine.parse(regexResults[i], { unwrap: true });
-        jsDocComments.push(jsDocComment);
+  switch (ext) {
+    case '.yml':
+    case '.yaml':
+      yaml.push(jsYaml.safeLoad(fileContent));
+      break;
+
+    case '.coffee':
+      regexResults = fileContent.match(csDocRegex);
+      if (regexResults) {
+        for (let i = 0; i < regexResults.length; i += 1) {
+          // Prepare input for doctrine
+          let part = regexResults[i].split('###');
+          part[0] = `/**`;
+          part[regexResults.length - 1] = '*/';
+          part = part.join('');
+          jsdoc.push(doctrine.parse(part, { unwrap: true }));
+        }
+      }
+      break;
+
+    default: {
+      regexResults = fileContent.match(jsDocRegex);
+      if (regexResults) {
+        for (let i = 0; i < regexResults.length; i += 1) {
+          jsdoc.push(doctrine.parse(regexResults[i], { unwrap: true }));
+        }
       }
     }
   }
 
   return {
     yaml,
-    jsdoc: jsDocComments,
+    jsdoc,
   };
 }
 
diff --git a/test/helpers.spec.js b/test/helpers.spec.js
index db688e91..eaf7f145 100644
--- a/test/helpers.spec.js
+++ b/test/helpers.spec.js
@@ -1,6 +1,7 @@
 /* eslint no-unused-expressions: 0 */
 const specHelper = require('../lib/helpers/specification');
 const hasEmptyProperty = require('../lib/helpers/hasEmptyProperty');
+const parseApiFileContent = require('../lib/helpers/parseApiFileContent');
 
 const swaggerObject = require('./files/v2/swaggerObject.json');
 const testData = require('./files/v2/testData');
@@ -91,17 +92,126 @@ describe('Helpers', () => {
     });
   });
 
-  it('hasEmptyProperty() identifies object with an empty object or array as property', () => {
-    const invalidA = { foo: {} };
-    const invalidB = { foo: [] };
-    const validA = { foo: { bar: 'baz' } };
-    const validB = { foo: ['¯_(ツ)_/¯'] };
-    const validC = { foo: '¯_(ツ)_/¯' };
+  describe('hasEmptyProperty', () => {
+    it('identifies object with an empty object or array as property', () => {
+      const invalidA = { foo: {} };
+      const invalidB = { foo: [] };
+      const validA = { foo: { bar: 'baz' } };
+      const validB = { foo: ['¯_(ツ)_/¯'] };
+      const validC = { foo: '¯_(ツ)_/¯' };
 
-    expect(hasEmptyProperty(invalidA)).toBe(true);
-    expect(hasEmptyProperty(invalidB)).toBe(true);
-    expect(hasEmptyProperty(validA)).toBe(false);
-    expect(hasEmptyProperty(validB)).toBe(false);
-    expect(hasEmptyProperty(validC)).toBe(false);
+      expect(hasEmptyProperty(invalidA)).toBe(true);
+      expect(hasEmptyProperty(invalidB)).toBe(true);
+      expect(hasEmptyProperty(validA)).toBe(false);
+      expect(hasEmptyProperty(validB)).toBe(false);
+      expect(hasEmptyProperty(validC)).toBe(false);
+    });
+  });
+
+  describe('parseApiFileContent', () => {
+    it('should extract jsdoc comments inside .js files', () => {
+      const fileContent = `
+        // Sets up the routes.
+        module.exports.setup = function (app) {
+          /**
+           * @swagger
+           * tags:
+           *   name: Users
+           *   description: User management and login
+           */
+
+          /**
+           * @swagger
+           * /users:
+           *   post:
+           *     description: Returns users
+           *     tags: [Users]
+           *     produces:
+           *       - application/json
+           *     parameters:
+           *       - $ref: '#/parameters/username'
+           *     responses:
+           *       200:
+           *         description: users
+           */
+          app.post('/users', (req, res) => {
+            res.json(req.body);
+          });
+        };
+      `;
+
+      expect(parseApiFileContent(fileContent, '.js')).toEqual({
+        yaml: [],
+        jsdoc: [
+          {
+            description: '',
+            tags: [
+              {
+                title: 'swagger',
+                description:
+                  'tags:\n  name: Users\n  description: User management and login',
+              },
+            ],
+          },
+          {
+            description: '',
+            tags: [
+              {
+                title: 'swagger',
+                description:
+                  "/users:\n  post:\n    description: Returns users\n    tags: [Users]\n    produces:\n      - application/json\n    parameters:\n      - $ref: '#/parameters/username'\n    responses:\n      200:\n        description: users",
+              },
+            ],
+          },
+        ],
+      });
+    });
+
+    it('should extract coffeescript comments inside .coffee files', () => {
+      const fileContent = `
+      # Coffeescript Example
+
+      ###
+      * @swagger
+      * /login:
+      *   post:
+      *     description: Login to the application
+      *     produces:
+      *       - application/json
+      *     parameters:
+      *       - name: username
+      *         description: Username to use for login.
+      *         in: formData
+      *         required: true
+      *         type: string
+      *       - name: password
+      *         description: User's password.
+      *         in: formData
+      *         required: true
+      *         type: string
+      *     responses:
+      *       200:
+      *         description: login
+      ###
+      app.post '/login', (req, res) ->
+        res.json req.body
+    `;
+
+      expect(parseApiFileContent(fileContent, '.coffee')).toEqual({
+        yaml: [],
+        jsdoc: [
+          {
+            description: '/',
+            tags: [
+              {
+                title: 'swagger',
+                description:
+                  "/login:\n  post:\n    description: Login to the application\n    produces:\n      - application/json\n    parameters:\n      - name: username\n        description: Username to use for login.\n        in: formData\n        required: true\n        type: string\n      - name: password\n        description: User's password.\n        in: formData\n        required: true\n        type: string\n    responses:\n      200:\n        description: login",
+              },
+            ],
+          },
+        ],
+      });
+    });
   });
 });