Repository: yargs/yargs
Pull request #881 (2017-05-24 08:52:54)

feat: middleware
OK this is a very WIP and ugly first draft of the middleware feature.
I will need some help to change passing the middleware using a chaining function instead of an array.
For more context check https://github.com/yargs/yargs/issues/876

@bcoe I would love to get some feedback from you.

TODO : 
- [x]  Add documentation
- [x]  Add more tests
- [x]  Cleanup
- [x]  Rebase and squash commits

bcoe: @Khaledgarbaya thanks for opening this pull! I have a lot of OSS time this weekend and will give feedback.
bcoe: @Khaledgarbaya one thing that's been on my mind, before we extend yargs with too many more features, is I'd love to split the massive README.md in the root folder into a few docs in a `./docs` folder, I was thinking something along the lines of:

`docs/examples`.
`docs/basics`.
`docs/advanced-features`.
`docs/commands`.

Haven't fully thought it out yet -- but would like to perform this decomposition in the fairly near future as we build out some more features.
Khaledgarbaya: @bcoe I guess we can wait until the restructuring PR is done and we can discuss this PR. I have no problem with this.
bcoe: @Khaledgarbaya sorry 👋 have been pretty busy juggling various other works (working on some pretty cool things at npm 😄 ).

I've taken a stab at restructuring the README over the past week, and would love your feedback:

https://github.com/yargs/yargs/pull/892

What section would this middleware fall under do you think (maybe its own section in advanced topics?) or an addition to the command API?

My only real concern with the pull request currently, is that the `command()` method ends up taking a lot of arguments -- this is partially my fault for already having overloaded the method so much.
Khaledgarbaya: hi @bcoe I was thinking of that too recently, I am thinking of two ways. 

1. is to have a map at yargs level that links middlewares to a specific command.
e.g.

```js
var argv = require('yargs')
    .usage('Usage: $0 <command> [options]')
    .command('count', 'Count the lines in a file')
    .middlewares({'count': [middleware1, middlware2]})
    .argv;
```

2. or have the use `middlewares` of the command 
```js
var argv = require('yargs')
    .usage('Usage: $0 <command> [options]')
    .command('count', 'Count the lines in a file')
    .middlewares([middleware1, middlware2])
    .argv;
```
First option will let us to define middlewares that can run before each command for example if we specify `'*'`. Second option is not bad too but if we follow the current implementation of yargs `middlewares` should follow the same patterns as `aliases` etc .. meaning you should be able to pass it as property if you go the object route or `export` it inside the command module if you use `commandDir`.

In conclusion option 2 will still introduce a new argument to the command and option 1 will reduce the number of arguments by one and give us the possibility to run the same middleware for all the commands if we want.

Given the structure of yargs I am leaning tward solution 1 since it won't involve a lot of changes to the library and in the future maybe we can improve the command creation.

Best,
Khaled

bcoe: 👋 hey @Khaledgarbaya sorry that I've been dropping this on the floor for so long, I've been on a couple trips this summer, and have also been dealing with some family issues

_long story short haven't had quite as many hours to work on OSS as I would like._

Now that I've refactored the docs for yargs, why don't you take a stab at adding a new advanced topic document on middleware, with a few practical examples; I think we can center the discussion of the optimal API around this document, and then work on getting this landed.

With yargs I've tried to take a slow methodical approach to evolving the API, which I understand can be a bit frustrating -- I promise we can get this landed though 👍 
Khaledgarbaya: hey, @bcoe, no worries I was also on vacation and I will definitely try to find to move this PR forward.
bcoe: @Khaledgarbaya a good way to get this slick feature over the finish line would be to join the community slack that I recently created:

http://devtoolscommunity.herokuapp.com/

would love to coordinate with you, and help you land this.
Khaledgarbaya: Hey @bcoe, I made some changes and added the docs Sorry for taking so long 
bcoe: @Morishiri interested in providing feedback on this feature? I'd love to unblock @Khaledgarbaya. 
Morishiri: I just took a look at it and it looks really nice, interesting feature, really usefull :) Fully approved

