Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for jsDoc filtering #111

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const options = {
},
},
apis: ['./routes.js'], // Path to the API docs
jsDocFilter: (jsDocComment) => { // Optional filtering mechanism applied on each API doc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, it feels a bit weird to return a boolean after receiving an argument. I mean, if this is meant to be a flag for a logic to be attached, I'd personally prefer something like jsDocFilter: true and pass the function on another property of the definition object. Or maybe simply attach the function which returns a reduced object.

return true;
}
};

// Initialize swagger-jsdoc -> returns validated swagger spec in json format
Expand All @@ -34,6 +37,10 @@ app.get('/api-docs.json', function(req, res) {
});
```

- `options.jsDocFilter` is a function which accepts only one variable `jsDocComment`. This `jsDocComment` represents each route documentation being iterated upon.

If you want to optionally perform filters on each route documentation, return boolean `true` or `false` accordingly on certain logical conditions. This is useful for conditionally displaying certain route documentation based on different server deployments.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the comment above, I'd personally prefer to keep boolean and object types separate in terms of logic. For instance, jsDocFilter can be simply a reducer function. If the function is set, it's obviously true :)


You could also use a framework like [swagger-tools](https://www.npmjs.com/package/swagger-tools) to serve the spec and a `swagger-ui`.

### How to document the API
Expand Down Expand Up @@ -68,6 +75,74 @@ app.post('/login', function(req, res) {
});
```

As said earlier, API documentation filters could be put in place before having such API rendered on the JSON file. A sample is shown in [app.js](../example/v2/app.js) where some form of filtering is done.
```javascript
function jsDocFilter(jsDocComment) {
// Do filtering logic here in order to determine whether
// the JSDoc under scrunity will be displayed or not.
// This function must return boolean. `true` to display, `false` to hide.
const docDescription = jsDocComment.description;

const features = docDescription.indexOf('feature') > -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the approach of storing checks in variables. For readability, you can name them hasFeatureX, hasFeatureY, etc. Also, since we are having modern JS version, you can freely use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes

const featureX = docDescription.indexOf('featureX') > -1; // featureX is the filter keyword
const featureY = docDescription.indexOf('featureY') > -1; // featureY is also another filter keyword

// `featureFilter` is some external environment variable
const enabledX =
featureX && envVars && envVars.featureFilter.indexOf('X') > -1;
const enabledY =
featureY && envVars && envVars.featureFilter.indexOf('Y') > -1;

const featuresEnabled = enabledX || enabledY;

const existingRoutes = [];

function includeDocs() {
const route =
jsDocComment &&
jsDocComment.tags &&
jsDocComment.tags[0] &&
jsDocComment.tags[0].description &&
jsDocComment.tags[0].description.split(':')[0];

if (existingRoutes.indexOf(route) === -1) {
// need to perform check if the route doc was previously added
return true;
}

return false;
}

// featured route documentation
if (features) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the place where I'd change the logic to return a reduced version of the docs.

if (featuresEnabled) {
return includeDocs();
}
} else {
// original routes included here
return includeDocs();
}

return false;
},
};
```

When a route filter needs to be applied, the filter keyword may be used. In the example below, the `featureX` (coded above `@swagger`) is a filter keyword for the route to be included in the rendering of the JSON.
Note that the filter only reads keywords above the `@swagger` identifier.
```javascript
/**
* featureX
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried using tags for grouping endpoints per given logical separation? https://swagger.io/docs/specification/grouping-operations-with-tags/

* @swagger
* /newFeatureX:
* get:
* description: Part of feature X
* responses:
* 200:
* description: hello feature X
*/
```

### Re-using Model Definitions

