From 724df10222ef3aa916023863e1f3fd79a99c41db Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Fri, 4 Feb 2022 17:06:33 +0000 Subject: [PATCH] refactor(errors): Node.js style validation/errors (#40) --- errors.js | 22 ++++++++++++++++++++++ index.js | 26 +++++++++++++++++++------- test/index.js | 18 ++++++++++++++++-- validators.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 errors.js create mode 100644 validators.js diff --git a/errors.js b/errors.js new file mode 100644 index 0000000..e16a094 --- /dev/null +++ b/errors.js @@ -0,0 +1,22 @@ +'use strict'; + +class ERR_INVALID_ARG_TYPE extends TypeError { + constructor(name, expected, actual) { + super(`${name} must be ${expected} got ${actual}`); + this.code = 'ERR_INVALID_ARG_TYPE'; + } +} + +class ERR_NOT_IMPLEMENTED extends Error { + constructor(feature) { + super(`${feature} not implemented`); + this.code = 'ERR_NOT_IMPLEMENTED'; + } +} + +module.exports = { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_NOT_IMPLEMENTED + } +}; diff --git a/index.js b/index.js index 55bb436..8e7065b 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,11 @@ 'use strict'; const { - ArrayIsArray, ArrayPrototypeConcat, ArrayPrototypeIncludes, ArrayPrototypeSlice, ArrayPrototypePush, + ObjectHasOwn, StringPrototypeCharAt, StringPrototypeIncludes, StringPrototypeIndexOf, @@ -13,6 +13,17 @@ const { StringPrototypeStartsWith, } = require('./primordials'); +const { + codes: { + ERR_NOT_IMPLEMENTED + } +} = require('./errors'); + +const { + validateArray, + validateObject +} = require('./validators'); + function getMainArgs() { // This function is a placeholder for proposed process.mainArgs. // Work out where to slice process.argv for user supplied arguments. @@ -75,11 +86,12 @@ const parseArgs = ( argv = getMainArgs(), options = {} ) => { - if (typeof options !== 'object' || options === null) { - throw new Error('Whoops!'); - } - if (options.withValue !== undefined && !ArrayIsArray(options.withValue)) { - throw new Error('Whoops! options.withValue should be an array.'); + validateArray(argv, 'argv'); + validateObject(options, 'options'); + for (const key of ['withValue', 'multiples']) { + if (ObjectHasOwn(options, key)) { + validateArray(options[key], `options.${key}`); + } } const result = { @@ -104,7 +116,7 @@ const parseArgs = ( } else if ( StringPrototypeCharAt(arg, 1) !== '-' ) { // Look for shortcodes: -fXzy - throw new Error('What are we doing with shortcodes!?!'); + throw new ERR_NOT_IMPLEMENTED('shortcodes'); } arg = StringPrototypeSlice(arg, 2); // remove leading -- diff --git a/test/index.js b/test/index.js index b6cef24..8972c8b 100644 --- a/test/index.js +++ b/test/index.js @@ -205,11 +205,23 @@ test('excess leading dashes on options are retained', function(t) { // Test bad inputs +test('invalid argument passed for options', function(t) { + const passedArgs = ['--so=wat']; + + t.throws(function() { parseArgs(passedArgs, 'bad value'); }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + + t.end(); +}); + test('boolean passed to "withValue" option', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { withValue: true }; - t.throws(function() { parseArgs(passedArgs, passedOptions); }); + t.throws(function() { parseArgs(passedArgs, passedOptions); }, { + code: 'ERR_INVALID_ARG_TYPE' + }); t.end(); }); @@ -218,7 +230,9 @@ test('string passed to "withValue" option', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { withValue: 'so' }; - t.throws(function() { parseArgs(passedArgs, passedOptions); }); + t.throws(function() { parseArgs(passedArgs, passedOptions); }, { + code: 'ERR_INVALID_ARG_TYPE' + }); t.end(); }); diff --git a/validators.js b/validators.js new file mode 100644 index 0000000..a8be5ff --- /dev/null +++ b/validators.js @@ -0,0 +1,45 @@ +'use strict'; + +const { + ArrayIsArray, +} = require('./primordials'); + +const { + codes: { + ERR_INVALID_ARG_TYPE + } +} = require('./errors'); + +function validateArray(value, name) { + if (!ArrayIsArray(value)) { + throw new ERR_INVALID_ARG_TYPE(name, 'Array', value); + } +} + +/** + * @param {unknown} value + * @param {string} name + * @param {{ + * allowArray?: boolean, + * allowFunction?: boolean, + * nullable?: boolean + * }} [options] + */ +function validateObject(value, name, options) { + const useDefaultOptions = options == null; + const allowArray = useDefaultOptions ? false : options.allowArray; + const allowFunction = useDefaultOptions ? false : options.allowFunction; + const nullable = useDefaultOptions ? false : options.nullable; + if ((!nullable && value === null) || + (!allowArray && ArrayIsArray(value)) || + (typeof value !== 'object' && ( + !allowFunction || typeof value !== 'function' + ))) { + throw new ERR_INVALID_ARG_TYPE(name, 'Object', value); + } +} + +module.exports = { + validateArray, + validateObject, +};