Repository: glennjones/hapi-swagger
Pull request #397 (2017-02-20 09:12:59)

Added option to use "label" as the next model name prefix
Currently the next model name is always "Model #". However if you have a dynamic model that uses the same label, but with different fields or description, then the numbering can balloon up very quickly.

I've added an option called "definitionPrefix", which when set to "useLabel", will use the label as the numbering base. E.g. If I have a model called "baseErrors", then all my flavours of baseErrors will be "baseErrors 1", "baseErrors 2" etc, which is better to read.


Patch:
diff --git a/lib/builder.js b/lib/builder.js
index bb21b04..b76b3e8 100644
--- a/lib/builder.js
+++ b/lib/builder.js
@@ -239,6 +239,7 @@ internals.removeNoneSchemaOptions = function (options) {
         'reuseDefinitions',
         'uiCompleteScript',
         'deReference',
+        'definitionPrefix',
         'validatorUrl',
         'jsonEditor',
         'acceptToProduce',
diff --git a/lib/defaults.js b/lib/defaults.js
index e79c334..95dca53 100644
--- a/lib/defaults.js
+++ b/lib/defaults.js
@@ -19,6 +19,7 @@ module.exports = {
     'uiCompleteScript': null,
     'xProperties': true,
     'reuseDefinitions': true,
+    'definitionPrefix': 'default',
     'deReference': false,
     'validatorUrl': '//online.swagger.io/validator',
     'acceptToProduce': true,  // internal, NOT public
diff --git a/lib/definitions.js b/lib/definitions.js
index 7460b1e..15547ae 100644
--- a/lib/definitions.js
+++ b/lib/definitions.js
@@ -44,11 +44,11 @@ internals.definitions.prototype.append = function (definitionName, definition, c
             out = definitionName;
         } else {
             // create new definition
-            out = internals.append(null, definition, currentCollection, settings);
+            out = internals.append(definitionName, definition, currentCollection, true, settings);
         }
     } else {
         // create new definition
-        out = internals.append(definitionName, definition, currentCollection, settings);
+        out = internals.append(definitionName, definition, currentCollection, false, settings);
     }
 
     return out;
@@ -64,7 +64,7 @@ internals.definitions.prototype.append = function (definitionName, definition, c
  * @param  {Object} settings
  * @return {String}
  */
-internals.append = function (definitionName, definition, currentCollection, settings) {
+internals.append = function (definitionName, definition, currentCollection, forceDynamicName, settings) {
 
     let out;
     let foundDefinitionName;
@@ -79,7 +79,17 @@ internals.append = function (definitionName, definition, currentCollection, sett
         out = foundDefinitionName;
     } else {
         // else create a new item using definitionName or next model number
-        out = definitionName || internals.nextModelName(currentCollection);
+        if (forceDynamicName) {
+            if (settings.definitionPrefix === 'useLabel') {
+                out = internals.nextModelName(definitionName + ' ', currentCollection);
+            }
+            else {
+                out = internals.nextModelName('Model ', currentCollection);
+            }
+        }
+        else {
+            out = definitionName || internals.nextModelName('Model ', currentCollection);
+        }
         currentCollection[out] = definition;
     }
     return out;
@@ -126,22 +136,22 @@ internals.hash = function (obj) {
 /**
  * creates a new unique model name
  *
+ * @param  {String} nextModelNamePrefix
  * @param  {Object} currentCollection
  * @return {String}
  */
-internals.nextModelName = function (currentCollection) {
-
+internals.nextModelName = function (nextModelNamePrefix, currentCollection) {
     let highest = 0;
     let key;
     for (key in currentCollection) {
-        if (Utilities.startsWith(key, 'Model')) {
-            let num = parseInt(key.replace('Model', ''), 10);
+        if (Utilities.startsWith(key, nextModelNamePrefix)) {
+            let num = parseInt(key.replace(nextModelNamePrefix, ''), 10) || 0;
             if (num && num > highest) {
                 highest = num;
             }
         }
     }
-    return 'Model ' + (highest + 1);
+    return nextModelNamePrefix + (highest + 1);
 };
 
 
diff --git a/lib/index.js b/lib/index.js
index c32a526..9fc2d0e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -31,6 +31,7 @@ const schema = Joi.object({
     uiCompleteScript: Joi.string().allow(null),
     xProperties: Joi.boolean(),
     reuseDefinitions: Joi.boolean(),
+    definitionPrefix: Joi.string(),
     deReference: Joi.boolean(),
     validatorUrl: Joi.string().allow(null),
     acceptToProduce: Joi.boolean(),
diff --git a/optionsreference.md b/optionsreference.md
index dfffd19..f102577 100644
--- a/optionsreference.md
+++ b/optionsreference.md
@@ -35,6 +35,7 @@ JSON (JSON endpoint needed to create UI)
 *  `produces`: (array) The mimetypes produced - default: `['application/json']`
 *  `xProperties`: Adds JOI data that cannot be use directly by swagger as metadata - default: `true`,
 *  `reuseDefinitions`: Reuse of definition models to save space - default: `true`,
+*  `definitionPrefix`: Dynamic naming convention. `default` or `useLabel` - default: `default`,
 *  `deReference`: Dereferences JSON output - default: `false`,
 *  `debug`: Validates the JSON ouput against swagger specification - default: `false`,
 
diff --git a/usageguide.md b/usageguide.md
index 1927c9c..8d4f5f0 100644
--- a/usageguide.md
+++ b/usageguide.md
@@ -118,7 +118,8 @@ validate: {
 ```
 __NOTE: the plugin reuses "definition models" these describe each JSON object use by an API i.e. a "user". This feature
 was added to reduce the size of the JSON. The reuse of models can cause names to be reused as well. Please switch
-`options.reuseDefinitions` to `false` if you are nameing your JOI objects.__
+`options.reuseDefinitions` to `false` if you are naming your JOI objects. By default objects are named in a "Model #"
+ format. To use the `label`, specify `options.definitionPrefix` as `useLabel`.__
 
 
 
@@ -744,4 +745,4 @@ There are a number of examples of different uses of `hapi-swagger` in the exampl
 Both these example use a custom HTML page
 *  [`be-more-hapi`](https://github.com/glennjones/be-more-hapi) - talk from Async.js on the October 2013 - old `hapi-swagger` example project, but keep update
 *  [`hapi-token-docs`](https://github.com/glennjones/hapi-token-docs) - A example site using HAPI, JWT tokens and swagger documentation
-*  [`time-to-be-hapi`](https://github.com/glennjones/time-to-be-hapi) - Londonjs talk March 2016 has many example uses of HAPI and one using `hapi-swagger`
\ No newline at end of file
+*  [`time-to-be-hapi`](https://github.com/glennjones/time-to-be-hapi) - Londonjs talk March 2016 has many example uses of HAPI and one using `hapi-swagger`


Test patch:
diff --git a/test/Integration/definitions-test.js b/test/Integration/definitions-test.js
index ddd4daa..eb10f66 100644
--- a/test/Integration/definitions-test.js
+++ b/test/Integration/definitions-test.js
@@ -190,6 +190,70 @@ lab.experiment('definitions', () => {
         });
     });
 
+    lab.test('definitionPrefix = useLabel', (done) => {
+
+        // use the label as a prefix for dynamic model names
+
+        const tempRoutes = [{
+            method: 'POST',
+            path: '/store1/',
+            config: {
+                handler: Helper.defaultHandler,
+                tags: ['api'],
+                validate: {
+                    payload: Joi.object({
+                        a: Joi.number(),
+                        b: Joi.number(),
+                        operator: Joi.string(),
+                        equals: Joi.number()
+                    }).label('A')
+                }
+            }
+        }, {
+            method: 'POST',
+            path: '/store2/',
+            config: {
+                handler: Helper.defaultHandler,
+                tags: ['api'],
+                validate: {
+                    payload: Joi.object({
+                        c: Joi.number(),
+                        v: Joi.number(),
+                        operator: Joi.string(),
+                        equals: Joi.number()
+                    }).label('A A')
+                }
+            }
+        }, {
+            method: 'POST',
+            path: '/store3/',
+            config: {
+                handler: Helper.defaultHandler,
+                tags: ['api'],
+                validate: {
+                    payload: Joi.object({
+                        c: Joi.number(),
+                        f: Joi.number(),
+                        operator: Joi.string(),
+                        equals: Joi.number()
+                    }).label('A')
+                }
+            }
+        }];
+
+        Helper.createServer({ definitionPrefix: 'useLabel' }, tempRoutes, (err, server) => {
+
+            expect(err).to.equal(null);
+            server.inject({ method: 'GET', url: '/swagger.json' }, function (response) {
+
+                expect(response.statusCode).to.equal(200);
+                expect(response.result.definitions.A).to.exist();
+                expect(response.result.definitions['A A']).to.exist();
+                expect(response.result.definitions['A 1']).to.exist();
+                Helper.validate(response, done, expect);
+            });
+        });
+    });
 
     lab.test('test that optional array is not in swagger output', (done) => {