From 5be206ac9ecd096531ed1726032484e7884293a8 Mon Sep 17 00:00:00 2001 From: Trevor Linton Date: Mon, 4 Feb 2019 15:39:29 -0800 Subject: [PATCH] feat: add applyBeforeValidation, for applying sync middleware before validation --- docs/api.md | 33 ++++++++++- lib/command.js | 13 ++++- lib/middleware.js | 18 +++++- test/middleware.js | 143 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 7 deletions(-) diff --git a/docs/api.md b/docs/api.md index e229d56da..d00f3c19f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -945,7 +945,7 @@ To submit a new translation for yargs: *The [Microsoft Terminology Search](http://www.microsoft.com/Language/en-US/Search.aspx) can be useful for finding the correct terminology in your locale.* -.middleware(callbacks) +.middleware(callbacks, [applyBeforeValidation]) ------------------------------------ Define global middleware functions to be called first, in list order, for all cli command. @@ -969,6 +969,37 @@ I'm another middleware function Running myCommand! ``` +Middleware can be applied before validation by setting the second parameter to `true`. This will execute the middleware prior to validation checks, but after parsing. + +Each callback is passed a reference to argv. The argv can be modified to affect the behavior of the validation and command execution. + +For example, an environment variable could potentially populate a required option: + +```js +var argv = require('yargs') + .middleware(function (argv) { + argv.username = process.env.USERNAME + argv.password = process.env.PASSWORD + }, true) + .command('do-something-logged-in', 'You must be logged in to perform this task' + { + 'username': { + 'demand': true, + 'string': true + }, + 'password': { + 'demand': true, + 'string': true + } + }, + function(argv) { + console.log('do something with the user login and password', argv.username, argv.password) + }) + ) + .argv + +``` + .nargs(key, count) ----------- diff --git a/lib/command.js b/lib/command.js index e643d1694..3f5507a06 100644 --- a/lib/command.js +++ b/lib/command.js @@ -2,7 +2,7 @@ const inspect = require('util').inspect const isPromise = require('./is-promise') -const {applyMiddleware} = require('./middleware') +const {applyMiddleware, applyPreCheckMiddlware} = require('./middleware') const path = require('path') const Parser = require('yargs-parser') @@ -224,6 +224,12 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { positionalMap = populatePositionals(commandHandler, innerArgv, currentContext, yargs) } + let preCheckMiddlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares || []) + preCheckMiddlewares = preCheckMiddlewares.filter((x) => x.applyBeforeValidation === true) + if (preCheckMiddlewares.length > 0 && !yargs._hasOutput()) { + applyPreCheckMiddlware(innerArgv, preCheckMiddlewares, yargs) + } + // we apply validation post-hoc, so that custom // checks get passed populated positional arguments. if (!yargs._hasOutput()) yargs._runValidation(innerArgv, aliases, positionalMap, yargs.parsed.error) @@ -231,9 +237,10 @@ module.exports = function command (yargs, usage, validation, globalMiddleware) { if (commandHandler.handler && !yargs._hasOutput()) { yargs._setHasOutput() - const middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares || []) + let middlewares = globalMiddleware.slice(0).concat(commandHandler.middlewares || []) + middlewares = middlewares.filter((x) => x.applyBeforeValidation !== true) - innerArgv = applyMiddleware(innerArgv, middlewares) + innerArgv = applyMiddleware(innerArgv, middlewares, yargs) const handlerResult = isPromise(innerArgv) ? innerArgv.then(argv => commandHandler.handler(argv)) diff --git a/lib/middleware.js b/lib/middleware.js index f58593fb7..5be51da7f 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -1,23 +1,27 @@ const isPromise = require('./is-promise') module.exports = function (globalMiddleware, context) { - return function (callback) { + return function (callback, applyBeforeValidation = false) { if (Array.isArray(callback)) { + for (let i = 0; i < callback.length; i++) { + callback[i].applyBeforeValidation = applyBeforeValidation + } Array.prototype.push.apply(globalMiddleware, callback) } else if (typeof callback === 'function') { + callback.applyBeforeValidation = applyBeforeValidation globalMiddleware.push(callback) } return context } } -module.exports.applyMiddleware = function (argv, middlewares) { +module.exports.applyMiddleware = function (argv, middlewares, yargs) { return middlewares .reduce((accumulation, middleware) => { if (isPromise(accumulation)) { return accumulation .then(initialObj => - Promise.all([initialObj, middleware(initialObj)]) + Promise.all([initialObj, middleware(initialObj, yargs)]) ) .then(([initialObj, middlewareObj]) => Object.assign(initialObj, middlewareObj) @@ -31,3 +35,11 @@ module.exports.applyMiddleware = function (argv, middlewares) { } }, argv) } + +module.exports.applyPreCheckMiddlware = function (argv, middlewares, yargs) { + for (let i = 0; i < middlewares.length; i++) { + if (middlewares[i](argv, yargs) instanceof Promise) { + throw new Error('The passed in middleware with applyBeforeValidation set to true may not be used with async functions.') + } + } +} diff --git a/test/middleware.js b/test/middleware.js index a19a9fdce..355f00669 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -49,6 +49,149 @@ describe('middleware', () => { .parse() }) + it('runs the before-validation middlware before reaching the handler', function (done) { + yargs(['mw']) + .middleware(function (argv) { + argv.mw = 'mw' + }, true) + .command( + 'mw', + 'adds func to middleware', + { + 'mw': { + 'demand': true, + 'string': true + } + }, + function (argv) { + // we should get the argv filled with data from the middleware + argv.mw.should.equal('mw') + return done() + } + ) + .exitProcess(false) // defaults to true. + .parse() + }) + + it('runs the before-validation middleware and ensures theres a context object with commands and availableOptions', function (done) { + yargs(['mw']) + .middleware(function (argv) { + argv.mw = 'foobar' + argv.other = true + }, true) + .command( + 'mw', + 'adds func to middleware', + { + 'mw': { + 'demand': true, + 'string': true + } + }, + function (argv) { + // we should get the argv filled with data from the middleware + argv.mw.should.equal('foobar') + argv.other.should.equal(true) + return done() + } + ) + .exitProcess(false) // defaults to true. + .parse() + }) + + it('runs the before-validation middlware with an array passed in and ensures theres a context object with commands and availableOptions', function (done) { + yargs(['mw']) + .middleware([function (argv) { + argv.mw = 'mw' + argv.other = true + }], true) + .command( + 'mw', + 'adds func to middleware', + { + 'mw': { + 'demand': true, + 'string': true + } + }, + function (argv) { + // we should get the argv filled with data from the middleware + argv.mw.should.equal('mw') + argv.other.should.equal(true) + return done() + } + ) + .exitProcess(false) // defaults to true. + .parse() + }) + + it('runs the before-validation middlware ensures if an async function is ran it throws an error', function (done) { + try { + yargs(['mw']) + .middleware([async function (argv) { + argv.mw = 'mw' + argv.other = true + }], true) + .command( + 'mw', + 'adds func to middleware', + { + 'mw': { + 'demand': true, + 'string': true + } + }, + function (argv) { + // we should get the argv filled with data from the middleware + argv.mw.should.equal('mw') + argv.other.should.equal(true) + return done(new Error('This should not have reached this point.')) + } + ) + .exitProcess(false) // defaults to true. + .parse() + } catch (err) { + expect(err.message).to.equal('The passed in middleware with applyBeforeValidation set to true may not be used with async functions.') + done() + } + }) + + it('Ensure middleware does not run non-before-validation middleware, and vice versa', function (done) { + let execPreOnce = false + let execPostOnce = false + yargs(['mw']) + .middleware([function (argv) { + expect(execPreOnce).to.equal(false) + execPreOnce = true + expect(argv).to.be.an('object') + argv.mw = 'mw' + argv.other = true + }], true) + .middleware([function (argv) { + expect(execPostOnce).to.equal(false) + execPostOnce = true + expect(argv).to.be.an('object') + }]) + .command( + 'mw', + 'adds func to middleware', + { + 'mw': { + 'demand': true, + 'string': true + } + }, + function (argv) { + // we should get the argv filled with data from the middleware + argv.mw.should.equal('mw') + argv.other.should.equal(true) + return done() + } + ) + .exitProcess(false) // defaults to true. + .parse() + }) + it('runs all middleware before reaching the handler', function (done) { yargs(['mw']) .middleware([