nonvalid
Version:
Simple callback-based JSON validator for complex use-cases
1,233 lines (1,135 loc) • 50.9 kB
JavaScript
'use strict';
const nonvalid = require('../src/nonvalid');
const random = require('random-seed');
function serialize(object) {
return JSON.stringify(object, (key, value) => {
switch (typeof value) {
case 'function':
return value.toString();
case 'bigint':
return value.toString() + 'n';
default:
return value;
}
});
}
function generateInt(g) {
const MAX_VALUE = 1000;
return g.intBetween(-MAX_VALUE, MAX_VALUE);
}
function generateFloat(g) {
const MIN_VALUE = 0;
const MAX_VALUE = 1;
return g.floatBetween(MIN_VALUE, MAX_VALUE);
}
function generateString(g) {
const MIN_LENGTH = 10;
const MAX_LENGTH = 15;
return g.string(g.intBetween(MIN_LENGTH, MAX_LENGTH));
}
function generateBoolean(g) {
return Boolean(g.range(2));
}
function generatePrimitive(g) {
switch (g.range(9)) {
case 0:
return generateInt(g);
case 1:
return generateFloat(g);
case 2:
return generateString(g);
case 3:
return generateBoolean(g);
case 4:
return null;
case 5:
return undefined;
case 6:
return BigInt(generateInt(g));
case 7:
return Symbol();
case 8:
return () => 0;
}
}
function generateTrees(args, nv, g) {
const { depth, breadth, injection } = args;
const path = injection ? args.path || [] : null;
if (depth === 0) {
return { trees: injection, path };
}
const injectionIndex = injection ? g.range(breadth) : -1;
const keys = generateBoolean(g)
? new Array(breadth).fill(0).map(_ => generateBoolean(g) ? generateString(g) : Symbol())
: null;
const values = new Array(breadth).fill(0).map((_, index) => {
const inject = index === injectionIndex;
if (inject || depth > 1 && generateBoolean(g)) {
return generateTrees({
...args,
depth: depth - 1,
injection: inject ? args.injection : null,
path: inject ? [...path, keys ? keys[index] : index] : null
}, nv, g);
} else {
const value = generatePrimitive(g);
return {
trees: [
value,
typeof value === 'function' || generateBoolean(g) ? value : v => v !== value
],
path: null
};
}
});
return {
trees: [0, 1]
.map(n => keys
? Object.fromEntries(values.map((item, i) => [keys[i], item.trees[n]]))
: values.map(item => item.trees[n])
).map((sub, n) => n === 1 && generateBoolean(g) ? v => nv(v, sub) : sub),
path: values.reduce((path, item) => path || item.path, null)
};
}
function perform(value, schema, result, error, extraErrorPath = []) {
const PARAMS = [[0, 0], [1, 1], [1, 10], [10, 1], [2, 4], [4, 2], [7, 7]];
for (const [depth, breadth] of PARAMS) {
const nv = nonvalid.instance();
const runSchema = (...args) => schema(nv, trees[0], path)(...args);
const options = {
depth,
breadth,
injection: [
value,
typeof schema === 'function' ? runSchema : schema
]
};
const {trees, path} = generateTrees(options, nv, random.create(serialize(options)));
const run = () => nv(...trees);
if (error) {
expect(run).toThrow(new Error(error));
} else {
expect(run()).toBe(result);
if (result) {
expect(nv.errorPath()).toEqual([...path, ...extraErrorPath]);
} else {
expect(nv.errorPath()).toBe(null);
}
}
}
}
const CANTPROCEED = 'Cannot proceed with validation after an error';
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runAllTimers();
});
describe('primitives', () => {
test('number', () => {
perform(123, 123, false);
perform(123, 123.5, true);
perform(123.5, 123.5, false);
perform(123.52, 123.51, true);
perform(123, -123, true);
});
test('string', () => {
perform('abc', 'abc', false);
perform('abc', 'abcd', true);
perform('abc', 'ab', true);
});
test('boolean', () => {
perform(true, true, false);
perform(false, false, false);
perform(true, false, true);
perform(false, true, true);
});
test('null', () => {
perform(null, null, false);
});
test('undefined', () => {
perform(undefined, undefined, false);
});
test('bigint', () => {
perform(BigInt(123), BigInt(123), false);
perform(BigInt(123), BigInt(124), true);
perform(BigInt(123), BigInt(-123), true);
});
test('symbol', () => {
const test = Symbol('test');
perform(test, test, false);
perform(test, Symbol('test'), true);
});
test('function', () => {
const f = () => 0;
const g = () => 0;
perform(f, nv => v => v !== f, false);
perform(f, nv => v => v !== g, true);
});
test('cross-type', () => {
const performBothWays = (a, b) => {
perform(a, b, true);
perform(b, a, true);
};
performBothWays('123', 123);
performBothWays(false, 0);
performBothWays(false, '');
performBothWays(null, 0);
performBothWays(null, '');
performBothWays(null, false);
performBothWays(undefined, 0);
performBothWays(undefined, '');
performBothWays(undefined, false);
performBothWays(undefined, null);
performBothWays(BigInt(123), 123);
performBothWays(BigInt(123), '123');
performBothWays(BigInt(123), '123n');
performBothWays(Symbol('test'), 'test');
performBothWays(Symbol('test'), false);
performBothWays(Symbol('test'), null);
performBothWays(Symbol('test'), undefined);
const f = () => 0;
perform(f, f.toString(), true);
perform(f, false, true);
perform(f, null, true);
perform(f, undefined, true);
});
});
describe('matchers', () => {
const E = 'test error';
const OUT = name => new Error(`${name}() called without arguments outside of any context`);
const ARGS = 'Matchers are supposed to be run with exactly one or no arguments';
test('number', () => {
expect(() => nonvalid.instance().number()).toThrow(OUT('number'));
expect(nonvalid.instance().number(123)).toBe(true);
expect(nonvalid.instance().number('123')).toBe(false);
expect(() => nonvalid.instance().number(1, 2)).toThrow(new Error(ARGS));
perform(123, nv => v => !nv.number(), false);
perform(-123, nv => v => !nv.number(v), false);
perform(NaN, nv => v => !nv.number(), true);
perform(Infinity, nv => v => !nv.number(v) && E, E);
perform(-Infinity, nv => v => !nv.number(), true);
perform('123', nv => v => !nv.number(v), true);
perform('-123', nv => v => !nv.number() && E, E);
perform('-123', nv => v => !nv.number('a', 'b') && E, null, ARGS);
});
test('string', () => {
expect(() => nonvalid.instance().string()).toThrow(OUT('string'));
expect(nonvalid.instance().string('123')).toBe(true);
expect(nonvalid.instance().string(123)).toBe(false);
perform('', nv => v => !nv.string(), false);
perform('123', nv => v => !nv.string(v), false);
perform('abc', nv => v => !nv.string(), false);
perform(123, nv => v => !nv.string() && E, E);
perform(null, nv => v => !nv.string(v), true);
});
test('boolean', () => {
expect(() => nonvalid.instance().boolean()).toThrow(OUT('boolean'));
expect(nonvalid.instance().boolean(false)).toBe(true);
expect(nonvalid.instance().boolean(null)).toBe(false);
perform(false, nv => v => !nv.boolean(), false);
perform(true, nv => v => !nv.boolean(v), false);
perform(null, nv => v => !nv.boolean(v) && E, E);
perform(undefined, nv => v => !nv.boolean(), true);
perform('false', nv => v => !nv.boolean() && E, E);
perform('true', nv => v => !nv.boolean(v), true);
});
test('null', () => {
expect(() => nonvalid.instance().null()).toThrow(OUT('null'));
expect(nonvalid.instance().null(null)).toBe(true);
expect(nonvalid.instance().null(false)).toBe(false);
perform(null, nv => v => !nv.null(), false);
perform(null, nv => v => !nv.null(v), false);
perform(undefined, nv => v => !nv.null(), true);
perform('null', nv => v => !nv.null(v) && E, E);
});
test('undefined', () => {
expect(() => nonvalid.instance().undefined()).toThrow(OUT('undefined'));
expect(nonvalid.instance().undefined(undefined)).toBe(true);
expect(nonvalid.instance().undefined(null)).toBe(false);
perform(undefined, nv => v => !nv.undefined(), false);
perform(undefined, nv => v => !nv.undefined(v), false);
perform(null, nv => v => !nv.undefined() && E, E);
perform('undefined', nv => v => !nv.undefined(v), true);
});
test('defined', () => {
expect(() => nonvalid.instance().defined()).toThrow(OUT('defined'));
expect(nonvalid.instance().defined(null)).toBe(true);
expect(nonvalid.instance().defined(undefined)).toBe(false);
perform('undefined', nv => v => !nv.defined(), false);
perform(null, nv => v => !nv.defined(v), false);
perform(false, nv => v => !nv.defined(v), false);
perform(undefined, nv => v => !nv.defined(), true);
perform(undefined, nv => v => !nv.defined(v) && E, E);
});
test('bigint', () => {
expect(() => nonvalid.instance().bigint()).toThrow(OUT('bigint'));
expect(nonvalid.instance().bigint(BigInt(123))).toBe(true);
expect(nonvalid.instance().bigint(123)).toBe(false);
perform(BigInt(123), nv => v => !nv.bigint(), false);
perform(BigInt(-123), nv => v => !nv.bigint(v), false);
perform(123, nv => v => !nv.bigint(v), true);
perform('123n', nv => v => !nv.bigint() && E, E);
});
test('symbol', () => {
expect(() => nonvalid.instance().symbol()).toThrow(OUT('symbol'));
expect(nonvalid.instance().symbol(Symbol())).toBe(true);
expect(nonvalid.instance().symbol({})).toBe(false);
perform(Symbol('test'), nv => v => !nv.symbol(), false);
perform('Symbol', nv => v => !nv.symbol(v), true);
perform({}, nv => v => !nv.symbol() && E, E);
});
test('function', () => {
expect(() => nonvalid.instance().function()).toThrow(OUT('function'));
expect(nonvalid.instance().function(() => {})).toBe(true);
expect(nonvalid.instance().function({})).toBe(false);
perform(() => 0, nv => v => !nv.function(), false);
perform(() => 0, nv => v => nv.function(), true);
perform(() => 0, nv => v => !nv.function(v), false);
perform(() => 0, nv => v => nv.function(v), true);
perform(() => 0, nv => v => nv.path().length === 0
? !nv.function(() => nv.value()) : nv.function(() => nv.root()), false);
perform(() => 0, nv => v => nv.path().length === 0
? nv.function(() => nv.value()) : !nv.function(() => nv.root()), true);
perform({}, nv => v => !nv.function(), true);
perform('function', nv => v => !nv.function(v) && E, E);
});
test('array', () => {
expect(() => nonvalid.instance().array()).toThrow(OUT('array'));
expect(nonvalid.instance().array([])).toBe(true);
expect(nonvalid.instance().array({})).toBe(false);
perform([], nv => v => !nv.array(), false);
perform([1, 2, 3], nv => v => !nv.array(v), false);
perform({}, nv => v => !nv.array() && E, E);
perform('[]', nv => v => !nv.array(v), true);
});
test('object', () => {
expect(() => nonvalid.instance().object()).toThrow(OUT('object'));
expect(nonvalid.instance().object({})).toBe(true);
expect(nonvalid.instance().object([])).toBe(false);
expect(nonvalid.instance().object(() => {})).toBe(false);
perform({}, nv => v => !nv.object(), false);
perform({a: 'bc'}, nv => v => !nv.object(v), false);
perform([], nv => v => !nv.object(v) && E, E);
perform(null, nv => v => !nv.object(), true);
perform(() => 0, nv => v => !nv.object() && E, E);
});
test('get', () => {
expect(() => nonvalid.instance().get()).toThrow(OUT('get'));
expect(nonvalid.instance().get(123)).toBe(123);
expect(nonvalid.instance().get(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
perform(123, nv => v => nv.get() !== 123, false);
perform('abc', nv => v => nv.get(v) !== 'abc', false);
perform(123, nv => v => nv.get(v) === 123 && E, E);
perform('abc', nv => v => nv.get() === 'abc', true);
});
test('adding matcher', () => {
const nv = nonvalid.instance();
nv.addMatcher('addOne', n => n + 1);
expect(() => nv.addOne()).toThrow(OUT('addOne'));
expect(() => nv.addOne(1, 2)).toThrow(new Error(ARGS));
expect(nv.addOne(5)).toBe(6);
const s = Symbol('abc');
nv.addMatcher(s, n => typeof n);
nv(123, () => {
nv.addMatcher(function subtractOne(n) { return n - 1; });
expect(nv.addOne(6)).toBe(7);
expect(nv.subtractOne(10)).toBe(9);
expect(nv.addOne()).toBe(124);
expect(nv.subtractOne()).toBe(122);
expect(nv.addOne(() => nv.value())).toBe(124);
expect(nv.subtractOne(() => nv.value())).toBe(122);
expect(nv[s](() => nv.value())).toBe('number');
nv.addMatcher('anotherMatcher', function wrongName(n) { return n; });
expect(nv.anotherMatcher('abc')).toBe('abc');
expect(() => nv.wrongName('abc')).toThrow();
expect(() => nv.anotherMatcher(() => {}, () => {})).toThrow(new Error(ARGS));
});
const NONNAMEDFUNC = 'Single addMatcher argument must be a named function';
const NONFUNC2 = 'Second addMatcher argument must be a function';
const NUMARGSA = 'addMatcher expects exactly one or two arguments';
const NUMARGSM = 'Matcher must accept exactly one parameter';
const TYPE = 'Name of a matcher should be either string or symbol';
const EXIST = name => `Validator already has property "${name}"`;
expect(() => nv.addMatcher({})).toThrow(new Error(NONNAMEDFUNC));
expect(() => nv.addMatcher(() => {})).toThrow(new Error(NONNAMEDFUNC));
expect(() => nv.addMatcher('addTwo', +2)).toThrow(new Error(NONFUNC2));
expect(() => nv.addMatcher()).toThrow(new Error(NUMARGSA));
expect(() => nv.addMatcher('addThree', n => n + 3, n => n)).toThrow(new Error(NUMARGSA));
expect(() => nv.addMatcher('addFour', () => 4)).toThrow(new Error(NUMARGSM));
expect(() => nv.addMatcher('addFive', (n, m) => n + 5)).toThrow(new Error(NUMARGSM));
expect(() => nv.addMatcher(3, n => n + 6)).toThrow(new Error(TYPE));
expect(() => nv.addMatcher(function a(){}, function b(){})).toThrow(new Error(TYPE));
expect(() => nv.addMatcher(function addOne(n) { return n + 1; }))
.toThrow(new Error(EXIST('addOne')));
expect(() => nv.addMatcher('subtractOne', n => n - 1))
.toThrow(new Error(EXIST('subtractOne')));
expect(() => nv.addMatcher('value', n => n)).toThrow(new Error(EXIST('value')));
expect(() => nv.addMatcher('other', n => n)).toThrow(new Error(EXIST('other')));
expect(() => nv.addMatcher(s, n => n)).toThrow(new Error(EXIST(s.toString())));
});
});
describe('keys and values', () => {
const VALUE_OUT = 'value() called outside of any context';
const KEY_OUT = 'key() called outside of any context';
const INDEX_OUT = 'index() called outside of any context';
const INDEX_OBJECT = 'index() can be called for arrays only';
const KEY_ARRAY = 'key() can be called for objects only';
const VALUES = [123, { abc: 123 }, ['abc', 123]];
test('context errors', () => {
expect(() => nonvalid.instance().value()).toThrow(new Error(VALUE_OUT));
expect(() => nonvalid.instance().key()).toThrow(new Error(KEY_OUT));
expect(() => nonvalid.instance().index()).toThrow(new Error(INDEX_OUT));
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(() => nv(value, () => nv.key())).toThrow(new Error(KEY_OUT));
}
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(() => nv(value, () => nv.index())).toThrow(new Error(INDEX_OUT));
}
const s = Symbol();
perform({ abc: 123 }, nv => v => nv(v, { abc: () => nv.index() }), null, INDEX_OBJECT);
perform({ [s]: 'abc' }, nv => v => nv(v, { [s]: () => nv.index() }), null, INDEX_OBJECT);
perform({ abc: 123 }, nv => v => nv({ [nv.other]: () => nv.index() }), null, INDEX_OBJECT);
perform([123], nv => v => nv([() => nv.key()]), null, KEY_ARRAY);
perform([123], nv => v => nv(v, [nv.end, () => nv.key()]), null, KEY_ARRAY);
});
test('retrieving keys and values', () => {
for (const value of VALUES) {
perform(value, (nv, tree, path) => (...args) => {
expect(args.length).toBe(2);
expect(args[0]).toBe(value);
expect(nv.value()).toBe(value);
if (path.length > 0) {
const key = path[path.length - 1];
expect(args[1]).toBe(key);
if (typeof key === 'number') {
expect(nv.index()).toBe(key);
} else {
expect(nv.key()).toBe(key);
}
} else {
expect(args[1]).toBe(undefined);
}
return false;
}, false);
}
});
});
describe('navigation', () => {
const ROOT_OUT = 'root() called outside of any object or array';
const UP_OUT = 'up() call navigates above any object or array';
const VALUES = [123, { abc: 123 }, ['abc', 123]];
test('root', () => {
expect(() => nonvalid.instance().root()).toThrow(new Error(ROOT_OUT));
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(() => nv(value, () => nv.root())).toThrow(new Error(ROOT_OUT));
}
for (const value of VALUES) {
perform(value, (nv, tree, path) => () => {
if (path.length > 0) {
expect(nv.root()).toBe(tree);
}
return false;
}, false);
}
});
test('up', () => {
expect(() => nonvalid.instance().up()).toThrow(new Error(UP_OUT));
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(() => nv(value, () => nv.up())).toThrow(new Error(UP_OUT));
}
for (const value of VALUES) {
perform(value, (nv, tree, path) => () => nv.up(path.length), null, UP_OUT);
perform(value, (nv, tree, path) => () => nv.up(path.length * 2), null, UP_OUT);
perform(value, (nv, tree, path) => () => {
if (path.length > 0) {
let current = tree;
for (let i = path.length - 1; i >= 0; i--) {
expect(nv.up(i)).toBe(current);
if (i === 0) {
expect(nv.up()).toBe(current);
} else {
current = current[path[path.length - i - 1]];
}
}
}
return false;
}, false);
}
});
test('safe', () => {
const E = { error: 'test' };
const navigate = (object, path) => {
let result = object;
for (const key of path) {
result = result[key];
}
return result;
};
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(nv.defined(() => nv.root())).toBe(true);
expect(nv.object(() => nv.root())).toBe(false);
}
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(() => nv(value, () => nv.null(() => nv.root()))).toThrow(new Error(ROOT_OUT));
}
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(nv.undefined(() => nv.up())).toBe(false);
expect(nv.function(() => nv.up())).toBe(true);
}
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(() => nv(value, () => nv.get(() => nv.up()))).toThrow(new Error(UP_OUT));
}
for (const value of VALUES) {
const nv = nonvalid.instance();
expect(nv.undefined(() => nv.value())).toBe(false);
expect(nv.function(() => nv.value())).toBe(true);
}
for (const value of VALUES) {
perform(value, (nv, tree, treePath) => () => {
if (treePath.length > 0) {
let extraPath;
if (Array.isArray(value)) {
extraPath = [0];
} else if (typeof value === 'object') {
extraPath = [Object.keys(value)[0]];
} else {
extraPath = [];
}
const path = [...treePath, ...extraPath];
let current = tree;
for (let i = 0; i <= path.length; i++) {
const getRoot = () => navigate(nv.root(), path.slice(0, i));
const getUp = () => navigate(nv.up(treePath.length - 1), path.slice(0, i));
const getUpOrValue = () => i < treePath.length
? nv.up(treePath.length - 1 - i)
: (i === treePath.length ? nv.value() : nv.value()[extraPath[0]]);
expect(nv.get(() => getRoot())).toBe(current);
expect(nv.get(() => getUp())).toBe(current);
expect(nv.get(() => getUpOrValue())).toBe(current);
expect(nv.get(() => getRoot().__n0)).toBe(undefined);
expect(nv.get(() => getUp().__n0)).toBe(undefined);
expect(nv.get(() => getUpOrValue().__n0)).toBe(undefined);
expect(nv.get(() => getRoot().__n0.__n1)).toBe(undefined);
expect(nv.get(() => getUp().__n0.__n1)).toBe(undefined);
expect(nv.get(() => getUpOrValue().__n0.__n1)).toBe(undefined);
expect(nv.get(() => getRoot()[Symbol()])).toBe(undefined);
expect(nv.get(() => getUp()[Symbol()])).toBe(undefined);
expect(nv.get(() => getUpOrValue()[Symbol()])).toBe(undefined);
if (i < path.length) {
current = current[path[i]];
}
}
}
return false;
}, false);
perform(value, (nv, tree, path) =>
() => path.length === 0 ? E : nv.defined(() => nv.root()) && E, E);
perform(value, (nv, tree, path) =>
() => path.length === 0 ? false : nv.undefined(() => nv.root()) && E, false);
perform(value, (nv, tree, path) => () => path.length === 0 ? true :
nv.object(() => nv.up()) || nv.array(() => nv.up()), true);
perform(value, (nv, tree, path) => () => path.length === 0 ? false :
nv.object(() => nv.up().__n0) || nv.array(() => nv.up().__n0), false);
perform(value, (nv, tree, path) =>
() => path.length === 0 ? false : nv.defined(() => nv.root().__n0) && E, false);
perform(value, (nv, tree, path) =>
() => path.length === 0 ? E : nv.undefined(() => nv.root().__n0) && E, E);
perform(value, (nv, tree, path) => () => path.length === 0 ? E :
!nv.null(() => nv.value()) && E, E);
perform(value, (nv, tree, path) => () => path.length === 0 ? true :
!nv.null(() => nv.value().__n0) && true, true);
}
});
test('safe, unconsumed', () => {
const UNCONSUMED = 'Value created in safe context was not consumed by any matcher';
const nv = nonvalid.instance();
nv(123, () => nv.get(() => {
nv.value();
return nv.value();
}));
expect(() => jest.runAllTimers()).toThrow(new Error(UNCONSUMED));
});
test('safe, misused', () => {
const MISUSED = 'Value created in safe context was used improperly';
const nv = nonvalid.instance();
nv({ v: 123 }, () => nv.get(() => {
const a = nv.value();
expect(Object.hasOwnProperty.call(a, 'v')).toBe(false);
const b = [][a];
return a;
}));
expect(() => jest.runAllTimers()).toThrow(new Error(MISUSED));
});
test('safe, unreturned', () => {
const UNRETURNED = 'Callback didn’t perform navigation or didn’t return its result';
{
const nv = nonvalid.instance();
expect(() => nv(123, () => {
expect(() => nv.get(() => 123)).toThrow(new Error(UNRETURNED));
})).toThrow(new Error(CANTPROCEED));
}
{
const nv = nonvalid.instance();
expect(() => nv(123, () => {
expect(() => nv.get(() => ({ unwrap: 'fake' }))).toThrow(new Error(UNRETURNED));
})).toThrow(new Error(CANTPROCEED));
}
});
test('safe multilevel', () => {
const E = 'safe error';
perform(
{ a: { aa: 1, ab: 2 }, b: { ba: 3, bb: 'aa' } },
nv => () => nv({
a: () => false,
b: { ba: () => false, bb: () => nv.undefined(() => nv.up(1).a[nv.value()]) && E }
}), false
);
perform(
{ a: { aa: 1, ab: 2 }, b: { ba: 3, bb: 'ba' } },
nv => () => nv({
a: () => false,
b: { ba: () => false, bb: () => nv.undefined(() => nv.up(1).a[nv.value()]) && E }
}), E, false, ['b', 'bb']
);
const objectA = {
a: { b: { bb: 'cc' } },
c: { d: { aa: 'bb' } },
e: { f: 'aa' }
};
{
const nv = nonvalid.instance();
expect(nv(objectA,
() => nv.get(() => nv.value().a.b[nv.value().c.d[nv.value().e.f]])))
.toBe('cc');
}
{
const nv = nonvalid.instance();
expect(nv(objectA,
() => nv.get(() => nv.value().a.b[
nv.get(() => nv.value().c.d[nv.get(() => nv.value().e.f)])
]))).toBe('cc');
}
{
const nv = nonvalid.instance();
expect(nv(objectA,
() => nv.undefined(() => nv.value().a.b[nv.value().c.d[nv.value().e.f]])))
.toBe(false);
}
const objectsA = [
{
aa: { b: { bb: 'cc' } },
c: { d: { aa: 'bb' } },
e: { f: 'aa' }
},
{
a: { bb: { bb: 'cc' } },
c: { d: { aa: 'bb' } },
e: { f: 'aa' }
},
{
a: { b: { bb: 'cc' } },
cc: { d: { aa: 'bb' } },
e: { f: 'aa' }
},
{
a: { b: { bb: 'cc' } },
c: { dd: { aa: 'bb' } },
e: { f: 'aa' }
},
{
a: { b: { bb: 'cc' } },
c: { d: { aa: 'bb' } },
ee: { f: 'aa' }
},
{
a: { b: { bb: 'cc' } },
c: { d: { aa: 'bb' } },
e: { ff: 'aa' }
},
{
a: { b: { bb: 'cc' } },
c: { d: { aa: 'bb' } },
e: { f: 'aaa' }
},
{
a: { b: { bb: 'cc' } },
c: { d: { aa: 'bbb' } },
e: { f: 'aa' }
},
{},
null
];
for (const o of objectsA) {
const nv = nonvalid.instance();
expect(nv(o, () => nv.undefined(() => nv.value().a.b[nv.value().c.d[nv.value().e.f]])))
.toBe(true);
}
const objectB = {
a: { b: 'aa' },
aa: { c: { d: 'bb' } },
bb: { e: { f: 'cc' } }
};
{
const nv = nonvalid.instance();
expect(nv(objectB,
() => nv.get(() => nv.value()[nv.value()[nv.value().a.b].c.d].e.f)))
.toBe('cc');
}
{
const nv = nonvalid.instance();
expect(nv(objectB,
() => nv.get(() => nv.value()[
nv.get(() => nv.value()[nv.get(() => nv.value().a.b)].c.d)
].e.f))).toBe('cc');
}
{
const nv = nonvalid.instance();
expect(nv(objectB,
() => nv.undefined(() => nv.value()[nv.value()[nv.value().a.b].c.d].e.f)))
.toBe(false);
}
const objectsB = [
{
aaa: { b: 'aa' },
aa: { c: { d: 'bb' } },
bb: { e: { f: 'cc' } }
},
{
a: { bb: 'aa' },
aa: { c: { d: 'bb' } },
bb: { e: { f: 'cc' } }
},
{
a: { b: 'aa' },
aaa: { c: { d: 'bb' } },
bb: { e: { f: 'cc' } }
},
{
a: { b: 'aa' },
aa: { cc: { d: 'bb' } },
bb: { e: { f: 'cc' } }
},
{
a: { b: 'aa' },
aa: { c: { dd: 'bb' } },
bb: { e: { f: 'cc' } }
},
{
a: { b: 'aa' },
aa: { c: { d: 'bb' } },
bbb: { e: { f: 'cc' } }
},
{
a: { b: 'aa' },
aa: { c: { d: 'bb' } },
bb: { ee: { f: 'cc' } }
},
{
a: { b: 'aa' },
aa: { c: { d: 'bb' } },
bb: { e: { ff: 'cc' } }
},
{},
null
];
for (const o of objectsB) {
const nv = nonvalid.instance();
expect(nv(o, () => nv.undefined(() => nv.value()[nv.value()[nv.value().a.b].c.d].e.f)))
.toBe(true);
}
const objectC = { a: 'a', b: 'b' };
{
const nv = nonvalid.instance();
expect(nv(objectC, {
a: () => nv.get(() => nv.root()[nv.up()[nv.value()]]),
b: 'b'
})).toBe('a');
}
{
const nv = nonvalid.instance();
expect(nv(objectC, {
a: () => nv.get(() => nv.get() === 'a' ? nv.root().a : nv.root().b),
b: 'b'
})).toBe('a');
}
{
const nv = nonvalid.instance();
expect(nv(objectC, {
a: () => nv.get(() => nv.get() === 'z' ? nv.root().a : nv.root().b),
b: 'b'
})).toBe('b');
}
{
const nv = nonvalid.instance();
expect(nv(objectC, {
a: () => nv.get(() => nv.get('z') === 'z' ? nv.root().a : nv.root().b),
b: 'b'
})).toBe('a');
}
{
const nv = nonvalid.instance();
let t;
expect(nv(objectC, {
a: () => nv.defined(() => nv.value()[t = nv.value()]),
b: () => nv.get(() => nv.root()[t])
})).toBe('a');
}
{
const nv = nonvalid.instance();
let t;
expect(nv(objectC, {
a: () => nv.undefined(() => t = nv.root()),
b: () => nv.undefined(() => nv.root()[t])
})).toBe(true);
}
{
const nv = nonvalid.instance();
const string = 'string';
const symbol = Symbol();
let counter = 0;
nv({
1: 'v1',
25: 'v25',
n1: 1,
n25: 25,
s25: '25',
true: 'true',
null: 'null',
tTrue: true,
sTrue: 'true',
tNull: null,
sNull: 'null',
array: [2, 100, string, symbol, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'],
string: string,
[symbol]: symbol
}, {
1: () => false,
25: () => false,
true: () => false,
null: () => false,
n1: () => false,
n25: () => false,
s25: () => false,
tTrue: () => false,
sTrue: () => false,
tNull: () => false,
sNull: () => false,
array: () => {
expect(nv.get(() => nv.value()[nv.value()[0]])).toBe(string);
expect(nv.get(() => nv.value()[nv.value()['0']])).toBe(string);
expect(nv.get(() => nv.value()[nv.value()['+0']])).toBe(undefined);
expect(nv.get(() => nv.value()[12])).toBe('i');
expect(nv.get(() => nv.value()['12'])).toBe('i');
expect(nv.get(() => nv.value()['12.'])).toBe(undefined);
expect(nv.get(() => nv.value()['abc'])).toBe(undefined);
expect(nv.get(() => nv.value()[symbol])).toBe(undefined);
expect(nv.get(() => nv.value()[nv.value()[1]])).toBe(undefined);
expect(nv.get(() => nv.value()[nv.value()[2]])).toBe(undefined);
expect(nv.get(() => nv.value()[nv.value()[3]])).toBe(undefined);
expect(nv.get(() => nv.value()[nv.value()[100]])).toBe(undefined);
expect(nv.get(() => nv.value()[symbol])).toBe(undefined);
expect(nv.get(() => nv.value()[nv.root()[symbol]])).toBe(undefined);
counter++;
return false;
},
string: () => {
expect(nv.get(() => nv.root()[string])).toBe(string);
expect(nv.get(() => nv.root()[25])).toBe('v25');
expect(nv.get(() => nv.root()['1'])).toBe('v1');
expect(nv.get(() => nv.root()['25'])).toBe('v25');
expect(nv.get(() => nv.root()[symbol])).toBe(symbol);
expect(nv.get(() => nv.root()[true])).toBe('true');
expect(nv.get(() => nv.root()[null])).toBe('null');
expect(nv.get(() => nv.root()[nv.value()])).toBe(string);
expect(nv.get(() => nv.root()[nv.root().n25])).toBe('v25');
expect(nv.get(() => nv.root()[nv.root().n1])).toBe('v1');
expect(nv.get(() => nv.root()[nv.root().s25])).toBe('v25');
expect(nv.get(() => nv.root()[nv.root()[symbol]])).toBe(symbol);
expect(nv.get(() => nv.root()[nv.root().tTrue])).toBe(undefined);
expect(nv.get(() => nv.root()[nv.root().sTrue])).toBe('true');
expect(nv.get(() => nv.root()[nv.root().tNull])).toBe(undefined);
expect(nv.get(() => nv.root()[nv.root().sNull])).toBe('null');
counter++;
return false;
},
[symbol]: () => false
});
expect(counter).toBe(2);
}
});
test('paths', () => {
perform(null, (nv, tree, path) => () => {
expect(nv.path()).toEqual(path);
return false;
}, false);
const E = {};
const BEFORE = 'path() called before validation';
const AFTER = 'path() called after validation';
const ERRBEFORE = 'errorPath() called before validation is completed';
const nv = nonvalid.instance();
expect(() => nv.path()).toThrow(BEFORE);
expect(() => nv.errorPath()).toThrow(ERRBEFORE);
const path = ['test', 0, Symbol('abc'), 1, '"\'`'];
const stringPaths = [
'json["test"]',
'json["test"][0]',
'json["test"][0][Symbol(abc)]',
'json["test"][0][Symbol(abc)][1]',
'json["test"][0][Symbol(abc)][1]["\\"\'`"]'
];
const error = nv({
[path[0]]: [{
[path[2]]: [0, {
[path[4]]: null
}]
}]
}, () => {
expect(nv.path()).toEqual([]);
expect(nv.path('json')).toEqual('json');
return nv({
[path[0]]: () => {
expect(nv.path()).toEqual(path.slice(0, 1));
expect(nv.path('json')).toEqual(stringPaths[0]);
return nv([() => {
expect(nv.path()).toEqual(path.slice(0, 2));
expect(nv.path('json')).toEqual(stringPaths[1]);
return nv({
[path[2]]: () => {
expect(nv.path()).toEqual(path.slice(0, 3));
expect(nv.path('json')).toEqual(stringPaths[2]);
return nv([0, () => {
expect(nv.path()).toEqual(path.slice(0, 4));
expect(nv.path('json')).toEqual(stringPaths[3]);
return nv({
[path[4]]: () => {
expect(nv.path()).toEqual(path.slice(0, 5));
expect(nv.path('json')).toEqual(stringPaths[4]);
expect(() => nv.errorPath()).toThrow(ERRBEFORE);
return E;
}
});
}]);
}
});
}
]);
}
});
});
expect(error).toEqual(E);
expect(nv.errorPath()).toEqual(path);
expect(nv.errorPath('json')).toBe(stringPaths[stringPaths.length - 1]);
expect(() => nv.path()).toThrow(AFTER);
});
});
describe('structure comparison', () => {
test('catch-other callback for objects', () => {
const E = 'false';
const AE = 5;
const N = null;
const NONFUNC = 'The catch-other callback must be a function';
const UNEXPECTED = 'Symbol(nonvalid.end) is not expected in an object schema';
const vars = [
['a', 'b', 'c', 'd'],
[Symbol('a'), Symbol('b'), Symbol('c'), 'd'],
['a', Symbol('b'), Symbol('c'), 'd'],
[Symbol('a'), 'b', 'c', 'd'],
[Symbol('a'), 'b', 'c', Symbol('d')],
];
for (const [a, b, c, d] of vars) {
perform({ [a]: a, [b]: b, [c]: c }, { [a]: a, [b]: b, [c]: c }, false);
perform({ [a]: a, [b]: b, [c]: c }, { [a]: a, [c]: c }, true);
perform({ [a]: a, [c]: c }, { [a]: a, [b]: b, [c]: c }, true, N, [b]);
perform({ [a]: a, [b]: b, [c]: c }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: (v, key) => key !== b, [nv.error]: AE }), false);
perform({ [a]: a, [b]: b, [c]: c }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: () => E }), E, N, [b]);
perform({ [a]: a, [b]: b, [c]: c, [d]: d }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: v => v !== b && E }), E, N, [d]);
perform({ [a]: a, [b]: b, [c]: c, [d]: d }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: v => v !== d && E, [nv.error]: AE }), E, N, [b]);
perform({ [a]: a, [b]: b, [c]: c, [d]: d }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: v => v !== b && v !== d && E }), false);
perform({ [a]: a, [b]: b, [c]: c }, nv => () =>
nv({ [a]: a, [c]: c, [b]: b, [nv.other]: () => E }), false);
perform({ [a]: a, [b]: b, [c]: c }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: E }), N, NONFUNC);
perform({ [a]: a, [c]: c }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: E }), N, NONFUNC);
perform({ [a]: a }, nv => () =>
nv({ [a]: a, [c]: c, [nv.other]: E }), N, NONFUNC);
}
perform({}, nv => () => nv({ [nv.end]: () => E }), null, UNEXPECTED);
});
test('comparing objects to non-objects', () => {
const E = Symbol();
const NONVALID = 'The shape error must be a non-function truthy value';
perform({}, {}, false);
perform([], {}, true);
perform(null, {}, true);
perform(() => {}, {}, true);
perform(undefined, {}, true);
perform(false, nv => () => nv({ [nv.error]: E }), E);
perform({}, nv => () => nv({ [nv.error]: E }), false);
perform(false, nv => () => nv({ [nv.error]: false }), null, NONVALID);
perform(false, nv => () => nv({ [nv.error]: null }), null, NONVALID);
perform({}, nv => () => nv({ [nv.error]: '' }), null, NONVALID);
perform({}, nv => () => nv({ [nv.error]: () => E }), null, NONVALID);
perform('[Object object]', nv => () => nv({ [nv.error]: () => E }), null, NONVALID);
});
test('arrays', () => {
const E = 'null';
const AE = 5;
const N = null;
const UNEXPECTED = name => `Symbol(nonvalid.${name}) is not expected in an array schema`;
const MANYEXTRA = 'Found more than 2 elements after the end-of-array marker';
const MANYENDS = 'Encountered multiple end-of-array markers';
const MANYFUNCS = 'Encountered multiple catch-other callbacks';
const MANYERRORS = 'Encountered multiple shape error values';
const FALSY = 'Shape error must be a truthy value';
for (const prefix of [[], ['a'], ['a', 'aa']]) {
const l = prefix.length;
perform([...prefix, 'b', 'c'], [...prefix, 'b', 'c'], false);
perform([...prefix, 'b', 'c'], [...prefix, 'b'], true);
perform([...prefix, 'b'], [...prefix, 'b', 'c'], true, N, [l + 1]);
perform([...prefix, 'b', 'c', 'd'],
[...prefix, , v => !['b', 'c', 'd'].includes(v) && E, AE], false);
perform([...prefix, 'b', 'c', 'd'], nv => () =>
nv([...prefix, nv.end, v => !['b', 'c', 'd'].includes(v) && E]), false);
perform([...prefix, 'b', 'c', 'd'],
[...prefix, , v => !['c', 'd'].includes(v) && E], E, N, [l]);
perform([...prefix, 'b', 'c', 'd'], nv => () =>
nv([...prefix, nv.end, AE, v => !['c', 'd'].includes(v) && E]), E, N, [l]);
perform([...prefix, 'b', 'c', 'd'],
[...prefix, , v => !['b', 'c'].includes(v) && E, AE], E, N, [l + 2]);
perform([...prefix, 'b', 'c', 'd'], nv => () =>
nv([...prefix, nv.end, v => !['b', 'c'].includes(v) && E]), E, N, [l + 2]);
perform([...prefix], [...prefix], false);
perform({}, [...prefix], true);
perform(null, [...prefix], true);
perform(() => {}, [...prefix], true);
perform(false, [...prefix], true);
perform(true, nv => () => nv([...prefix, nv.end]), true);
perform(Symbol(), nv => () => nv([...prefix, nv.end, () => false]), true);
perform(Symbol(), [...prefix, , () => []], true);
perform(undefined, nv => () => nv([...prefix, , E]), E);
perform({}, nv => () => nv([...prefix, nv.end, E]), E);
perform(AE, nv => () => nv([...prefix, nv.end, () => false, E]), E);
perform('', nv => () => nv([...prefix, , E, () => false]), E);
perform([...prefix], nv => () => nv([...prefix, nv.other]), N, UNEXPECTED('other'));
perform([...prefix, 1, 2], nv => () =>
nv([...prefix, nv.error, 2]), N, UNEXPECTED('error'));
perform([...prefix, 'b', 'c', 'd'],
[...prefix, , v => !['b', 'c', 'd'].includes(v) && E, AE, AE], N, MANYEXTRA);
perform([], nv => () => nv([...prefix, nv.end, AE, v => true, () => {}]), N, MANYEXTRA);
perform([...prefix, 'b', 'c', 'd'], nv => () =>
nv([...prefix, , v => !['b', 'c', 'd'].includes(v) && E, nv.end]), N, MANYENDS);
perform([], nv => () => nv([...prefix, nv.end, ,]), N, MANYENDS);
perform([...prefix], nv => () =>
nv([...prefix, nv.end, () => {}, () => {}]), N, MANYFUNCS);
perform([], [...prefix, , AE, AE], N, MANYERRORS);
perform([...prefix], nv => () => nv([...prefix, nv.end, false]), N, FALSY);
perform([], [...prefix, , () => {}, null], N, FALSY);
perform(null, nv => () => nv([...prefix, nv.end, '', () => {}]), N, FALSY);
}
});
});
describe('misc', () => {
test('always return false for falsy', () => {
expect(nonvalid.instance()(null, () => null)).toBe(false);
expect(nonvalid.instance()({}, () => {})).toBe(false);
expect(nonvalid.instance()({ abc: 123 }, { abc: () => '' })).toBe(false);
});
test('chaining of values', () => {
const check = (nv, v, key, actualV, actualKey) => {
expect(nv.value()).toBe(actualV);
expect(v).toBe(actualV);
expect(key).toBe(actualKey);
};
{
const nv = nonvalid.instance();
nv(123, (v, key) => {
check(nv, v, key, 123);
nv(v, (v, key) => {
check(nv, v, key, 123);
nv((v, key) => {
check(nv, v, key, 123);
});
check(nv, v, key, 123);
});
check(nv, v, key, 123);
});
}
{
const nv = nonvalid.instance();
nv({ abc: { def: 'ghi' } }, { abc: { def: (v, key) => {
check(nv, v, key, 'ghi', 'def');
nv((v, key) => {
check(nv, v, key, 'ghi', 'def');
nv(v, (v, key) => {
check(nv, v, key, 'ghi', 'def');
});
check(nv, v, key, 'ghi', 'def');
});
check(nv, v, key, 'ghi', 'def');
} } });
}
});
test('resetting error from above', () => {
const object = { a: { b: 'c' } };
{
const nv = nonvalid.instance();
const error = nv(object, () => nv({ a: () => nv({ b: 'd' }) }));
expect(error).toBe(true);
expect(nv.errorPath()).toEqual(['a', 'b']);
}
{
const nv = nonvalid.instance();
const error = nv(object, () => nv({ a: () => nv({ b: 'd' }) && false }));
expect(error).toBe(false);
expect(nv.errorPath()).toBe(null);
}
{
const nv = nonvalid.instance();
const error = nv(object, () => nv({ a: () => nv({ b: 'd' }) && false }) || true);
expect(error).toBe(true);
expect(nv.errorPath()).toEqual([]);
}
});
test('reusing instances and handling errors', () => {
const E = 'custom';
const ANOTHER = 'To validate another value, use nonvalid.instance()';
const NOCONTEXT = 'Validator called with no value outside of any context';
expect(nonvalid(123, 123)).toBe(false);
expect(() => nonvalid(123, 123)).toThrow(new Error(ANOTHER));
{
const nv = nonvalid.instance();
expect(nv(123, 124)).toBe(true);
expect(() => nv(123, 124)).toThrow(new Error(ANOTHER));
}
expect(nonvalid.instance()(123, 123)).toBe(false);
{
const nv = nonvalid.instance();
expect(() => nv(123, () => { throw new Error(E); })).toThrow(new Error(E));
expect(() => nv(123, 124)).toThrow(new Error(ANOTHER));
}
{
const nv = nonvalid.instance();
expect(() => nv({
value: 123
}, () => {
try { nv({ value: () => { throw new Error(E); } }); } catch (e) {}
return false;
})).toThrow(new Error(CANTPROCEED));
expect(() => nv(123, 124)).toThrow(new Error(ANOTHER));
}
expect(() => nonvalid.instance()(() => false)).toThrow(new Error(NOCONTEXT));