A model may be the same for multiple endpoints (Ex. User POST,PUT responses).
Expand Down
51 changes: 51 additions & 0 deletions example/v2/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Dependencies
const express = require('express');
const bodyParser = require('body-parser');
const envVars = require('./envVars');
const routes = require('./routes');
const routes2 = require('./routes2');
const swaggerJSDoc = require('../..');
Expand Down Expand Up @@ -37,6 +38,56 @@ const options = {
swaggerDefinition,
// Path to the API docs
apis: ['./example/v2/routes*.js', './example/v2/parameters.yaml'],

// jsDocFilter has only one parameter - jsDocComment
// jsDocComment contains the actual route jsDocumentation
jsDocFilter: function jsDocFilter(jsDocComment) {
// Do filtering logic here in order to determine whether
// the JSDoc under scrunity will be displayed or not.
// This function must return boolean. `true` to display, `false` to hide.
const docDescription = jsDocComment.description;

const features = docDescription.indexOf('feature') > -1;
const featureX = docDescription.indexOf('featureX') > -1;
const featureY = docDescription.indexOf('featureY') > -1;

const enabledX =
featureX && envVars && envVars.featureFilter.indexOf('X') > -1;
const enabledY =
featureY && envVars && envVars.featureFilter.indexOf('Y') > -1;

const featuresEnabled = enabledX || enabledY;

const existingRoutes = [];

function includeDocs() {
const route =
jsDocComment &&
jsDocComment.tags &&
jsDocComment.tags[0] &&
jsDocComment.tags[0].description &&
jsDocComment.tags[0].description.split(':')[0];

if (existingRoutes.indexOf(route) === -1) {
// need to perform check if the route doc was previously added
return true;
}

return false;
}

// featured route documentation
if (features) {
if (featuresEnabled) {
return includeDocs();
}
} else {
// original routes included here
return includeDocs();
}

return false;
},
};

// Initialize swagger-jsdoc -> returns validated swagger spec in json format
Expand Down
12 changes: 12 additions & 0 deletions example/v2/envVars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Mimics a Node server's set of environment variables
*/
module.exports = {
/*
* Switch between sample values of filter 'X' or 'Y'.
* to see display behavior in swagger-jsdoc filtering.
* If 'X' is defined, 'featureY' documentation should
* not show up in the /api-docs.json and vice-versa.
*/
featureFilter: 'X',
};
28 changes: 28 additions & 0 deletions example/v2/routes2.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,32 @@ module.exports.setup = function(app) {
app.get('/hello', (req, res) => {
res.send('Hello World (Version 2)!');
});

/**
* featureX
* @swagger
* /newFeatureX:
* get:
* description: Part of feature X
* responses:
* 200:
* description: hello feature X
*/
app.get('/newFeatureX', (req, res) => {
res.send('This is a new feature X!');
});

/**
* featureY
* @swagger
* /newFeatureY:
* get:
* description: Part of feature Y
* responses:
* 200:
* description: hello feature Y
*/
app.get('/newFeatureY', (req, res) => {
res.send('This is another new feature Y!');
});
};
2 changes: 1 addition & 1 deletion lib/helpers/getSpecificationObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function getSpecificationObject(options) {
const apiPaths = convertGlobPaths(options.apis);

for (let i = 0; i < apiPaths.length; i += 1) {
const files = parseApiFile(apiPaths[i]);
const files = parseApiFile(apiPaths[i], options.jsDocFilter);
const swaggerJsDocComments = filterJsDocComments(files.jsdoc);

specHelper.addDataToSwaggerObject(specification, files.yaml);
Expand Down
8 changes: 6 additions & 2 deletions lib/helpers/parseApiFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ const jsYaml = require('js-yaml');
* Parses the provided API file for JSDoc comments.
* @function
* @param {string} file - File to be parsed
* @param {object} jsDocFilter - Function returning boolean to filter docs
* @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files
* @requires doctrine
*/
function parseApiFile(file) {
function parseApiFile(file, jsDocFilter) {
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm;
const fileContent = fs.readFileSync(file, { encoding: 'utf8' });
const ext = path.extname(file);
Expand All @@ -24,7 +25,10 @@ function parseApiFile(file) {
if (regexResults) {
for (let i = 0; i < regexResults.length; i += 1) {
const jsDocComment = doctrine.parse(regexResults[i], { unwrap: true });
jsDocComments.push(jsDocComment);

if (typeof jsDocFilter !== 'function' || !!jsDocFilter(jsDocComment)) {
jsDocComments.push(jsDocComment);
}
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ const getSpecificationObject = require('./helpers/getSpecificationObject');
* @requires swagger-parser
*/
module.exports = options => {
if ((!options.swaggerDefinition || !options.definition) && !options.apis) {
if (
(!options.swaggerDefinition ||
!options.definition ||
!options.jsDocFilter) &&
!options.apis
) {
throw new Error('Provided options are incorrect.');
}

Expand Down
12 changes: 11 additions & 1 deletion test/example/v2/swagger-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@
}
}
}
},
"/newFeatureX": {
"get": {
"description": "Part of feature X",
"responses": {
"200": {
"description": "hello feature X"
}
}
}
}
},
"definitions": {
Expand Down Expand Up @@ -132,4 +142,4 @@
"name": "Accounts",
"description": "Accounts"
}]
}
}