Repository: glennjones/hapi-swagger (js)

Issue:
Allow vendor extension at the route level (x-*)
#### Context

* *hapi-swagger version*: 9.2.0

#### What are you trying to achieve or the steps to reproduce?

It is now possible to provide arbitrary vendor extensions with `x-*` outside of the info property but it still does not work on a route level. It seems like this could be easily fixed and that would allow to generate code samples with swagger.

```js
                // at the route level
                plugins: {
                    'hapi-swagger': {
                      'x-code-samples':
                        {
                            lang: 'JavaScript',
                            source: 'console.log(\'Hello World\');'
                      },
                    },
                }
```

#### What result did you get?

nothing is displayed in the swagger.json

#### What did you expect?

to have a 'x-code-samples' property for the corresponding operation in the swagger output

#### Proposal

It seems that there is some old code which allowed 'x-meta' at the route level option, why not make it accept anything instead of just 'x-meta'?

Pull request description:
Allow vendor extensions at route level
Resolves #550 

I tried to follow the existing code philosophy as much as possible. I thus mutated some objects since it was already the case. 

Patch:
diff --git a/lib/paths.js b/lib/paths.js
index d990b45..39f9e85 100644
--- a/lib/paths.js
+++ b/lib/paths.js
@@ -80,10 +80,10 @@ internals.paths.prototype.build = function (routes) {
             deprecated: Hoek.reach(routeOptions, 'deprecated') || null,
             id: Hoek.reach(routeOptions, 'id') || null,
             groups: route.group,
-            'x-meta': Hoek.reach(routeOptions, 'x-meta') || null
         };
 
 
+        Utilities.assignVendorExtensions(routeData, routeOptions);
         routeData.path = Utilities.replaceInPath(routeData.path, ['endpoints'], this.settings.pathReplacements);
 
 
@@ -330,9 +330,9 @@ internals.paths.prototype.buildRoutes = function (routes) {
         if (route.order) {
             out['x-order'] = route.order;
         }
-        if (route['x-meta']) {
-            out['x-meta'] = route['x-meta'];
-        }
+
+        Utilities.assignVendorExtensions(out, route);
+
         if (route.deprecated !== null) {
             out.deprecated = route.deprecated;
         }
diff --git a/lib/utilities.js b/lib/utilities.js
index 1a64339..cac76eb 100644
--- a/lib/utilities.js
+++ b/lib/utilities.js
@@ -340,7 +340,7 @@ utilities.geJoiLabel = function (joiObj) {
 
 
 /**
- * turns a javascript object into JOI object
+ * returns a javascript object into JOI object
  * needed to covert custom parameters objects passed in by plug-in route options
  *
  * @param  {Object} obj
@@ -487,3 +487,25 @@ utilities.removeTrailingSlash = function (str) {
     }
     return str;
 };
+
+/**
+ * Assign vendor extensions: x-* to the target. This mutates target.
+ *
+ * @param  {Object} target
+ * @param  {Object} source
+ * @return {Object}
+ */
+utilities.assignVendorExtensions = function (target, source) {
+    if (!this.isObject(target) || !this.isObject(source)) {
+        return target;
+    }
+
+
+    for (const sourceProperty in source) {
+        if(sourceProperty.startsWith('x-') && sourceProperty.length > 2) {
+            // this may override existing x- properties which should not be an issue since values should be identical.
+            target[sourceProperty] = Hoek.reach(source, sourceProperty) || null;
+        }
+    }
+    return target;
+};


Test patch:
diff --git a/test/Integration/plugin-test.js b/test/Integration/plugin-test.js
index 3e17f0e..a03f175 100644
--- a/test/Integration/plugin-test.js
+++ b/test/Integration/plugin-test.js
@@ -350,6 +350,32 @@ lab.experiment('plugin', () => {
         });
     });
 
+    lab.test('test aribrary vendor extensions (x-*) appears in swagger', async() => {
+        const testRoutes = [{
+            method: 'POST',
+            path: '/test/',
+            config: {
+                handler: Helper.defaultHandler,
+                tags: ['api'],
+                plugins: {
+                    'hapi-swagger': {
+                        'x-code-samples': {
+                            lang: 'JavaScript',
+                            source: 'console.log("Hello World");'
+                        },
+                        'x-custom-string': 'some string',
+                    },
+                },
+            }
+        }];
+        const server = await Helper.createServer({}, testRoutes);
+        const response = await server.inject({ method: 'GET', url: '/swagger.json' });
+        expect(response.result.paths['/test/'].post['x-code-samples']).to.equal({
+            lang: 'JavaScript',
+            source: 'console.log("Hello World");'
+        });
+        expect(response.result.paths['/test/'].post['x-custom-string']).to.equal('some string');
+    });
 
     lab.test('test {disableDropdown: true} in swagger', async() => {
         const testRoutes = [{
diff --git a/test/unit/utilities-test.js b/test/unit/utilities-test.js
index 8227020..cd3114f 100644
--- a/test/unit/utilities-test.js
+++ b/test/unit/utilities-test.js
@@ -237,7 +237,16 @@ lab.experiment('utilities', () => {
         expect(Utilities.replaceInPath('api/users.search', ['groups'], pathReplacements)).to.equal('api/users');
     });
 
+    lab.test('mergeVendorExtensions', () => {
 
+        expect(Utilities.assignVendorExtensions({a: 1, b: 2}, {'x-a': 1, 'x-b': 2, c: 3})).to.equal({a: 1, b: 2, 'x-a': 1, 'x-b': 2});
+        expect(Utilities.assignVendorExtensions({a: 1, b: 2}, null)).to.equal({a: 1, b: 2});
+        expect(Utilities.assignVendorExtensions(null, null)).to.equal(null);
+        expect(Utilities.assignVendorExtensions(null, {'x-a': 1, 'x-b': 2, c: 3})).to.equal(null);
+        expect(Utilities.assignVendorExtensions({a: 1, b: 2}, {'x-a': 1, 'x-b': null, c: 3})).to.equal({a: 1, b: 2, 'x-a': 1, 'x-b': null});
+        expect(Utilities.assignVendorExtensions({a: 1, b: 2, 'x-a':100}, {'x-a': 1, 'x-b': 2, c: 3})).to.equal({a: 1, b: 2, 'x-a': 1, 'x-b': 2});
+        expect(Utilities.assignVendorExtensions({a: 1, b: 2}, {'x-': 1})).to.equal({a: 1, b: 2});
+    });
 
 
 });