Patch:
diff --git a/docs/advanced.md b/docs/advanced.md
index 47b79ea..935eb15 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -425,3 +425,47 @@ some of yargs' parsing features:
 
 See the [yargs-parser](https://github.com/yargs/yargs-parser#configuration) module
 for detailed documentation of this feature.
+
+## Midleware
+Sometimes you might want to transform arguments before they reach the command handler.
+For example, you perhaps you want to validate that credentials have been provided and otherwise load credentials from a file.
+Middleware is simply a stack of functions, each of which is passed the the current parsed arguments, which it can in turn update by adding values, removing values, or overwriting values.
+
+Diagram:
+
+```
+                        --------------         --------------        ---------
+stdin ----> argv ----> | Middleware 1 | ----> | Middleware 2 | ---> | Command |
+                        --------------         --------------        ---------
+```
+
+### Example Credentials Middleware
+
+In this example, our middleware will check if the `username` and `password` is provided. If not, it will load them from `~/.credentials`, and fill in the `argv.username` and `argv.password` values.
+
+#### Middleware function
+
+```
+const normalizeCredentials = (argv) => {
+  if (!argv.username || !argv.password) {
+    const credentials = JSON.parse(fs.readSync('~/.credentials'))
+    return credentials
+  }
+  return {}
+}
+```
+
+#### yargs parsing configuration
+
+```
+var argv = require('yargs')
+  .usage('Usage: $0 <command> [options]')
+  .command('login', 'Authenticate user', (yargs) =>{
+        return yargs.option('username')
+                    .option('password')
+      } ,(argv) => {
+        authenticateUser(argv.username, argv.password)
+      })
+  .middlewares([normalizeCredentials])
+  .argv;
+```
diff --git a/lib/command.js b/lib/command.js
index 5ac848e..395c7c6 100644
--- a/lib/command.js
+++ b/lib/command.js
@@ -11,27 +11,26 @@ const DEFAULT_MARKER = /(^\*)|(^\$0)/
 // arguments.
 module.exports = function command (yargs, usage, validation) {
   const self = {}
-
   let handlers = {}
   let aliasMap = {}
   let defaultCommand
-  self.addHandler = function addHandler (cmd, description, builder, handler) {
+  self.addHandler = function addHandler (cmd, description, builder, handler, middlewares) {
     let aliases = []
     handler = handler || (() => {})
-
+    middlewares = middlewares || []
     if (Array.isArray(cmd)) {
       aliases = cmd.slice(1)
       cmd = cmd[0]
     } else if (typeof cmd === 'object') {
       let command = (Array.isArray(cmd.command) || typeof cmd.command === 'string') ? cmd.command : moduleName(cmd)
       if (cmd.aliases) command = [].concat(command).concat(cmd.aliases)
-      self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler)
+      self.addHandler(command, extractDesc(cmd), cmd.builder, cmd.handler, cmd.middlewares)
       return
     }
 
     // allow a module to be provided instead of separate builder and handler
     if (typeof builder === 'object' && builder.builder && typeof builder.handler === 'function') {
-      self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler)
+      self.addHandler([cmd].concat(aliases), description, builder.builder, builder.handler, builder.middlewares)
       return
     }
 
@@ -50,6 +49,7 @@ module.exports = function command (yargs, usage, validation) {
       }
       return true
     })
+
     // standardize on $0 for default command.
     if (parsedAliases.length === 0 && isDefault) parsedAliases.push('$0')
 
@@ -74,6 +74,7 @@ module.exports = function command (yargs, usage, validation) {
       description: description,
       handler,
       builder: builder || {},
+      middlewares: middlewares || [],
       demanded: parsedCommand.demanded,
       optional: parsedCommand.optional
     }
@@ -225,6 +226,12 @@ module.exports = function command (yargs, usage, validation) {
 
     if (commandHandler.handler && !yargs._hasOutput()) {
       yargs._setHasOutput()
+      if (commandHandler.middlewares.length > 0) {
+        const middlewareArgs = commandHandler.middlewares.reduce(function (initialObj, middleware) {
+          return Object.assign(initialObj, middleware(innerArgv))
+        }, {})
+        Object.assign(innerArgv, middlewareArgs)
+      }
       commandHandler.handler(innerArgv)
     }
 
diff --git a/yargs.js b/yargs.js
index 7f04ab4..011cc4f 100644
--- a/yargs.js
+++ b/yargs.js
@@ -338,9 +338,9 @@ function Yargs (processArgs, cwd, parentRequire) {
     return self
   }
 
-  self.command = function (cmd, description, builder, handler) {
-    argsert('<string|array|object> [string|boolean] [function|object] [function]', [cmd, description, builder, handler], arguments.length)
-    command.addHandler(cmd, description, builder, handler)
+  self.command = function (cmd, description, builder, handler, middlewares) {
+    argsert('<string|array|object> [string|boolean] [function|object] [function] [array]', [cmd, description, builder, handler, middlewares], arguments.length)
+    command.addHandler(cmd, description, builder, handler, middlewares)
     return self
   }
 


Test patch:
diff --git a/test/yargs.js b/test/yargs.js
index 80439c7..361dbec 100644
--- a/test/yargs.js
+++ b/test/yargs.js
@@ -289,7 +289,25 @@ describe('yargs dsl tests', () => {
         .exitProcess(false) // defaults to true.
         .argv
     })
-
+    it('runs all middleware before reaching the handler', function (done) {
+      yargs(['foo'])
+        .command(
+          'foo',
+          'handle foo things',
+          function () {},
+          function (argv) {
+            // we should get the argv filled with data from the middleware
+            argv._[0].should.equal('foo')
+            argv.hello.should.equal('world')
+            return done()
+          },
+        [function (argv) {
+          return {hello: 'world'}
+        }]
+        )
+        .exitProcess(false) // defaults to true.
+        .argv
+    })
     it('recommends a similar command if no command handler is found', () => {
       const r = checkOutput(() => {
         yargs(['boat'])