Skip to main content
Module

x/ok_computer/ok-computer.test.ts

λ "Functions all the way down" data validation for JavaScript and TypeScript.
Go to Latest
File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209
import { listErrors, isError, hasError, create, is, typeOf, instanceOf, number, boolean, bigint, string, symbol, fn, undef, nul, integer, finite, anyArray, or, xor, and, maxLength, minLength, length, nullish, min, max, includes, pattern, oneOf, not, INTROSPECT, array, tuple, all, object, OBJECT_ROOT, err, merge, when, match, andPeers, assert, nandPeers, orPeers, oxorPeers, xorPeers} from './ok-computer.ts';import { ANDError, NegateError, ORError, XORError, asStructure, isIStructure} from './errors.ts';
describe('listErrors', () => { describe('valid', () => { [ undefined, asStructure({ foo: undefined, bar: undefined, baz: asStructure({ qux: asStructure({}) }) }), asStructure([]) ].forEach((val) => { it(`${val} returns an empty array`, () => { expect(listErrors(val)).toEqual([]); }); }); });
describe('root errors', () => { [ // Primitives true, false, null, -1, 0, 1, 2, Infinity, NaN, '', ' ', 'a', 'ab', Symbol('foo'), // Objects {}, [], () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val, i) => { it(`${i + 1}. "${ val === null ? 'null' : val.toString() }" returns the error w/ path`, () => { expect(listErrors(val)).toEqual([{ path: '', err: val }]); }); }); });
describe('structural errors', () => { it('returns the error w/ path', () => { const err = new Error('Invalid'); const errors = asStructure({ foo: err }); expect(listErrors(errors)).toEqual([{ path: 'foo', err }]); });
it('returns multiple errors w/ path', () => { const err1 = new Error('Invalid'); const err2 = 'Invalid'; const err3 = false; const err4 = 0; const err5 = { is: 'invalid' }; const err6 = null; const err7 = -1; const errors = asStructure({ foo: asStructure({ bar: asStructure({ qux: err1, quux: err2, corge: err3, grault: asStructure({ garply: err4 }) }), quuz: err5 }), waldo: asStructure([err6, err7]) }); // Currently performs a depth-first search for errors, not sure if I care about order? expect(listErrors(errors)).toEqual([ { path: 'foo.bar.qux', err: err1 }, { path: 'foo.bar.quux', err: err2 }, { path: 'foo.bar.corge', err: err3 }, { path: 'foo.bar.grault.garply', err: err4 }, { path: 'foo.quuz', err: err5 }, { path: 'waldo.0', err: err6 }, { path: 'waldo.1', err: err7 } ]); }); });});
[ { name: 'isError', fn: isError }, { name: 'hasError', fn: hasError }].forEach(({ name, fn }) => { describe(name, () => { describe('valid', () => { [ undefined, asStructure({ foo: undefined, bar: undefined, baz: asStructure({ qux: asStructure({}) }) }), asStructure([]) ].forEach((val, i) => { it(`${i + 1}. ${val} returns false`, () => { expect(fn(val)).toBe(false); }); }); });
describe('invalid', () => { [ // Primitives true, false, null, -1, 0, 1, 2, Infinity, NaN, '', ' ', 'a', 'ab', Symbol('foo'), // Objects {}, [], () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09'), asStructure({ foo: 'bar' }), asStructure({ foo: undefined, bar: asStructure({ baz: 0 }) }), asStructure(['foo']) ].forEach((val, i) => { it(`${i + 1}. "${ val === null ? 'null' : val.toString() }" returns true`, () => { expect(fn(val)).toBe(true); }); }); }); });});
describe('create', () => { it('accepts a predicate and returns a validator', () => { const error = 'Expected typeof string'; const validator = create((value) => typeof value === 'string', error); expect(validator).toBeInstanceOf(Function); expect(validator('A string')).toBe(undefined); expect(validator(123)).toBe(error); expect(validator(undefined)).toBe(error); expect(validator(null)).toBe(error); const error2 = { id: 'invalid' }; const validator2 = create((value) => typeof value === 'number', error2); expect(validator2).toBeInstanceOf(Function); expect(validator2(123)).toBe(undefined); expect(validator2('A string')).toBe(error2); expect(validator2(undefined)).toBe(error2); expect(validator2(null)).toBe(error2); });
it('supports error introspection', () => { const error = 'Invalid'; const validator = create(() => true, error); expect(validator(INTROSPECT)).toBe(error); });});
describe('is', () => { test("is('foo')", () => { const validator = is('foo'); const err = 'Expected foo'; expect(validator('foo')).toBe(undefined); expect(validator('A string')).toBe(err); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test('is(123)', () => { const validator = is(123); const err = 'Expected 123'; expect(validator(123)).toBe(undefined); expect(validator(1)).toBe(err); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test("is({ foo: 'bar' })", () => { const obj = { foo: 'bar' }; const validator = is(obj); const err = 'Expected [object Object]'; expect(validator(obj)).toBe(undefined); expect(validator({ foo: 'bar' })).toBe(err); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test('introspection', () => { const validator = is(INTROSPECT); const err = 'Expected Symbol(ok-computer.introspect)'; expect(validator(INTROSPECT)).toBe(err); });});
describe('typeOf', () => { test("typeOf('string')", () => { const validator = typeOf('string'); const err = 'Expected typeof string'; expect(validator('A string')).toBe(undefined); expect(validator(123)).toBe(err); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test("typeOf('number')", () => { const validator = typeOf('number'); const err = 'Expected typeof number'; expect(validator('A string')).toBe(err); expect(validator(123)).toBe(undefined); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test('introspection', () => { const validator = typeOf('number'); const err = 'Expected typeof number'; expect(validator(INTROSPECT)).toBe(err); });});
describe('instanceOf', () => { test(`instanceOf(Date)`, () => { const validator = instanceOf(Date); const err = 'Expected instanceof Date'; expect(validator(new Date('2020-01-01'))).toBe(undefined); expect(validator({ foo: 'bar' })).toBe(err); expect(validator(123)).toBe(err); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test(`instanceOf(Ctor)`, () => { class Ctor {} const validator = instanceOf(Ctor); const err = 'Expected instanceof Ctor'; expect(validator(new Ctor())).toBe(undefined); expect(validator(123)).toBe(err); expect(validator(undefined)).toBe(err); expect(validator(null)).toBe(err); });
test('introspection', () => { const validator = instanceOf(function Foo() {}); const err = 'Expected instanceof Foo'; expect(validator(INTROSPECT)).toBe(err); });});
describe('number', () => { it('checks whether value is a number', () => { const err = 'Expected typeof number'; expect(number(123)).toBe(undefined); expect(number('A string')).toBe(err); expect(number(undefined)).toBe(err); expect(number(null)).toBe(err); });});
describe('boolean', () => { it('checks whether value is a boolean', () => { const err = 'Expected typeof boolean'; expect(boolean(true)).toBe(undefined); expect(boolean(false)).toBe(undefined); expect(boolean('A String')).toBe(err); expect(boolean(undefined)).toBe(err); expect(boolean(null)).toBe(err); });});
describe('bigint', () => { it('checks whether value is a bigint', () => { const err = 'Expected typeof bigint'; expect(bigint(0n)).toBe(undefined); expect(bigint(BigInt(100))).toBe(undefined); expect(bigint('A String')).toBe(err); expect(bigint(100)).toBe(err); expect(bigint(undefined)).toBe(err); expect(bigint(null)).toBe(err); });});
describe('string', () => { it('checks whether value is a string', () => { const err = 'Expected typeof string'; expect(string('A string')).toBe(undefined); expect(string(123)).toBe(err); expect(string(undefined)).toBe(err); expect(string(null)).toBe(err); });});
describe('symbol', () => { it('checks whether value is a symbol', () => { const err = 'Expected typeof symbol'; expect(symbol(Symbol('foo'))).toBe(undefined); expect(symbol('A string')).toBe(err); expect(symbol(123)).toBe(err); expect(symbol(undefined)).toBe(err); expect(symbol(null)).toBe(err); });});
describe('fn', () => { it('checks whether value is a function', () => { const err = 'Expected typeof function'; expect(fn(function foo() {})).toBe(undefined); expect(fn(() => {})).toBe(undefined); expect(fn(class Ctor {})).toBe(undefined); expect(fn('A string')).toBe(err); expect(fn(123)).toBe(err); expect(fn(undefined)).toBe(err); expect(fn(null)).toBe(err); });});
describe('undef', () => { it('checks whether value is undefined', () => { const err = 'Expected typeof undefined'; expect(undef(undefined)).toBe(undefined); expect(undef(null)).toBe(err); expect(undef(0)).toBe(err); expect(undef(false)).toBe(err); expect(undef('A string')).toBe(err); });});
describe('nul', () => { it('checks whether value is null', () => { const err = 'Expected null'; expect(nul(null)).toBe(undefined); expect(nul(undefined)).toBe(err); expect(nul(0)).toBe(err); expect(nul(false)).toBe(err); expect(nul('A string')).toBe(err); });});
describe('integer', () => { it('checks whether value is an integer', () => { const err = 'Expected integer'; expect(integer(100)).toBe(undefined); expect(integer(0)).toBe(undefined); expect(integer(-100)).toBe(undefined); expect(integer(0.5)).toBe(err); expect(integer(1.00001)).toBe(err); expect(integer(-100.1)).toBe(err); expect(integer(false)).toBe(err); expect(integer('A string')).toBe(err); expect(integer(undefined)).toBe(err); expect(integer(null)).toBe(err); });});
describe('finite', () => { it('checks whether value is finite', () => { const err = 'Expected finite number'; expect(finite(100)).toBe(undefined); expect(finite(0)).toBe(undefined); expect(finite(-100)).toBe(undefined); expect(finite(0.5)).toBe(undefined); expect(finite(Infinity)).toBe(err); expect(finite(-Infinity)).toBe(err); expect(finite(false)).toBe(err); expect(finite('A string')).toBe(err); expect(finite(undefined)).toBe(err); expect(finite(null)).toBe(err); });});
describe('anyArray', () => { it('checks whether value is an array', () => { const err = 'Expected array'; expect(anyArray([])).toBe(undefined); expect(anyArray([true, 'A string', 0, false, {}])).toBe(undefined); expect(anyArray({})).toBe(err); expect(anyArray({ length: 10 })).toBe(err); expect(anyArray('A string')).toBe(err); expect(anyArray(123)).toBe(err); expect(anyArray(undefined)).toBe(err); expect(anyArray(null)).toBe(err); });});
describe('or', () => { describe('single clause', () => { it('passes if value matches at least one validator', () => { const err = new ORError(['Expected typeof string']); const validator = or(string); expect(validator('A string')).toBe(undefined); expect(validator(123)).toEqual(err); expect(validator(true)).toEqual(err); expect(validator(undefined)).toEqual(err); expect(validator(null)).toEqual(err); }); });
describe('multiple clauses', () => { it('passes if value matches at least one validator', () => { const err = new ORError([ 'Expected typeof string', 'Expected typeof number', 'Expected typeof boolean' ]); const validator = or(string, number, boolean); expect(validator('A string')).toBe(undefined); expect(validator(123)).toBe(undefined); expect(validator(false)).toBe(undefined); expect(validator([])).toEqual(err); expect(validator(undefined)).toEqual(err); expect(validator(null)).toEqual(err); }); });
describe('introspection', () => { it('returns the error', () => { const validator = or( create(() => true), () => 'Expected something else' ); const err = validator(INTROSPECT); expect(err).toEqual(new ORError(['Invalid', 'Expected something else'])); }); });
describe('short-circuit evaluation', () => { it('does not unnecessarily call validators', () => { const validator1 = or( create(() => true, 'Invalid'), (value) => { if (value === INTROSPECT) { // Introspection is allowed return 'Invalid'; } throw new Error('Should not get here'); } ); expect(() => validator1('123')).not.toThrow(); }); });});
describe('xor', () => { describe('single clause', () => { it('passes if value matches exactly one validator', () => { const err = new XORError(['Expected typeof string']); const validator = xor(string); expect(validator('A string')).toBe(undefined); expect(validator(123)).toEqual(err); expect(validator(true)).toEqual(err); expect(validator(undefined)).toEqual(err); expect(validator(null)).toEqual(err); }); });
describe('multiple clauses', () => { it('passes if value matches exactly one validator', () => { const err = new XORError([ 'Expected to include foo', 'Expected to include bar', 'Expected to include baz' ]); const validator = xor(includes('foo'), includes('bar'), includes('baz')); expect(validator('foo')).toBe(undefined); expect(validator('bar')).toBe(undefined); expect(validator('baz')).toBe(undefined); expect(validator('foo blah')).toBe(undefined); expect(validator('fou bar blaz')).toBe(undefined); expect(validator('blah baz')).toBe(undefined); expect(validator('foo bar')).toEqual(err); expect(validator('foo baz')).toEqual(err); expect(validator('bar baz')).toEqual(err); expect(validator('foo bar baz')).toEqual(err); expect(validator('barb food')).toEqual(err); expect(validator(false)).toEqual(err); expect(validator([])).toEqual(err); expect(validator(undefined)).toEqual(err); expect(validator(null)).toEqual(err); }); });
describe('introspection', () => { it('returns an error', () => { const validator = xor( create(() => true), () => 'Expected something else' ); const err = validator(INTROSPECT); expect(err).toEqual(new XORError(['Invalid', 'Expected something else'])); }); });
describe('short-circuit evaluation', () => { it('does not unnecessarily call validators', () => { const validator1 = xor( create(() => true, 'Invalid'), create(() => true, 'Invalid'), (value) => { if (value === INTROSPECT) { // Introspection is allowed return 'Invalid'; } throw new Error('Should not get here'); } ); expect(() => validator1('123')).not.toThrow(); }); });});
describe('and', () => { describe('single clause', () => { it('passes if all values pass', () => { const err = new ANDError(['Expected typeof string']); const validator = and(string); expect(validator('A string')).toBe(undefined); expect(validator('A')).toBe(undefined); expect(validator([1, 2, 3, 4])).toEqual(err); expect(validator(true)).toEqual(err); expect(validator(undefined)).toEqual(err); expect(validator(null)).toEqual(err); }); });
describe('multiple clauses', () => { it('passes if value matches at least one validator', () => { const err = new ANDError([ 'Expected typeof string', 'Expected min length 2', 'Expected max length 8' ]); const validator = and(string, minLength(2), maxLength(8)); expect(validator('A string')).toBe(undefined); expect(validator('A ')).toBe(undefined); expect(validator(' A')).toBe(undefined); // NOTE: Alternatively we could return just the validators // which failed here, e.g. // new ANDError([ // 'Expected min length 2', // 'Expected max length 8' // ]); expect(validator('A')).toEqual(err); // And // new ANDError([ // 'Expected max length 8' // ]); expect(validator('A longer string')).toEqual(err); expect(validator([1, 2])).toEqual(err); expect(validator([1, 2, 3, 4])).toEqual(err); expect(validator(true)).toEqual(err); expect(validator(undefined)).toEqual(err); expect(validator(null)).toEqual(err); }); });
describe('introspection', () => { it('returns an error', () => { const validator = and( create(() => true), () => 'Expected something else' ); const err = validator(INTROSPECT); expect(err).toEqual(new ANDError(['Invalid', 'Expected something else'])); }); });
describe('short-circuit evaluation', () => { it('does not unnecessarily call validators', () => { const validator1 = and( create(() => false, 'Invalid'), (value) => { if (value === INTROSPECT) { // Introspection is allowed return 'Invalid'; } throw new Error('Should not get here'); } ); expect(() => validator1('123')).not.toThrow(); }); });});
describe('maxLength(3)', () => { describe('valid', () => { [ '', ' ', ' ', 'a', 'ab', 'abc', [], ['a'], ['a', 'b'], ['a', 'b', 'c'], [0, new Date(), {}] ].forEach((val) => { it(`${val}`, () => { const validator = maxLength(3); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ ' ', ['An', 'arr', 'ay', 'Asd'], true, false, null, -1, 0, 1, 2, Infinity, NaN, // Symbol("foo"), {}, () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected max length 3'; const validator = maxLength(3); expect(validator(val)).toBe(err); }); }); });});
describe('minLength(3)', () => { describe('valid', () => { [' ', 'A string', ['An', 'arr', 'ay'], ['An', 'arr', 'ay', 'ok']].forEach( (val) => { it(`${val}`, () => { const validator = minLength(3); expect(validator(val)).toBe(undefined); }); } ); });
describe('invalid', () => { [ 'A', [], ['An', 'ay'], '', ' ', 'a', 'ab', true, false, null, -1, 0, 1, 2, Infinity, NaN, // Symbol("foo"), {}, () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected min length 3'; const validator = minLength(3); expect(validator(val)).toBe(err); }); }); });});
describe('length(3)', () => { describe('valid', () => { [' ', 'abc', ['a', 'b', 'c'], [0, new Date(), {}]].forEach((val) => { it(`${val}`, () => { const validator = length(3); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ '', ' ', ' ', ' ', [], ['An'], ['An', 'arr'], ['An', 'arr', 'ay', 'Asd'], true, false, null, -1, 0, 1, 2, Infinity, NaN, // Symbol("foo"), {}, () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected length 3'; const validator = length(3); expect(validator(val)).toBe(err); }); }); });});
describe('length(3, 5)', () => { describe('valid', () => { [ ' ', ' ', ' ', 'abc', 'abcd', 'abcde', ['a', 'b', 'c'], ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd', 'e'], [0, new Date(), {}], [0, new Date(), {}, false], [0, new Date(), {}, false, 'a'] ].forEach((val) => { it(`${val}`, () => { const validator = length(3, 5); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ '', ' ', ' ', [], ['An'], ['An', 'arr'], ['a', 'b', 'c', 'd', 'e', 'f'], true, false, null, -1, 0, 1, 2, Infinity, NaN, // Symbol("foo"), {}, () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected length between 3 and 5'; const validator = length(3, 5); expect(validator(val)).toBe(err); }); }); });});
describe('nullish', () => { it('checks whether value is null or undefined', () => { const err = 'Expected nullish'; const validator = nullish; expect(validator(undefined)).toBe(undefined); expect(validator(null)).toBe(undefined); expect(validator(0)).toBe(err); expect(validator(false)).toBe(err); expect(validator('A string')).toBe(err); });});
describe('min(3)', () => { describe('valid', () => { [3, 4, 5, Infinity].forEach((val) => { it(`${val}`, () => { const validator = min(3); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ -1, 0, 1, 2, NaN, true, false, null, // Symbol("foo"), {}, [], () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected min 3'; const validator = min(3); expect(validator(val)).toBe(err); }); }); });});
describe('max(3)', () => { describe('valid', () => { [-1, 0, 1, 2, 3].forEach((val) => { it(`${val}`, () => { const validator = max(3); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ 4, 5, Infinity, NaN, true, false, null, // Symbol("foo"), {}, [], () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected max 3'; const validator = max(3); expect(validator(val)).toBe(err); }); }); });});
describe("includes('a')", () => { describe('valid', () => { ['a', 'abc', 'bac', ['a'], ['a', 'b', 'c'], ['b', 'a', 'c']].forEach( (val) => { it(`${val}`, () => { const validator = includes('a'); expect(validator(val)).toBe(undefined); }); } ); });
describe('invalid', () => { [ '', [], 'b', 'Abc', 0, 1, Infinity, NaN, true, false, null, // Symbol("foo"), {}, () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected to include a'; const validator = includes('a'); expect(validator(val)).toBe(err); }); }); });});
describe('pattern(/[A-Z]/)', () => { describe('valid', () => { ['A', 'Abc', 'aBc'].forEach((val) => { it(`${val}`, () => { const validator = pattern(/[A-Z]/); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ 'a', 'abc', 0, 1, Infinity, NaN, true, false, null, // Symbol("foo"), {}, [], () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected to match pattern /[A-Z]/'; const validator = pattern(/[A-Z]/); expect(validator(val)).toBe(err); }); }); });});
describe("oneOf(0, 1, '2', false)", () => { describe('valid', () => { [0, 1, '2', false].forEach((val) => { it(`${val}`, () => { const validator = oneOf(0, 1, '2', false); expect(validator(val)).toBe(undefined); }); }); });
describe('invalid', () => { [ 'a', 'abc', 2, Infinity, NaN, true, null, // Symbol("foo"), {}, [], () => {}, /foo/, new Set(), new Map(), new Error(), new Date('2021-01-09') ].forEach((val) => { it(`${val}`, () => { const err = 'Expected one of 0, 1, 2, false'; const validator = oneOf(0, 1, '2', false); expect(validator(val)).toBe(err); }); }); });});
describe('not', () => { it('inverses a validator', () => { const err = new NegateError('Expected typeof string'); const validator = not(string); expect(validator('foo')).toEqual(err); expect(validator(1)).toBe(undefined); expect(validator(undefined)).toBe(undefined); expect(validator(null)).toBe(undefined);
const err2 = new NegateError('Expected typeof number'); const validator2 = not(number); expect(validator2(1)).toEqual(err2); expect(validator2('foo')).toBe(undefined); expect(validator2(undefined)).toBe(undefined); expect(validator2(null)).toBe(undefined); });});
describe('array', () => { it('returns an array of errors', () => { const validator = array(string); expect(validator(true)).toEqual(asStructure(['Expected array'])); expect(validator(false)).toEqual(asStructure(['Expected array'])); expect(validator('A String')).toEqual(asStructure(['Expected array'])); expect(validator(undefined)).toEqual(asStructure(['Expected array'])); expect(validator(null)).toEqual(asStructure(['Expected array'])); expect(validator([true])).toEqual(asStructure(['Expected typeof string'])); expect(validator([false])).toEqual(asStructure(['Expected typeof string'])); expect(validator(['A String'])).toEqual(asStructure([undefined])); expect(validator(['A String', 'another string'])).toEqual( asStructure([undefined, undefined]) ); expect(validator([undefined])).toEqual( asStructure(['Expected typeof string']) ); expect(validator([null])).toEqual(asStructure(['Expected typeof string'])); expect(validator([true, false, 'A String', undefined, null])).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof string', undefined, 'Expected typeof string', 'Expected typeof string' ]) ); expect(validator(INTROSPECT)).toEqual( asStructure(['Expected typeof string']) ); });});
describe('tuple', () => { it('returns an array of errors', () => { const validator = tuple(string, boolean, number); expect(validator(['A string', true, 123])).toEqual( asStructure([undefined, undefined, undefined]) ); expect(validator(undefined)).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator('A string')).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator(['A string', true, 123, 'Another string'])).toEqual( asStructure([undefined, undefined, undefined, 'Extraneous element']) ); expect(validator([234, true, 123, 'Another string'])).toEqual( asStructure([ 'Expected typeof string', undefined, undefined, 'Extraneous element' ]) ); expect(validator([234, 'true', '123', 'Another string'])).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number', 'Extraneous element' ]) ); expect(validator(['A string', true])).toEqual( asStructure([undefined, undefined, 'Expected typeof number']) ); expect(validator([true, true])).toEqual( asStructure([ 'Expected typeof string', undefined, 'Expected typeof number' ]) ); expect(validator([234, 'true'])).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator([234, 'true', false])).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator([])).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator([123])).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator(['123'])).toEqual( asStructure([ undefined, 'Expected typeof boolean', 'Expected typeof number' ]) ); expect(validator(INTROSPECT)).toEqual( asStructure([ 'Expected typeof string', 'Expected typeof boolean', 'Expected typeof number' ]) ); const validator2 = tuple(string, nullish); expect(validator2(['A string', null])).toEqual( asStructure([undefined, undefined]) ); expect(validator2(['A string', undefined])).toEqual( asStructure([undefined, undefined]) ); expect(validator2(['A string'])).toEqual( asStructure([undefined, 'Expected nullish']) ); expect(validator2([123])).toEqual( asStructure(['Expected typeof string', 'Expected nullish']) ); });});
describe('all', () => { it('behaves like `and` but only returns errors for validators have failed', () => { const validator = all( string, pattern(/[A-Z]/), includes('_'), minLength(8), maxLength(10) ); expect(validator('A_foo_78')).toEqual(undefined); expect(validator('a_foo_78')).toEqual( new ANDError(['Expected to match pattern /[A-Z]/']) ); expect(validator('a-foo-78')).toEqual( new ANDError([ 'Expected to match pattern /[A-Z]/', 'Expected to include _' ]) ); expect(validator('a-foo-7')).toEqual( new ANDError([ 'Expected to match pattern /[A-Z]/', 'Expected to include _', 'Expected min length 8' ]) ); expect(validator('a-foo-78901')).toEqual( new ANDError([ 'Expected to match pattern /[A-Z]/', 'Expected to include _', 'Expected max length 10' ]) ); expect(validator(1)).toEqual( new ANDError([ 'Expected typeof string', 'Expected to match pattern /[A-Z]/', 'Expected to include _', 'Expected min length 8', 'Expected max length 10' ]) ); });
it("and doesn't short-circuit evaluation", () => { const validator1 = all( create(() => false, 'First error'), create(() => true, 'Second error'), create(() => false, 'Third error') ); expect(validator1('123')).toEqual( new ANDError(['First error', 'Third error']) ); });
describe('introspection', () => { it('returns an error', () => { const validator = all( create(() => true), () => 'Expected something else' ); const err = validator(INTROSPECT); expect(err).toEqual(new ANDError(['Invalid', 'Expected something else'])); }); });});
describe('object', () => { it('returns a root error if value is not a plain object', () => { const validator = object({}); expect(validator(undefined)).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(null)).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(true)).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(false)).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(0)).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(BigInt(0))).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(() => {})).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator(new (class Foo {})())).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); function Bar() {} expect( validator( // @ts-ignore new Bar() ) ).toEqual(asStructure({ [OBJECT_ROOT]: 'Expected object' })); expect(validator(new Date())).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object' }) ); expect(validator({})).toEqual({}); expect(validator(new Object())).toEqual({}); expect(validator(Object.create(null))).toEqual({}); });
describe('object({ ...validators })', () => { it('checks value is an object matching validators', () => { const validator = object({ firstName: and(string, minLength(1), maxLength(255)), middleName: or(nullish, and(string, minLength(1), maxLength(255))), lastName: and(string, minLength(1), maxLength(255)), address: object({ line1: string, line2: string, city: string, state: or(nullish, string), zip: string, country: string, [Symbol.for('test')]: object({ [Symbol.for('test2')]: string }) }), favouriteColors: and(array(string), maxLength(3)) }); const validUser = { firstName: 'Lewis', lastName: 'Hamilton', address: { line1: '123 Fake street', line2: 'Somehwere?', city: 'Nowhere', state: 'EW', zip: '10001', country: 'GB', [Symbol.for('test')]: { [Symbol.for('test2')]: 'foo' } }, favouriteColors: ['Blue', 'Red'] }; expect(validator(validUser)).toMatchInlineSnapshot(` Object { "address": Object { "city": undefined, "country": undefined, "line1": undefined, "line2": undefined, "state": undefined, "zip": undefined, Symbol(test): Object { Symbol(test2): undefined, }, }, "favouriteColors": undefined, "firstName": undefined, "lastName": undefined, "middleName": undefined, } `); expect(isError(validator(validUser))).toBe(false); expect(hasError(validator(validUser))).toBe(false); const invalidUser = { firstName: 'Richard', middleName: null, lastName: 123, address: { line1: 'asd', line2: 'asd' }, favouriteColors: ['1', '2', '3', '4'] }; const errors = validator(invalidUser); expect(hasError(errors)).toBe(true); expect(isError(errors)).toBe(true);
expect(errors).toMatchInlineSnapshot(` Object { "address": Object { "city": "Expected typeof string", "country": "Expected typeof string", "line1": undefined, "line2": undefined, "state": undefined, "zip": "Expected typeof string", Symbol(test): Object { Symbol(ok-computer.object-root): "Expected object", Symbol(test2): "Expected typeof string", }, }, "favouriteColors": Object { "errors": Array [ Array [ "Expected typeof string", ], "Expected max length 3", ], "operator": "AND", "type": "ANDError", }, "firstName": undefined, "lastName": "(Expected typeof string and expected min length 1 and expected max length 255)", "middleName": undefined, } `); expect(listErrors(errors)).toMatchInlineSnapshot(` Array [ Object { "err": "(Expected typeof string and expected min length 1 and expected max length 255)", "path": "lastName", }, Object { "err": "Expected typeof string", "path": "address.city", }, Object { "err": "Expected typeof string", "path": "address.zip", }, Object { "err": "Expected typeof string", "path": "address.country", }, Object { "err": "Expected object", "path": "address.Symbol(test).Symbol(ok-computer.object-root)", }, Object { "err": "Expected typeof string", "path": "address.Symbol(test).Symbol(test2)", }, Object { "err": Object { "errors": Array [ Array [ "Expected typeof string", ], "Expected max length 3", ], "operator": "AND", "type": "ANDError", }, "path": "favouriteColors", }, ] `); });
it('returns a root error if unknown properties are found', () => { const validator = object({ firstName: and(string, minLength(1), maxLength(255)), middleName: or(nullish, and(string, minLength(1), maxLength(255))), lastName: and(string, minLength(1), maxLength(255)) }); const valid = validator({ firstName: 'Lewis', lastName: 'Hamilton' }); expect(valid).toMatchInlineSnapshot(` Object { "firstName": undefined, "lastName": undefined, "middleName": undefined, } `); expect(isError(valid)).toBe(false); const invalid = validator({ firstName: 'Lewis', lastName: 'Hamilton', unknownProp1: 'property', unknownProp2: ['prop'] }); expect(invalid).toMatchInlineSnapshot(` Object { "firstName": undefined, "lastName": undefined, "middleName": undefined, Symbol(ok-computer.object-root): "Unknown properties \\"unknownProp1\\", \\"unknownProp2\\"", } `); expect(isError(invalid)).toBe(true); });
it('allows call sites to opt-out of unknown property check', () => { const validator = object( { firstName: and(string, minLength(1), maxLength(255)), middleName: or(nullish, and(string, minLength(1), maxLength(255))), lastName: and(string, minLength(1), maxLength(255)) }, { allowUnknown: true } ); const valid = validator({ firstName: 'Lewis', lastName: 'Hamilton', unknownProp1: 'property' }); expect(valid).toMatchInlineSnapshot(` Object { "firstName": undefined, "lastName": undefined, "middleName": undefined, } `); expect(isError(valid)).toBe(false); }); });
it('supports introspection', () => { const validator = object({ firstName: string, lastName: create(() => true), picture: object({ url: err(or(nullish, string), 'Expected nullish or string') }) }); expect(validator(INTROSPECT)).toEqual( asStructure({ [OBJECT_ROOT]: 'Expected object', firstName: 'Expected typeof string', lastName: 'Invalid', picture: { [OBJECT_ROOT]: 'Expected object', url: 'Expected nullish or string' } }) ); });});
describe('merge', () => { it('merges object validators', () => { // NOTE: `merge` currently only works when passing `allowUnknown: true` // into the input validators 😔 const validator1 = object({ firstName: string }, { allowUnknown: true }); const validator2 = object( { lastName: or(nullish, string), age: number }, { allowUnknown: true } ); const validator3 = object({ age: integer }, { allowUnknown: true }); const validator4 = merge(validator1, validator2, validator3); expect(validator4('foo')).toEqual( asStructure({ age: 'Expected integer', firstName: 'Expected typeof string', lastName: undefined, [OBJECT_ROOT]: 'Expected object' }) ); expect( validator4({ firstName: 'A string', age: 10 }) ).toEqual( asStructure({ age: undefined, firstName: undefined, lastName: undefined }) ); const err = validator4({}); expect(isIStructure(err)).toBe(true); });});
describe('when', () => { it('executes the validator only when the predicate passes', () => { const validator1 = when(() => true)(string); const validator2 = when(() => false)(string); expect(validator1('123')).toBe(undefined); expect(validator2('123')).toBe(undefined); expect(validator1(123)).toBe('Expected typeof string'); expect(validator2(123)).toBe(undefined); });
it('supports introspection', () => { const validator1 = when(() => true)(string); const validator2 = when(() => false)(string); expect(validator1(INTROSPECT)).toBe('Expected typeof string'); expect(validator2(INTROSPECT)).toBe('Expected typeof string'); });});
describe('match', () => { it('checks value matches sibling', () => { const validator = match('password'); expect(validator(undefined)).toBe('Expected to match password'); expect(validator(null)).toBe('Expected to match password'); expect(validator('password123')).toBe('Expected to match password'); expect(validator('password123', {})).toBe('Expected to match password'); expect(validator('password123', { foo: 'password123' })).toBe( 'Expected to match password' ); expect(validator('password123', { password: 'bar' })).toBe( 'Expected to match password' ); expect(validator('password123', { password: 'password123' })).toBe( undefined ); });});
describe('andPeer', () => { const validator = object({ A: and(or(nullish, string), andPeers('B')), B: and(or(nullish, string), andPeers('A')) });
[ { description: 'Both', input: { A: '1', B: '2' }, valid: true }, { description: 'Neither', input: {}, valid: true }, { description: 'A only', input: { A: '1' }, valid: false }, { description: 'B only', input: { B: '2' }, valid: false }, { description: 'invalid A and invalid B', input: { A: Infinity, B: false }, valid: false }, { description: 'invalid A', input: { A: /123/, B: '123' }, valid: false }, { description: 'invalid B', input: { A: '123', B: 123 }, valid: false }, { description: 'invalid A only', input: { A: [] }, valid: false }, { description: 'invalid B only', input: { B: {} }, valid: false } ].forEach((t) => { test(t.description, () => { const errors = validator(t.input); if (t.valid) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });
const validator2 = object({ A: and(or(nullish, string), andPeers('B', 'C')), B: and(or(nullish, string), andPeers('A', 'C')), C: and(or(nullish, string), andPeers('A', 'B')) });
[ [0, 0, 0, 1], // This would be false in an `and` truth table, but we want all-or-nothing (there's no point in a pure 'and' peer fn; just make them all required) [0, 0, 1, 0], [0, 1, 0, 0], [0, 1, 1, 0], [1, 0, 0, 0], [1, 0, 1, 0], [1, 1, 0, 0], [1, 1, 1, 1] ].forEach(([A, B, C, Q]) => { test(`${A} ${B} ${C} ${Q}`, () => { const toVal = (v: unknown) => (v ? 'valid value' : undefined); const errors = validator2({ A: toVal(A), B: toVal(B), C: toVal(C) }); if (Q) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });});
describe('nandPeer', () => { const validator = object({ A: and(or(nullish, string), nandPeers('B')), B: and(or(nullish, string), nandPeers('A')) });
[ { description: 'Both', input: { A: '1', B: '2' }, valid: false }, { description: 'Neither', input: {}, valid: true }, { description: 'A only', input: { A: '1' }, valid: true }, { description: 'B only', input: { B: '2' }, valid: true }, { description: 'invalid A and invalid B', input: { A: new Date(), B: false }, valid: false }, { description: 'invalid A', input: { A: new Date(), B: '123' }, valid: false }, { description: 'invalid B', input: { A: '123', B: new Date() }, valid: false }, { description: 'invalid A only', input: { A: [] }, valid: false }, { description: 'invalid B only', input: { B: {} }, valid: false } ].forEach((t) => { test(t.description, () => { const errors = validator(t.input); if (t.valid) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });
const validator2 = object({ A: and(or(nullish, string), nandPeers('B', 'C')), B: and(or(nullish, string), nandPeers('A', 'C')), C: and(or(nullish, string), nandPeers('A', 'B')) });
// https://www.electronics-tutorials.ws/logic/logic_5.html [ [0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 0, 1], [0, 1, 1, 1], [1, 0, 0, 1], [1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 0] ].forEach(([A, B, C, Q]) => { test(`${A} ${B} ${C} ${Q}`, () => { const toVal = (v: unknown) => (v ? 'valid value' : undefined); const errors = validator2({ A: toVal(A), B: toVal(B), C: toVal(C) }); if (Q) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });});
describe('orPeer', () => { const validator = object({ A: and(or(nullish, string), orPeers('B')), B: and(or(nullish, string), orPeers('A')) });
[ { description: 'Both', input: { A: '1', B: '2' }, valid: true }, { description: 'Neither', input: {}, valid: false }, { description: 'A only', input: { A: '1' }, valid: true }, { description: 'B only', input: { B: '2' }, valid: true }, { description: 'invalid A and invalid B', input: { A: new Date(), B: false }, valid: false }, { description: 'invalid A', input: { A: new Date(), B: '123' }, valid: false }, { description: 'invalid B', input: { A: '123', B: new Date() }, valid: false }, { description: 'invalid A only', input: { A: [] }, valid: false }, { description: 'invalid B only', input: { B: {} }, valid: false } ].forEach((t) => { test(t.description, () => { const errors = validator(t.input); if (t.valid) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });
const validator2 = object({ A: and(or(nullish, string), orPeers('B', 'C')), B: and(or(nullish, string), orPeers('A', 'C')), C: and(or(nullish, string), orPeers('A', 'B')) });
// https://www.electronics-tutorials.ws/logic/logic_3.html [ [0, 0, 0, 0], [0, 0, 1, 1], [0, 1, 0, 1], [0, 1, 1, 1], [1, 0, 0, 1], [1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 1] ].forEach(([A, B, C, Q]) => { test(`${A} ${B} ${C} ${Q}`, () => { const toVal = (v: unknown) => (v ? 'valid value' : undefined); const input = { A: toVal(A), B: toVal(B), C: toVal(C) }; const errors = validator2(input); if (Q) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });});
describe('xorPeer', () => { const validator = object({ A: and(or(nullish, string), xorPeers('B')), B: and(or(nullish, string), xorPeers('A')) });
[ { description: 'Both', input: { A: '1', B: '2' }, valid: false }, { description: 'Neither', input: {}, valid: false }, { description: 'A only', input: { A: '1' }, valid: true }, { description: 'B only', input: { B: '2' }, valid: true }, { description: 'invalid A and invalid B', input: { A: new Date(), B: false }, valid: false }, { description: 'invalid A', input: { A: new Date(), B: '123' }, valid: false }, { description: 'invalid B', input: { A: '123', B: new Date() }, valid: false }, { description: 'invalid A only', input: { A: [] }, valid: false }, { description: 'invalid B only', input: { B: {} }, valid: false } ].forEach((t) => { test(t.description, () => { const errors = validator(t.input); if (t.valid) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });
const validator2 = object({ A: and(or(nullish, string), xorPeers('B', 'C')), B: and(or(nullish, string), xorPeers('A', 'C')), C: and(or(nullish, string), xorPeers('A', 'B')) });
// https://www.electronics-tutorials.ws/logic/logic_7.html [ [0, 0, 0, 0], [0, 0, 1, 1], [0, 1, 0, 1], [0, 1, 1, 0], [1, 0, 0, 1], [1, 0, 1, 0], [1, 1, 0, 0], [1, 1, 1, 0] // This would be true in an `xor` truth table 🤷‍♂️, but don't think it's what we want? ].forEach(([A, B, C, Q]) => { test(`${A} ${B} ${C} ${Q}`, () => { const toVal = (v: unknown) => (v ? 'valid value' : undefined); const input = { A: toVal(A), B: toVal(B), C: toVal(C) }; const errors = validator2(input); if (Q) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });});
describe('oxorPeer', () => { const validator = object({ A: and(or(nullish, string), oxorPeers('B')), B: and(or(nullish, string), oxorPeers('A')) });
[ { description: 'Both', input: { A: '1', B: '2' }, valid: false }, { description: 'Neither', input: {}, valid: true }, { description: 'A only', input: { A: '1' }, valid: true }, { description: 'B only', input: { B: '2' }, valid: true }, { description: 'invalid A and invalid B', input: { A: new Date(), B: false }, valid: false }, // TODO: These test cases are missing invalid A and invalid B (w/o the other) { description: 'invalid A', input: { A: new Date(), B: '123' }, valid: false }, { description: 'invalid B', input: { A: '123', B: new Date() }, valid: false }, { description: 'invalid A only', input: { A: [] }, valid: false }, { description: 'invalid B only', input: { B: {} }, valid: false } ].forEach((t) => { test(t.description, () => { const errors = validator(t.input); if (t.valid) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });
const validator2 = object({ A: and(or(nullish, string), oxorPeers('B', 'C')), B: and(or(nullish, string), oxorPeers('A', 'C')), C: and(or(nullish, string), oxorPeers('A', 'B')) });
[ [0, 0, 0, 1], [0, 0, 1, 1], [0, 1, 0, 1], [0, 1, 1, 0], [1, 0, 0, 1], [1, 0, 1, 0], [1, 1, 0, 0], [1, 1, 1, 0] // This would be true in an `xor` truth table 🤷‍♂️, but don't think it's what we want? ].forEach(([A, B, C, Q]) => { test(`${A} ${B} ${C} ${Q}`, () => { const toVal = (v: unknown) => (v ? 'valid value' : undefined); const input = { A: toVal(A), B: toVal(B), C: toVal(C) }; const errors = validator2(input); if (Q) { expect(() => assert(errors)).not.toThrow(); } else { expect(() => assert(errors)).toThrow(); } }); });});