Support route creation with path parameters and secure keys. (#114)

* 3.11.0

* 3.12.0

* Fixing #100

Connect echo action to trigger with rule to ensure result has
activation identifier.

* 3.13.1

* Support route creation with path parameters and secure keys.

Parse path parameters from route options and generate correct swagger.
New creation parameter to pass authentication key for secured web actions.
diff --git a/README.md b/README.md
index 280440f..65671b3 100644
--- a/README.md
+++ b/README.md
@@ -490,6 +490,7 @@
 - `responsetype` - content type returned by web action, possible values: `html`, `http`, `json`, `text` and `svg` (default: `json`).
 - `basepath` - base URI path for endpoints (default: `/`)
 - `name` - identifier for API (default: `basepath`)
+- `secure_key` - auth key for secure web action
 
 ### add route (swagger)
 
diff --git a/lib/routes.js b/lib/routes.js
index a40e85e..6c1eaf1 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -104,6 +104,12 @@
       action: this.routeSwaggerAction(params)
     }
 
+    const pathParameters = this.parsePathParameters(params.relpath)
+
+    if (pathParameters.length) {
+      apidoc.pathParameters = pathParameters.map(this.createPathParameter)
+    }
+
     if (params.name) {
       apidoc.apiName = params.name
     }
@@ -117,13 +123,20 @@
     if (params.action.startsWith('/')) {
       namespace = names.parseNamespace(params.action)
     }
-    return {
+
+    const body = {
       name: id,
       namespace: namespace,
       backendMethod: `GET`,
       backendUrl: this.actionUrlPath(id, namespace),
       authkey: this.client.options.apiKey
     }
+
+    if (params.secure_key) {
+      body.secureKey = params.secure_key
+    }
+
+    return body
   }
 
   routeBasepath (params) {
@@ -143,6 +156,36 @@
   hasAccessToken () {
     return !!this.client.options.apigwToken
   }
+
+  // return list of path parameters from paths
+  // e.g. /book/{id}
+  // Multiple parameters are supported.
+  parsePathParameters (path) {
+    const regex = /{([^}]+)\}/g
+    const findAllParams = p => {
+      const ids = []
+      let id = regex.exec(p)
+      while (id) {
+        ids.push(id[1])
+        id = regex.exec(p)
+      }
+      return ids
+    }
+
+    return path.split('/')
+      .map(findAllParams)
+      .reduce((sum, el) => sum.concat(el), [])
+  }
+
+  createPathParameter (name) {
+    return {
+      name: name,
+      in: 'path',
+      description: `Default description for '${name}'`,
+      required: true,
+      type: 'string'
+    }
+  }
 }
 
 module.exports = Routes
diff --git a/test/unit/routes.test.js b/test/unit/routes.test.js
index 30eab78..e813a99 100644
--- a/test/unit/routes.test.js
+++ b/test/unit/routes.test.js
@@ -391,6 +391,42 @@
   return routes.create({relpath: '/hello', operation: 'GET', action: 'helloAction', responsetype: 'http'})
 })
 
+test('should create a route with secure key', t => {
+  t.plan(3)
+  const pathUrl = path => `https://openwhisk.ng.bluemix.net/api/v1/${path}`
+  const apiKey = 'username:password'
+  const secureKey = 'some_key'
+  const clientOptions = {apiKey}
+  const client = {pathUrl, options: clientOptions}
+
+  const body = {
+    apidoc: {
+      namespace: '_',
+      gatewayBasePath: '/',
+      gatewayPath: '/hello',
+      gatewayMethod: 'GET',
+      id: 'API:_:/',
+      action: {
+        name: 'helloAction',
+        namespace: '_',
+        backendMethod: 'GET',
+        backendUrl: 'https://openwhisk.ng.bluemix.net/api/v1/web/_/default/helloAction.http',
+        authkey: apiKey,
+        secureKey: secureKey
+      }
+    }
+  }
+
+  client.request = (method, path, _options) => {
+    t.is(method, 'POST')
+    t.is(path, routes.routeMgmtApiPath('createApi'))
+    t.deepEqual(_options.body, body)
+  }
+
+  const routes = new Routes(client)
+  return routes.create({relpath: '/hello', operation: 'GET', action: 'helloAction', secure_key: secureKey})
+})
+
 test('should create a route with apigwToken and action with package', t => {
   t.plan(4)
   const pathUrl = path => `https://openwhisk.ng.bluemix.net/api/v1/${path}`
@@ -631,6 +667,72 @@
   return routes.create({relpath: '/hello', operation: 'GET', action: '/test/helloAction'})
 })
 
+test('should create a route with path parameters', t => {
+  t.plan(3)
+  const pathUrl = path => `https://openwhisk.ng.bluemix.net/api/v1/${path}`
+  const apiKey = 'username:password'
+  const clientOptions = {apiKey}
+  const client = {pathUrl, options: clientOptions}
+
+  const body = {
+    apidoc: {
+      namespace: '_',
+      gatewayBasePath: '/',
+      gatewayPath: '/foo/{bar}/{baz}',
+      gatewayMethod: 'GET',
+      id: 'API:_:/',
+      action: {
+        name: 'helloAction',
+        namespace: '_',
+        backendMethod: 'GET',
+        backendUrl: 'https://openwhisk.ng.bluemix.net/api/v1/web/_/default/helloAction.http',
+        authkey: apiKey
+      },
+      pathParameters: [
+        {
+          name: 'bar',
+          in: 'path',
+          description: "Default description for 'bar'",
+          required: true,
+          type: 'string'
+        },
+        {
+          name: 'baz',
+          in: 'path',
+          description: "Default description for 'baz'",
+          required: true,
+          type: 'string'
+        }
+      ]
+    }
+  }
+
+  client.request = (method, path, _options) => {
+    t.is(method, 'POST')
+    t.is(path, routes.routeMgmtApiPath('createApi'))
+    t.deepEqual(_options.body, body)
+  }
+
+  const routes = new Routes(client)
+  return routes.create({relpath: '/foo/{bar}/{baz}', operation: 'GET', action: 'helloAction'})
+})
+
+test('should parse path parameters', t => {
+  const routes = new Routes()
+  t.deepEqual(routes.parsePathParameters('/foo/{bar}'), ['bar'])
+  t.deepEqual(routes.parsePathParameters('/{foo}/bar'), ['foo'])
+  t.deepEqual(routes.parsePathParameters('/{foo}/{bar}'), ['foo', 'bar'])
+
+  t.deepEqual(routes.parsePathParameters('/{foo}bar}'), ['foo'])
+  t.deepEqual(routes.parsePathParameters('/{foo}{bar}'), ['foo', 'bar'])
+  t.deepEqual(routes.parsePathParameters('/a{foo}c{bar}b'), ['foo', 'bar'])
+
+  t.deepEqual(routes.parsePathParameters('/foo/bar'), [])
+  t.deepEqual(routes.parsePathParameters('/{foo/bar}'), [])
+  t.deepEqual(routes.parsePathParameters('/foo/bar}'), [])
+  t.deepEqual(routes.parsePathParameters('/{foo/bar'), [])
+})
+
 test('create routes missing mandatory parameters', t => {
   const routes = new Routes()
   t.throws(() => { routes.create() }, /Missing mandatory parameters: relpath, operation, action/)