diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 5fd0ea3c..5b8ae2b8 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -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 @@ -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 @@ -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). diff --git a/example/v2/app.js b/example/v2/app.js index 141c3dbb..1686651a 100644 --- a/example/v2/app.js +++ b/example/v2/app.js @@ -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('../..'); @@ -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 diff --git a/example/v2/envVars.js b/example/v2/envVars.js new file mode 100644 index 00000000..95c4b8e1 --- /dev/null +++ b/example/v2/envVars.js @@ -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', +}; diff --git a/example/v2/routes2.js b/example/v2/routes2.js index 4f091a00..44d755c1 100644 --- a/example/v2/routes2.js +++ b/example/v2/routes2.js @@ -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!'); + }); }; diff --git a/lib/helpers/getSpecificationObject.js b/lib/helpers/getSpecificationObject.js index aa5906b1..f640529f 100644 --- a/lib/helpers/getSpecificationObject.js +++ b/lib/helpers/getSpecificationObject.js @@ -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); diff --git a/lib/helpers/parseApiFile.js b/lib/helpers/parseApiFile.js index ef72a457..fdea336e 100644 --- a/lib/helpers/parseApiFile.js +++ b/lib/helpers/parseApiFile.js @@ -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); @@ -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); + } } } } diff --git a/lib/index.js b/lib/index.js index 76ecfafd..4468a516 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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.'); } diff --git a/test/example/v2/swagger-spec.json b/test/example/v2/swagger-spec.json index 942b197b..465e8874 100644 --- a/test/example/v2/swagger-spec.json +++ b/test/example/v2/swagger-spec.json @@ -90,6 +90,16 @@ } } } + }, + "/newFeatureX": { + "get": { + "description": "Part of feature X", + "responses": { + "200": { + "description": "hello feature X" + } + } + } } }, "definitions": { @@ -132,4 +142,4 @@ "name": "Accounts", "description": "Accounts" }] -} \ No newline at end of file +}