UNPKG

have2

Version:

Slick arguments validator for javascript functions.

568 lines (471 loc) 16.8 kB
'use strict' var test = require('tape') var have = require('./tools/require-have')() var check = require('./tools/case-checker')(have) var constants = require('./_constants') var NON_ARGS = constants.NON_ARGS var NON_OBJS = constants.NON_OBJS var FUNC = constants.FUNC // --- TESTS --- // test('have module', function (t) { t.equal(typeof have, 'function', 'should exports a function') t.end() }) test('have throws if `args` argument does not looks like an array or an object', function (t) { NON_ARGS.forEach(function (thing) { t.throws(function () { have(thing, {}) }, /argument should be/i, 'should throws with ' + Object.prototype.toString.call(thing)) }) t.end() }) test('have *not* throws if `args` is function `arguments`', function (t) { t.doesNotThrow(function () { var func = function () { have(arguments, { }) } func.call(this) }) t.end() }) test('have throws if `schema` argument does not looks like an object or non empty array', function (t) { NON_OBJS.concat([[]]).forEach(function (thing) { t.throws(function () { have([], thing) }, /schema/i, 'should throw with ' + Object.prototype.toString.call(thing)) }) t.end() } ) test('have with empty schema', function (t) { [ [[], {}], [[], [{}]], [[], [{}, {}]], [[], [[{}], {}]] ].forEach(function (args) { t.doesNotThrow(function () { have.apply(null, args) }, 'should *not* throws with ' + JSON.stringify(args[1]) + ' schema') t.doesNotThrow(function () { have.strict.apply(null, args) }, 'should *not* throws with ' + JSON.stringify(args[1]) + ' schema (in strict mode)') }) t.end() }) test('have with basic schema', function (t) { var SCHEMA = { one: 'string', two: 'number', three: 'function', four: 'object', five: 'boolean' } var args = ['str', 123, FUNC, { }, true] check(t).doesNotThrowIf({ 'all arguments given correctly': [ args, SCHEMA, { one: args[0], two: args[1], three: args[2], four: args[3], five: args[4] }] }) check(t).throwsIf({ 'first argument is missing': [[], SCHEMA, /one/i], 'second argument is missing': [['str'], SCHEMA, /two/i], 'third argument is missing': [['str', 123], SCHEMA, /three/i], 'fourth argument is missing': [['str', 123, FUNC], SCHEMA, /four/i], 'fifth argument is missing': [['str', 123, FUNC, {}], SCHEMA, /five/i], 'first argument is of invalid type': [[123], SCHEMA, /one/i], 'second argument is of invalid type': [['str', 'str'], SCHEMA, /two/i], 'third argument is of invalid type': [['str', 123, 'str'], SCHEMA, /three/i], 'fourth argument is of invalid type': [['str', 123, FUNC, 123], SCHEMA, /four/i], 'fifth argument is of invalid type': [['str', 123, FUNC, /rx/i, 123], SCHEMA, /five/i] }) t.end() }) test('have with RegExp schema', function (t) { var SCHEMA = { one: 'regex' } var args = [new RegExp(), /test/i] check(t).doesNotThrowIf({ 'RegExp instance given as argument': [[args[0]], SCHEMA, { one: args[0] }], 'regular expression literal given as argument': [[args[1]], SCHEMA, { one: args[1] }] }) check(t).throwsIf({ 'regex argument is missing': [[], SCHEMA, /one/i], 'regex argument is *not* a RegExp': [['123'], SCHEMA, /one/i] }) t.end() }) test('have with Date schema', function (t) { var args = [new Date()] var SCHEMA = { one: 'date' } check(t).doesNotThrowIf({ 'date argument is given correctly': [args, SCHEMA, { one: args[0] }] }) check(t).throwsIf({ 'date argument is *not* a Date instance': [[{ }], SCHEMA, /one/i], 'date argument is missing': [[], SCHEMA, /one/i], 'date argument is a string': [['' + new Date()], SCHEMA, /one/i] }) t.end() }) test('have with array schema', function (t) { var SCHEMA = { arr: 'array' } var args = [[123, 456]] check(t).doesNotThrowIf({ 'array argument given correctly': [args, SCHEMA, { arr: args[0] }] }) check(t).throwsIf({ 'array argument is missing': [[], SCHEMA, /arr/i], 'array argument is *not* an array': [['str'], SCHEMA, /arr/i] }) t.end() }) test('have with array member type specified schema', function (t) { var SCHEMA = { nums: 'number array' } var args = [[123, 456]] check(t).doesNotThrowIf({ 'array argument with members given correctly': [args, SCHEMA, { nums: args[0] }] }) check(t).throwsIf({ 'array member is falsy': [[[null]], SCHEMA, /element/i], 'array member is of invalid type': [[['str']], SCHEMA, /nums/i] }) t.end() }) test('have with OR argument schema', function (t) { var SCHEMA = { str: 'string or number' } check(t).doesNotThrowIf({ 'argument is of the first type': [['str'], SCHEMA, { str: 'str' }], 'argument is of the second type': [[123], SCHEMA, { str: 123 }] }) check(t).throwsIf({ 'argument is missing or falsy': [[null], SCHEMA, /str/i], 'argument is of neither the specified type': [[FUNC], SCHEMA, /str/i] }) t.end() }) test('have with optional argument schema', function (t) { var SCHEMA = { str: 'string', num: 'optional number', cb: 'function' } var cases = [ ['str', FUNC], ['str', 123, FUNC], ['str', null, FUNC], ['str', undefined, FUNC] ] check(t).doesNotThrowIf({ 'optional argument omitted but other arguments are given correctly': [cases[0], SCHEMA, { str: cases[0][0], cb: cases[0][1] }], 'optional arguments specified and all other arguments are given correctly': [cases[1], SCHEMA, { str: cases[1][0], num: cases[1][1], cb: cases[1][2] }], 'optional argument is specified as `null`': [cases[2], SCHEMA, { str: cases[2][0], num: cases[2][1], cb: cases[2][2] }], 'optional argument is specified as `undefined`': [cases[3], SCHEMA, { str: cases[3][0], num: cases[3][1], cb: cases[3][2] }] }) check(t).throwsIf({ 'arguments before the optional arg is missing': [[], SCHEMA, /str/i], 'arguments after the optional arg is missing': [['str', 123], SCHEMA, /cb/i], 'arguments before the optional arg is of invalid type': [[123], SCHEMA, /str/i], 'arguments after the optional arg is of invalid type': [['str', 123, 'str'], SCHEMA, /cb/i] }) t.end() }) test('have with complex `string array or number array` schema', function (t) { var SCHEMA = { arg: 'string array or number array' } NON_ARGS.forEach(function (thing) { t.throws(function () { have([thing], SCHEMA) }, /array/i, 'should throws if ' + Object.prototype.toString.call(thing) + ' is given') }) t.throws(function () { have([{ }], SCHEMA) }, /neither/i, 'should throws if array given but element is of neither type') t.doesNotThrow(function () { have([['string']], SCHEMA) }, 'should *not* throws if element is of string type') t.doesNotThrow(function () { have([[123]], SCHEMA) }, 'should *not* throws if element is of number type') t.end() }) test('have with complex `opt num, str, opt str, str, opt num` schema (ambiguous optionals)', function (t) { var SCHEMA = { one: 'opt num', two: 'str', three: 'opt str', four: 'str', five: 'opt num', six: 'opt str' } // WARN: explicit check for `undefined` is slippery, better to not rely on this. check(t).doesNotThrowIf({ 'three strings are given': [ ['str', 'abc', 'def'], SCHEMA, { two: 'str', three: 'abc', four: 'def', five: undefined, six: undefined }], 'a number and three strings are given': [ [123, 'a', 'b', 'c'], SCHEMA, { one: 123, two: 'a', three: 'b', four: 'c', five: undefined, six: undefined }], 'a number, three strings and undefined are given': [ [123, 'a', 'b', 'c', undefined], SCHEMA, { one: 123, two: 'a', three: 'b', four: 'c', five: undefined, six: undefined }], 'undefined, three strings and number are given': [ [undefined, 'a', 'b', 'c', 456], SCHEMA, { one: undefined, two: 'a', three: 'b', four: 'c', five: 456, six: undefined }], 'only the 3rd argument is undefined': [ [123, 'a', undefined, 'c', 456, 'd'], SCHEMA, { one: 123, two: 'a', three: undefined, four: 'c', five: 456, six: 'd' }], 'everything is given': [ [123, 'a', 'b', 'c', 456, 'd'], SCHEMA, { one: 123, two: 'a', three: 'b', four: 'c', five: 456, six: 'd' }] }) check(t).throwsIf({ 'nothing is given': [[], SCHEMA, /two/i], 'a single argument is given': [['two'], SCHEMA, /four/i], 'two numbers are given': [[123, 456], SCHEMA, /two/i], 'two strings are given': [['str', 'abc'], SCHEMA, /four/i], 'only the 3rd arg is omitted': [[123, 'str', 'abc', 456, 'def'], SCHEMA, /four/i] }) t.end() } ) // actually an invalid type but we're testing it to make sure nonetheless test('have with complex `str or opt num` type (nested optional)', function (t) { var SCHEMA = { one: 'str or opt num' } check(t).doesNotThrowIf({ 'argument is given but of neither type': [[{ }], SCHEMA], 'string argument is given': [['str'], SCHEMA], 'number argument is given': [[123], SCHEMA] }) t.end() }) test('have with complex `str or num or arr or func` type (nested ORs)', function (t) { var SCHEMA = { one: 'str or num or arr or func' } check(t).doesNotThrowIf({ 'string is given': [['str'], SCHEMA], 'number is given': [[123], SCHEMA], 'array is given': [[[]], SCHEMA], 'function is given': [[FUNC], SCHEMA] }) check(t).throwsIf({ 'object is given': [[{ }], SCHEMA] }) t.end() }) test('have with complex `num arr arr arr` type (nested arrays)', function (t) { var SCHEMA = { one: 'num arr arr arr' } var ARR_1L = [] var ARR_2L = [[]] var ARR_3L = [[[]]] var ARR_4L = [[[[]]]] var ARR_EXAMPLE_1 = [[[123]]] var ARR_EXAMPLE_2 = [[[123], [456]], [[789]]] check(t).doesNotThrowIf({ 'an empty array is given': [ [ ARR_1L ], SCHEMA, { one: ARR_1L }], 'a nested empty array is given': [ [ ARR_2L ], SCHEMA, { one: ARR_2L }], 'deeply nested empty array is given': [ [ ARR_3L ], SCHEMA, { one: ARR_3L }], 'multiple nested empty arrays are given': [ [ ARR_1L, ARR_2L, ARR_3L, ARR_4L ], SCHEMA, { one: ARR_1L }], 'number array is given at correct depth': [ [ ARR_EXAMPLE_1 ], SCHEMA, { one: ARR_EXAMPLE_1 }], 'multiple nested number arrays are given': [ [ ARR_EXAMPLE_2 ], SCHEMA, { one: ARR_EXAMPLE_2 }] }) check(t).throwsIf({ 'non-array is given at first nesting level': [[ [123] ], SCHEMA, /one/i], 'non-array is given at second nesting level': [[ [[123]] ], SCHEMA, /one/i] }) t.end() }) test('have with schema matchers variants', function (t) { var m, v, variants, arg var cases = {} var matchers = { 's|str|string': 'str', 'n|num|number': 1, 'b|bool|boolean': true, 'f|fun|func|function': function () {}, 'a|arr|array': [], 'o|obj|object|Object': {}, 'r|rx|regex|regexp': /regex/, 'd|date': new Date(), 'opt str|optional str': 'str' } for (m in matchers) { variants = m.split(/\|/) for (v in variants) { arg = matchers[m] cases['built-in "' + variants[v] + '" matcher specified'] = [[arg], { arg: variants[v] }, { arg: arg }] } } check(t).doesNotThrowIf(cases) check(t).throwsIf({ 'unknown matcher specified': [['str'], { arg: 'foo' }, /`arg`.+is not foo/i] }) check(t).throwsIf({ 'argument not correspond to matcher alias in schema': [[1], { arg: 's' }, /`arg`.+is not string/i] }) t.end() }) test('have with unexpected and extra arguments (strict mode)', function (t) { var SCHEMA = { one: 'str', tow: 'num', three: 'opt str' } check(t).throwsIf({ 'wrong optional argument specified': [['str', 1, 777], SCHEMA, /Unexpected argument "777"/i], 'extra argument specified': [['str', 1, 'str', 'extra'], SCHEMA, /Unexpected argument "extra"/i], 'extra undefined argument specified': [['str', 1, 'str', undefined], SCHEMA, /Unexpected argument "undefined"/i], 'wrong argument specified in named arguments': [{ one: 'str', tow: 2, extra: 3 }, SCHEMA, /Unexpected `extra` argument/i], 'extra argument specified in named arguments': [{ one: 'str', tow: 2, three: 'str', extra: 55 }, SCHEMA, /Unexpected `extra` argument/i], 'extra undefined argument specified in named arguments': [{ one: 'str', tow: 2, three: 'str', extra: undefined }, SCHEMA, /Unexpected `extra` argument/i] }, true) t.end() }) test('have with several schemas', function (t) { var obj = {} var date = new Date() var SCHEMA = [ { one1: 'obj', tow1: 'opt bool' }, { one2: 'str', tow2: 'opt obj', three2: 'opt bool' }, { one3: 'str', tow3: 'opt str', three3: 'opt bool' } ] check(t).doesNotThrowIf({ '[str, str, boot] arguments correspond to third schema in list': [['str1', 'str2', true], SCHEMA, { one3: 'str1', tow3: 'str2', three3: true }], '[str, obj, bool] arguments correspond to second schema in list': [['str1', obj, true], SCHEMA, { one2: 'str1', tow2: obj, three2: true }], '[str, obj] arguments correspond to second schema in list': [['str1', obj], SCHEMA, { one2: 'str1', tow2: obj }], '[str, date] arguments correspond to second schema in list': [['str1', date], SCHEMA, { one2: 'str1' }] }) check(t).throwsIf({ '[num] arguments not correspond to any schema in list': [[1], SCHEMA, /one1/i] }) t.end() }) test('have with several schemas (strict mode)', function (t) { var obj = {} var SCHEMA = [ { one1: 'obj', tow1: 'opt bool' }, { one2: 'str', tow2: 'opt obj', three2: 'opt bool' }, { one3: 'str', tow3: 'opt str', three3: 'opt bool' } ] var cases = { '[str, str, boot] arguments correspond to third schema in list': [['str1', 'str2', true], SCHEMA, { one3: 'str1', tow3: 'str2', three3: true }], '[str, obj, bool] arguments correspond to second schema in list': [['str1', obj, true], SCHEMA, { one2: 'str1', tow2: obj, three2: true }], '[str, obj] arguments correspond to second schema in list': [['str1', obj], SCHEMA, { one2: 'str1', tow2: obj }] } check(t).throwsIf({ '[num] arguments not correspond to any schema in list': [[1], SCHEMA, /one1/i], '[str, num] arguments not correspond to any schema in list': [['str1', 555], SCHEMA, /"555"/i], 'long string arguments not correspond to any schema in list': [['str1', 'str2', 'This string argument is longer when 15 characters'], SCHEMA, /"This string arg\.\."/i] }, true) check(t).doesNotThrowIf(cases, true) t.end() }) test('have with named arguments object', function (t) { var SCHEMA = { one: 'str', tow: 'str', three: 'opt num or num arr' } check(t).doesNotThrowIf({ '{ one: String, tow: String, three: Number } object correspond to schema': [ { one: 'one', tow: 'tow', three: 3 }, SCHEMA, { one: 'one', tow: 'tow', three: 3 }] }) check(t).throwsIf({ '{ one: String, tow: Number, three: Number } object not correspond to schema': [ { one: 'one', tow: 2, three: 3 }, SCHEMA, /`tow`.+is not/i] }) check(t).throwsIf({ '{ one: String, tow: Number, three: Number } object not correspond to schema': [ { one: 'one', tou: 2, three: 3 }, SCHEMA, /foo/i] }) t.end() }) test('have with array like arguments or named arguments object', function (t) { var SCHEMA = [ { one: 'str', tow: 'str', three: 'opt num or num arr' }, have.argumentsObject ] check(t).doesNotThrowIf({ '[String, String, Number] object correspond to schema': [ ['one', 'tow', 3], SCHEMA, { one: 'one', tow: 'tow', three: 3 }] }, true) check(t).doesNotThrowIf({ '{ one: String, tow: String, three: Number } object correspond to schema': [ { one: 'one', tow: 'tow', three: 3 }, SCHEMA, { one: 'one', tow: 'tow', three: 3 }] }, true) check(t).doesNotThrowIf({ '{ one: String, tow: String, three: Number } object correspond to schema': [ [{ one: 'one', tow: 'tow', three: 3 }], SCHEMA, { one: 'one', tow: 'tow', three: 3 }] }, true) check(t).throwsIf({ '{ one: String, tow: Number, three: Number } object not correspond to schema': [ [{ one: 'one', tow: 2, three: 3 }], SCHEMA, /`tow`.+is not/i] }, true) t.end() })