Repository: glennjones/hapi-swagger
Pull request #441 (2017-07-29 16:15:58)

Adding tagsGroupingFilter option
When setting the `grouping` option to `tags`, the plugin assumes routes should be grouped by all tags (except `api`). This leads to routes being documented more than once if a route has multiple tags.

This change gives users more fine grained control over which tags are used for grouping (and simplifies the logic in the process).


Patch:
diff --git a/lib/builder.js b/lib/builder.js
index 4860163..27ef0e2 100644
--- a/lib/builder.js
+++ b/lib/builder.js
@@ -43,6 +43,7 @@ builder.schema = Joi.object({
     securityDefinitions: Joi.any(),
     security: Joi.any(),
     grouping: Joi.string().valid(['path', 'tags']),
+    tagsGroupingFilter: Joi.func(),
     tags: Joi.any(),
     cors: Joi.boolean(),
     externalDocs: Joi.object({
@@ -234,6 +235,7 @@ internals.removeNoneSchemaOptions = function(options) {
         'sortEndpoints',
         'sortPaths',
         'grouping',
+        'tagsGroupingFilter',
         'xProperties',
         'reuseDefinitions',
         'uiCompleteScript',
diff --git a/lib/defaults.js b/lib/defaults.js
index e79c334..edbb659 100644
--- a/lib/defaults.js
+++ b/lib/defaults.js
@@ -16,6 +16,7 @@ module.exports = {
     'sortEndpoints': 'path',
     'sortPaths': 'unsorted',
     'grouping': 'path',
+    'tagsGroupingFilter': (tag) => tag !== 'api',
     'uiCompleteScript': null,
     'xProperties': true,
     'reuseDefinitions': true,
diff --git a/lib/paths.js b/lib/paths.js
index 81ccf04..eb0e581 100644
--- a/lib/paths.js
+++ b/lib/paths.js
@@ -192,10 +192,7 @@ internals.paths.prototype.buildRoutes = function (routes) {
 
         // tags in swagger are used for grouping
         if (this.settings.grouping === 'tags') {
-            out.tags = route.tags.slice(0);
-            while (out.tags.indexOf('api') !== -1) {
-                out.tags.splice(out.tags.indexOf('api'), 1);
-            }
+            out.tags = route.tags.filter(this.settings.tagsGroupingFilter);
         }
         else {
             out.tags = route.groups;
diff --git a/optionsreference.md b/optionsreference.md
index fbe3ae9..5b7edc6 100644
--- a/optionsreference.md
+++ b/optionsreference.md
@@ -16,14 +16,14 @@ JSON (JSON endpoint needed to create UI)
 * `pathPrefixSize`: (number) Selects what segment of the URL path is used to group endpoints - default: `1`
 * `pathReplacements` : (array) methods for modifying path and group names in documentation - default: `[]`
 * `info`
-  * `title` (string) The title of the application -  default: `API documentation`
-  * `version` (string) The version number of the API -  default: `0.0.1`
-  * `description` (string)  A short description of the application
-  * `termsOfService` (string) A URL to the Terms of Service of the API
+  * `title`: (string) The title of the application -  default: `API documentation`
+  * `version`: (string) The version number of the API -  default: `0.0.1`
+  * `description`: (string) A short description of the application
+  * `termsOfService`: (string) A URL to the Terms of Service of the API
   * `contact`
-    * `name` (string) A contact name for the API
-    * `url` (string) A URL pointing to the contact information. MUST be formatted as a URL
-    * `email` (string) A email address of the contact person/organization. MUST be formatted as an email address.
+    * `name`: (string) A contact name for the API
+    * `url`: (string) A URL pointing to the contact information. MUST be formatted as a URL
+    * `email`: (string) A email address of the contact person/organization. MUST be formatted as an email address.
   * `license`
     * `name` (string) The name of the license used for the API
     * `url` (string) The URL to the license used by the API. MUST be formatted as a URL
@@ -31,7 +31,8 @@ JSON (JSON endpoint needed to create UI)
     of the object returned by the JSON endpoint. This allows custom properties to be defined as options and
     copied as such.
 *  `tags`: (array) containing array of [Tag Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#tagObject) used to group endpoints in UI. No defaults are provided.
-*  `grouping` (string) how to create grouping of endpoints value either `path` or `tags` - default: `path`
+*  `grouping`: (string) how to create grouping of endpoints value either `path` or `tags` - default: `path`
+*  `tagsGroupingFilter`: (function) A function used to determine which tags should be used for grouping (when `grouping` is set to `tags`) - default: `(tag) => tag !== 'api'`
 *  `securityDefinitions:`: (object) Containing [Security Definitions Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#securityDefinitionsObject). No defaults are provided.
 *  `payloadType`: (string) How payload parameters are displayed `json` or `form` - default: `json`
 *  `consumes`: (array) The mimetypes consumed - default: `['application/json']`
@@ -42,15 +43,15 @@ JSON (JSON endpoint needed to create UI)
 *  `debug`: Validates the JSON ouput against swagger specification - default: `false`,
 
 UI
-* `swaggerUI`:  (boolean) Add files that support SwaggerUI. Only removes files if `documentationPage` is also set to false - default: `true`
-* `swaggerUIPath`:  (string) The path of to all the SwaggerUI resources - default: `/swaggerui/`
-* `documentationPage`:  (boolean) Add documentation page - default: `true`
-* `documentationPath`:  (string) The path of the documentation page - default: `/documentation`
+* `swaggerUI`: (boolean) Add files that support SwaggerUI. Only removes files if `documentationPage` is also set to false - default: `true`
+* `swaggerUIPath`: (string) The path of to all the SwaggerUI resources - default: `/swaggerui/`
+* `documentationPage`: (boolean) Add documentation page - default: `true`
+* `documentationPath`: (string) The path of the documentation page - default: `/documentation`
 * `expanded`: (string) If UI is expanded when opened. `none`, `list` or `full` - default: `list`
 * `jsonEditor`: (boolean) If UI should use JSON Edtior - default: `false`
 * `sortTags`: (string) a sort method for `tags` i.e. groups in UI. `default` or `name`
 * `sortEndpoints`: (string) a sort method for endpoints in UI. `path`, `method`, `ordered`
-* `lang`: (string) The language of the UI `en`, `es`, `fr`, `it`, `ja`, `pl`, `pt`, `ru`, `tr` or `zh-cn`  - default: `en`
+* `lang`: (string) The language of the UI `en`, `es`, `fr`, `it`, `ja`, `pl`, `pt`, `ru`, `tr` or `zh-cn` - default: `en`
 * `uiCompleteScript`: (string) A JavaScript string injected into the HTML, called when UI loads - default: `null`
 * `validatorUrl`: (string || null) sets the external validating URL Can swtich off by setting to `null`
 
@@ -61,11 +62,11 @@ UI
 *  `consumes`: (array) The mimetypes consumed - default: `['application/json']`
 *  `produces`: (array) The mimetypes produced - default: `['application/json']`
 *  `validate`
-   *  `params` : (JOI object) allows you to `param` route documentation outside of HAPI validation
-   *  `query` : (JOI object) allows you to `query` route documentation outside of HAPI validation
-   *  `headers` : (JOI object) allows you to `headers` route documentation outside of HAPI validation
-   *  `payload` : (JOI object) allows you to `payload` route documentation outside of HAPI validation
-*  `security` :  Hoek.reach(routeOptions, 'security') || null,
+   *  `params`: (JOI object) allows you to `param` route documentation outside of HAPI validation
+   *  `query`: (JOI object) allows you to `query` route documentation outside of HAPI validation
+   *  `headers`: (JOI object) allows you to `headers` route documentation outside of HAPI validation
+   *  `payload`: (JOI object) allows you to `payload` route documentation outside of HAPI validation
+*  `security`: Hoek.reach(routeOptions, 'security') || null,
 *  `order`: (int) The order in which endpoints are displayed, works with options.sortEndpoints = 'ordered'
 *  `deprecated`: (boolean) Whether a endpoint has been deprecated - default: false
 


Test patch:
diff --git a/test/Integration/grouping-test.js b/test/Integration/grouping-test.js
index 00952a5..4d0cf97 100644
--- a/test/Integration/grouping-test.js
+++ b/test/Integration/grouping-test.js
@@ -22,7 +22,7 @@ let testPlugin = function (plugin, options, next) {
                 reply('Hi from grouping 1');
             },
             description: 'plugin1',
-            tags: ['api', 'hello group']
+            tags: ['api', 'hello group', 'another group']
         }
     });
 
@@ -131,6 +131,65 @@ lab.experiment('tag grouping', () => {
 
     lab.test('group by tags', (done) => {
 
+        const connection = server.select('docs');
+        connection.inject({ method: 'GET', url: '/swagger.json' }, function (response) {
+
+            expect(response.statusCode).to.equal(200);
+            expect(response.result.host).to.equal('localhost:3000');
+            expect(response.result.paths['/grouping1']).to.equal({
+
+                'get': {
+                    'tags': [
+                        'hello group',
+                        'another group'
+                    ],
+                    'responses': {
+                        'default': {
+                            'schema': {
+                                'type': 'string'
+                            },
+                            'description': 'Successful'
+                        }
+                    },
+                    'operationId': 'getGrouping1',
+                    'summary': 'plugin1'
+                }
+            });
+            Helper.validate(response, done, expect);
+        });
+    });
+});
+
+
+lab.experiment('tag grouping with tasGroupingFilter', () => {
+
+    const server = new Hapi.Server();
+
+    lab.before(function (done) {
+
+        swaggerOptions.grouping = 'tags';
+        swaggerOptions.tagsGroupingFilter = (tag) => tag === 'hello group';
+        server.connection({ host: 'localhost', port: 3000, labels: 'api' });
+        server.connection({ host: 'localhost', port: 3001, labels: 'docs' });
+        server.register([
+            Inert,
+            Vision,
+            {
+                register: testPlugin,
+                select: ['api']
+            },
+            {
+                register: HapiSwagger,
+                options: swaggerOptions,
+                select: ['docs']
+            }
+        ]);
+
+        server.start(() => { done(); });
+    });
+
+    lab.test('group by filtered tags', (done) => {
+
         const connection = server.select('docs');
         connection.inject({ method: 'GET', url: '/swagger.json' }, function (response) {