Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: function argument validation #773

Merged
merged 6 commits into from Feb 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 66 additions & 0 deletions lib/argsert.js
@@ -0,0 +1,66 @@
const command = require('./command')()

const positionName = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth']

module.exports = function (expected, callerArguments, length) {
// preface the argument description with "cmd", so
// that we can run it through yargs' command parser.
var position = 0
var parsed = {demanded: [], optional: []}
if (typeof expected === 'object') {
length = callerArguments
callerArguments = expected
} else {
parsed = command.parseCommand('cmd ' + expected)
}
const args = [].slice.call(callerArguments)

while (args.length && args[args.length - 1] === undefined) args.pop()
length = length || args.length

if (length < parsed.demanded.length) {
throw Error('Not enough arguments provided. Expected ' + parsed.demanded.length +
' but received ' + args.length + '.')
}

const totalCommands = parsed.demanded.length + parsed.optional.length
if (length > totalCommands) {
throw Error('Too many arguments provided. Expected max ' + totalCommands +
' but received ' + length + '.')
}

parsed.demanded.forEach(function (demanded) {
const arg = args.shift()
const observedType = guessType(arg)
const matchingTypes = demanded.cmd.filter(function (type) {
return type === observedType || type === '*'
})
if (matchingTypes.length === 0) argumentTypeError(observedType, demanded.cmd, position, false)
position += 1
})

parsed.optional.forEach(function (optional) {
if (args.length === 0) return
const arg = args.shift()
const observedType = guessType(arg)
const matchingTypes = optional.cmd.filter(function (type) {
return type === observedType || type === '*'
})
if (matchingTypes.length === 0) argumentTypeError(observedType, optional.cmd, position, true)
position += 1
})
}

function guessType (arg) {
if (Array.isArray(arg)) {
return 'array'
} else if (arg === null) {
return 'null'
}
return typeof arg
}

function argumentTypeError (observedType, allowedTypes, position, optional) {
throw Error('Invalid ' + (positionName[position] || 'manyith') + ' argument.' +
' Expected ' + allowedTypes.join(' or ') + ' but received ' + observedType + '.')
}
6 changes: 3 additions & 3 deletions lib/command.js
Expand Up @@ -28,9 +28,9 @@ module.exports = function (yargs, usage, validation) {
return
}

var parsedCommand = parseCommand(cmd)
var parsedCommand = self.parseCommand(cmd)
aliases = aliases.map(function (alias) {
alias = parseCommand(alias).cmd // remove positional args
alias = self.parseCommand(alias).cmd // remove positional args
aliasMap[alias] = parsedCommand.cmd
return alias
})
Expand Down Expand Up @@ -94,7 +94,7 @@ module.exports = function (yargs, usage, validation) {
return false
}

function parseCommand (cmd) {
self.parseCommand = function (cmd) {
var extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ')
var splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/)
var bregex = /\.*[\][<>]/g
Expand Down
4 changes: 2 additions & 2 deletions lib/usage.js
Expand Up @@ -235,7 +235,7 @@ module.exports = function (yargs, y18n) {

var extra = [
type,
demandedOptions[key] ? '[' + __('required') + ']' : null,
(key in demandedOptions) ? '[' + __('required') + ']' : null,
options.choices && options.choices[key] ? '[' + __('choices:') + ' ' +
self.stringifiedValues(options.choices[key]) + ']' : null,
defaultString(options.default[key], options.defaultDescription[key])
Expand Down Expand Up @@ -330,7 +330,7 @@ module.exports = function (yargs, y18n) {
// copy descriptions.
if (descriptions[alias]) self.describe(key, descriptions[alias])
// copy demanded.
if (demandedOptions[alias]) yargs.demandOption(key, demandedOptions[alias].msg)
if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
// type messages.
if (~options.boolean.indexOf(alias)) yargs.boolean(key)
if (~options.count.indexOf(alias)) yargs.count(key)
Expand Down
2 changes: 1 addition & 1 deletion lib/validation.js
Expand Up @@ -98,7 +98,7 @@ module.exports = function (yargs, usage, y18n) {
if (missing) {
const customMsgs = []
Object.keys(missing).forEach(function (key) {
const msg = missing[key].msg
const msg = missing[key]
if (msg && customMsgs.indexOf(msg) < 0) {
customMsgs.push(msg)
}
Expand Down
98 changes: 98 additions & 0 deletions test/argsert.js
@@ -0,0 +1,98 @@
/* global describe, it */

const argsert = require('../lib/argsert')
const expect = require('chai').expect

require('chai').should()

describe('Argsert', function () {
it('does not throw exception if optional argument is not provided', function () {
argsert('[object]', [].slice.call(arguments))
})

it('throws exception if wrong type is provided for optional argument', function () {
function foo (opts) {
argsert('[object|number]', [].slice.call(arguments))
}
expect(function () {
foo('hello')
}).to.throw(/Invalid first argument. Expected object or number but received string./)
})

it('does not throw exception if optional argument is valid', function () {
function foo (opts) {
argsert('[object]', [].slice.call(arguments))
}
foo({foo: 'bar'})
})

it('throws exception if required argument is not provided', function () {
expect(function () {
argsert('<object>', [].slice.call(arguments))
}).to.throw(/Not enough arguments provided. Expected 1 but received 0./)
})

it('throws exception if required argument is of wrong type', function () {
function foo (opts) {
argsert('<object>', [].slice.call(arguments))
}
expect(function () {
foo('bar')
}).to.throw(/Invalid first argument. Expected object but received string./)
})

it('supports a combination of required and optional arguments', function () {
function foo (opts) {
argsert('<array> <string|object> [string|object]', [].slice.call(arguments))
}
foo([], 'foo', {})
})

it('throws an exception if too many arguments are provided', function () {
function foo (expected) {
argsert('<array> [batman]', [].slice.call(arguments))
}
expect(function () {
foo([], 33, 99)
}).to.throw(/Too many arguments provided. Expected max 2 but received 3./)
})

it('configures function to accept 0 parameters, if only arguments object is provided', function () {
function foo (expected) {
argsert([].slice.call(arguments))
}
expect(function () {
foo(99)
}).to.throw(/Too many arguments provided. Expected max 0 but received 1./)
})

it('allows for any type if * is provided', function () {
function foo (opts) {
argsert('<*>', [].slice.call(arguments))
}
foo('bar')
})

it('should ignore trailing undefined values', function () {
function foo (opts) {
argsert('<*>', [].slice.call(arguments))
}
foo('bar', undefined, undefined)
})

it('should not ignore undefined values that are not trailing', function () {
function foo (opts) {
argsert('<*>', [].slice.call(arguments))
}
expect(function () {
foo('bar', undefined, undefined, 33)
}).to.throw(/Too many arguments provided. Expected max 1 but received 4./)
})

it('supports null as special type', function () {
function foo (arg) {
argsert('<null>', [].slice.call(arguments))
}
foo(null)
})
})