Skip to content

Commit

Permalink
Add support for API documentation filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
identityclash committed Aug 22, 2018
1 parent d676ba4 commit 2e539ed
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 5 deletions.
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
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.

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;
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) {
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
* @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"
}]
}
}

0 comments on commit 2e539ed

Please sign in to comment.