Skip to content

Commit 2e539ed

Browse files
committed
Add support for API documentation filtering
1 parent d676ba4 commit 2e539ed

File tree

8 files changed

+190
-5
lines changed

8 files changed

+190
-5
lines changed

docs/GETTING-STARTED.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const options = {
1313
},
1414
},
1515
apis: ['./routes.js'], // Path to the API docs
16+
jsDocFilter: (jsDocComment) => { // Optional filtering mechanism applied on each API doc
17+
return true;
18+
}
1619
};
1720

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

40+
- `options.jsDocFilter` is a function which accepts only one variable `jsDocComment`. This `jsDocComment` represents each route documentation being iterated upon.
41+
42+
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.
43+
3744
You could also use a framework like [swagger-tools](https://www.npmjs.com/package/swagger-tools) to serve the spec and a `swagger-ui`.
3845

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

78+
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.
79+
```javascript
80+
function jsDocFilter(jsDocComment) {
81+
// Do filtering logic here in order to determine whether
82+
// the JSDoc under scrunity will be displayed or not.
83+
// This function must return boolean. `true` to display, `false` to hide.
84+
const docDescription = jsDocComment.description;
85+
86+
const features = docDescription.indexOf('feature') > -1;
87+
const featureX = docDescription.indexOf('featureX') > -1; // featureX is the filter keyword
88+
const featureY = docDescription.indexOf('featureY') > -1; // featureY is also another filter keyword
89+
90+
// `featureFilter` is some external environment variable
91+
const enabledX =
92+
featureX && envVars && envVars.featureFilter.indexOf('X') > -1;
93+
const enabledY =
94+
featureY && envVars && envVars.featureFilter.indexOf('Y') > -1;
95+
96+
const featuresEnabled = enabledX || enabledY;
97+
98+
const existingRoutes = [];
99+
100+
function includeDocs() {
101+
const route =
102+
jsDocComment &&
103+
jsDocComment.tags &&
104+
jsDocComment.tags[0] &&
105+
jsDocComment.tags[0].description &&
106+
jsDocComment.tags[0].description.split(':')[0];
107+
108+
if (existingRoutes.indexOf(route) === -1) {
109+
// need to perform check if the route doc was previously added
110+
return true;
111+
}
112+
113+
return false;
114+
}
115+
116+
// featured route documentation
117+
if (features) {
118+
if (featuresEnabled) {
119+
return includeDocs();
120+
}
121+
} else {
122+
// original routes included here
123+
return includeDocs();
124+
}
125+
126+
return false;
127+
},
128+
};
129+
```
130+
131+
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.
132+
Note that the filter only reads keywords above the `@swagger` identifier.
133+
```javascript
134+
/**
135+
* featureX
136+
* @swagger
137+
* /newFeatureX:
138+
* get:
139+
* description: Part of feature X
140+
* responses:
141+
* 200:
142+
* description: hello feature X
143+
*/
144+
```
145+
71146
### Re-using Model Definitions
72147

73148
A model may be the same for multiple endpoints (Ex. User POST,PUT responses).

example/v2/app.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Dependencies
44
const express = require('express');
55
const bodyParser = require('body-parser');
6+
const envVars = require('./envVars');
67
const routes = require('./routes');
78
const routes2 = require('./routes2');
89
const swaggerJSDoc = require('../..');
@@ -37,6 +38,56 @@ const options = {
3738
swaggerDefinition,
3839
// Path to the API docs
3940
apis: ['./example/v2/routes*.js', './example/v2/parameters.yaml'],
41+
42+
// jsDocFilter has only one parameter - jsDocComment
43+
// jsDocComment contains the actual route jsDocumentation
44+
jsDocFilter: function jsDocFilter(jsDocComment) {
45+
// Do filtering logic here in order to determine whether
46+
// the JSDoc under scrunity will be displayed or not.
47+
// This function must return boolean. `true` to display, `false` to hide.
48+
const docDescription = jsDocComment.description;
49+
50+
const features = docDescription.indexOf('feature') > -1;
51+
const featureX = docDescription.indexOf('featureX') > -1;
52+
const featureY = docDescription.indexOf('featureY') > -1;
53+
54+
const enabledX =
55+
featureX && envVars && envVars.featureFilter.indexOf('X') > -1;
56+
const enabledY =
57+
featureY && envVars && envVars.featureFilter.indexOf('Y') > -1;
58+
59+
const featuresEnabled = enabledX || enabledY;
60+
61+
const existingRoutes = [];
62+
63+
function includeDocs() {
64+
const route =
65+
jsDocComment &&
66+
jsDocComment.tags &&
67+
jsDocComment.tags[0] &&
68+
jsDocComment.tags[0].description &&
69+
jsDocComment.tags[0].description.split(':')[0];
70+
71+
if (existingRoutes.indexOf(route) === -1) {
72+
// need to perform check if the route doc was previously added
73+
return true;
74+
}
75+
76+
return false;
77+
}
78+
79+
// featured route documentation
80+
if (features) {
81+
if (featuresEnabled) {
82+
return includeDocs();
83+
}
84+
} else {
85+
// original routes included here
86+
return includeDocs();
87+
}
88+
89+
return false;
90+
},
4091
};
4192

4293
// Initialize swagger-jsdoc -> returns validated swagger spec in json format

example/v2/envVars.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Mimics a Node server's set of environment variables
3+
*/
4+
module.exports = {
5+
/*
6+
* Switch between sample values of filter 'X' or 'Y'.
7+
* to see display behavior in swagger-jsdoc filtering.
8+
* If 'X' is defined, 'featureY' documentation should
9+
* not show up in the /api-docs.json and vice-versa.
10+
*/
11+
featureFilter: 'X',
12+
};

example/v2/routes2.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,32 @@ module.exports.setup = function(app) {
1414
app.get('/hello', (req, res) => {
1515
res.send('Hello World (Version 2)!');
1616
});
17+
18+
/**
19+
* featureX
20+
* @swagger
21+
* /newFeatureX:
22+
* get:
23+
* description: Part of feature X
24+
* responses:
25+
* 200:
26+
* description: hello feature X
27+
*/
28+
app.get('/newFeatureX', (req, res) => {
29+
res.send('This is a new feature X!');
30+
});
31+
32+
/**
33+
* featureY
34+
* @swagger
35+
* /newFeatureY:
36+
* get:
37+
* description: Part of feature Y
38+
* responses:
39+
* 200:
40+
* description: hello feature Y
41+
*/
42+
app.get('/newFeatureY', (req, res) => {
43+
res.send('This is another new feature Y!');
44+
});
1745
};

lib/helpers/getSpecificationObject.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function getSpecificationObject(options) {
4848
const apiPaths = convertGlobPaths(options.apis);
4949

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

5454
specHelper.addDataToSwaggerObject(specification, files.yaml);

lib/helpers/parseApiFile.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ const jsYaml = require('js-yaml');
77
* Parses the provided API file for JSDoc comments.
88
* @function
99
* @param {string} file - File to be parsed
10+
* @param {object} jsDocFilter - Function returning boolean to filter docs
1011
* @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files
1112
* @requires doctrine
1213
*/
13-
function parseApiFile(file) {
14+
function parseApiFile(file, jsDocFilter) {
1415
const jsDocRegex = /\/\*\*([\s\S]*?)\*\//gm;
1516
const fileContent = fs.readFileSync(file, { encoding: 'utf8' });
1617
const ext = path.extname(file);
@@ -24,7 +25,10 @@ function parseApiFile(file) {
2425
if (regexResults) {
2526
for (let i = 0; i < regexResults.length; i += 1) {
2627
const jsDocComment = doctrine.parse(regexResults[i], { unwrap: true });
27-
jsDocComments.push(jsDocComment);
28+
29+
if (typeof jsDocFilter !== 'function' || !!jsDocFilter(jsDocComment)) {
30+
jsDocComments.push(jsDocComment);
31+
}
2832
}
2933
}
3034
}

lib/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ const getSpecificationObject = require('./helpers/getSpecificationObject');
1010
* @requires swagger-parser
1111
*/
1212
module.exports = options => {
13-
if ((!options.swaggerDefinition || !options.definition) && !options.apis) {
13+
if (
14+
(!options.swaggerDefinition ||
15+
!options.definition ||
16+
!options.jsDocFilter) &&
17+
!options.apis
18+
) {
1419
throw new Error('Provided options are incorrect.');
1520
}
1621

test/example/v2/swagger-spec.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@
9090
}
9191
}
9292
}
93+
},
94+
"/newFeatureX": {
95+
"get": {
96+
"description": "Part of feature X",
97+
"responses": {
98+
"200": {
99+
"description": "hello feature X"
100+
}
101+
}
102+
}
93103
}
94104
},
95105
"definitions": {
@@ -132,4 +142,4 @@
132142
"name": "Accounts",
133143
"description": "Accounts"
134144
}]
135-
}
145+
}

0 commit comments

Comments
 (0)