UNPKG

results

Version:

Discriminated Unions including Maybe (an option type) and Result for javascript with fewer bugs

571 lines (557 loc) 20.7 kB
var assert = require('assert'); try { var Immutable = require('immutable'); } catch (err) { throw new Error('Immutablejs must be installed to run tests'); } var { Union, Result, Ok, Err, Maybe, Some, None } = require('./index'); describe('Union', () => { it('should fail if options are missing', () => { assert.throws(() => Union()); }); it('should accept an object', () => { const U = Union({A: null}); assert.equal(U.match(U.A(), {A: () => 42}), 42); }); it('should.. work for empty object... I guess', () => { assert.ok(Union({})); }); it('should stringify nicely', () => { assert.equal(String(Union({A: null})), '[Union { A }]'); assert.equal(String(Union({A: null, B: null})), '[Union { A, B }]'); const u = Union({A: null}); assert.equal(String(u.A()), `[A(undefined) from Union{ A }]`); assert.equal(String(u.A(1)), `[A(1) from Union{ A }]`); }); describe('.equals', () => { const U = Union({A: null, B: null}); const ae1 = U.A(); const ae2 = U.A(); const a1a = U.A('a'); const a1a2 = U.A('a'); const a1ab = U.A('b'); const be1 = U.B(); const b1a = U.B('a'); const ar = U.A(U.A(U.A())); const ar2 = U.A(U.A(U.A())); const arn = U.A(U.A(U.B())); it('should be true for the same instance', () => { assert.ok(ae1.equals(ae1)); }); it('should be true for the same member with the same params', () => { assert.ok(ae1.equals(ae2)); assert.ok(a1a.equals(a1a2)); }); it('should be false for the same member with different params', () => { assert.ifError(ae1.equals(a1a)); assert.ifError(a1a.equals(a1ab)); }); it('should be false for different members with any params', () => { assert.ifError(ae1.equals(be1)); assert.ifError(a1a.equals(b1a)); }); it('should check recursively', () => { assert.ok(ar.equals(ar2)); assert.ifError(ar.equals(arn)); }); it('should return false for another instance implementing .equals', () => { const alien = { equals: () => true }; assert.ifError(ae1.equals(alien)); }); }); describe('.hashCode', () => { it('should return a 32-bit int', () => { const hashCode = Union({A: null}).A().hashCode(); assert.strictEqual(Number(hashCode), hashCode); assert.equal(hashCode % 1, 0); // remainder zero only if it's an int }); it('should be equal for equivalent members', () => { const { A } = Union({A: null}); assert.equal(A().hashCode(), A().hashCode()); assert.equal(A(1).hashCode(), A(1).hashCode()); }); }); describe('.match', () => { it('should throw if the instance is not from this union', () => { const U1 = Union({A: null}); const U2 = Union({A: null}); assert.throws(() => U1.match(U2.A(), {A: () => 1})); try { U1.match(U2.A(1), {A: () => 1}); assert.fail('should have thrown for non-member'); } catch (err) { assert.equal(err.message, 'match called on a non-member option: \'[A(1) from Union{ A }]\'. Expected a member from Union{ A }'); } }); it('should throw if the match paths are not exhaustive', () => { const U = Union({A: 0, B: 0}); assert.throws(() => U.match(U.A(), {a: () => 'whatever'})); }); it('should allow literal _ as a key (not just the imported symbol)', () => { const U = Union({A: 0, B: 0}); assert.equal(U.match(U.A(), {_: () => 42}), 42); assert.equal(U.match(U.B(), {_: () => 42}), 42); }); it('should pass a value to `match` callbacks', () => { const U = Union({A: 1}); assert.equal(U.match(U.A(42), {A: (v) => v}), 42); }); it('should apply its payloads to the catch-all handler', () => { const U = Union({A: 1}); assert.equal(U.match(U.A(42), {_: n => n}), 42); }); it('should throw for unrecognized keys', () => { var U = Union({A: 0, B: 1}); var f = () => null; assert.throws(() => U.match(U.A(), {A: f, B: f, C: f})); }); it('should give a useful error message for a non-function match prop', () => { const U = Union({A: {}}); try { U.match(U.A(), {}); assert.fail('should have thrown'); } catch (err) { assert(err instanceof Error); assert.equal(err.message, `Non-exhaustive match is missing 'A'`); } try { U.match(U.A(), {A: 1}); assert.fail('should have thrown'); } catch (err) { assert.equal(err.message, `match expected a function for 'A', but found a 'number'`); } }); }); describe('.options', () => { it('should be exposed', () => { const U = Union({A: null}); assert.deepEqual(U.options, {A: null}); }); it('should have all the options', () => { const U = Union({A: null, B: null, C: null}); assert.deepEqual(U.options, {A: null, B: null, C: null}); }); it('should throw if constructed with `options` as an option', () => { assert.throws(() => Union({options: null})); }); }); describe('errors thrown by results', () => { it('should be instance of Error', () => { assert.throws(() => Union()); // no members try { Union(); } catch (err) { assert(err instanceof Error); } }); }); describe('is', () => { it('should deep-check equality for two union option members', () => { const U = Union({A: null}); assert.ok(Union.is(U.A(1), U.A(1))); assert.ifError(Union.is(U.A(1), U.A(2))); }); }); }); describe('Maybe', () => { it('should map to Some or None callbacks', () => { assert.equal(Maybe.match(Some(1), {Some: (v) => v, None: () => null}), 1); assert.equal(Maybe.match(None(), {Some: () => null, None: () => 1}), 1); }); it('should self-identify', () => { assert.ok(Some(1).isSome()); assert.ok(None().isNone()); }); it('should other-anti-identify', () => { assert.equal(Some(1).isNone(), false); assert.equal(None().isSome(), false); }); it('.Some should use Maybes given to it', () => { assert.equal(Some(Some(1)).unwrap(), 1); assert.equal(Some(None()).isNone(), true); }); it('should throw or not for expect', () => { assert.doesNotThrow(() => {Some(1).expect('err')}); assert.throws(() => {None().expect('err')}, 'err'); }); it('should unwrap or throw', () => { assert.equal(Some(1).unwrap(), 1); assert.throws(() => None().unwrap()); }); it('should unwrap its value or a default for unwrapOr', () => { assert.equal(Some(1).unwrapOr(2), 1); assert.equal(None().unwrapOr(2), 2); }); it('should unwrap its value or call a function for unwrapOrElse', () => { assert.equal(Some(1).unwrapOrElse(() => 2), 1); assert.equal(None().unwrapOrElse(() => 2), 2); }); it('should convert to Ok/Err', () => { assert.ok(Some(1).okOr('sad').isOk()); assert.ok(None().okOr('sad').isErr()); }); it('.okOrElse', () => { assert.ok(Some(1).okOrElse(() => 'sad').isOk()); assert.ok(None().okOrElse(() => 'sad').isErr()); }); it('Some should resolve a promise with .promiseOr', () => { return Some(1) .promiseOr() .then(v => v === 1 ? v : Promise.reject(new Error(`got '${v}', expected 1`))); }); it('None should reject with .promiseOr', () => { return None() .promiseOr(1) .catch(e => e === 1 ? Promise.resolve(e) : Promise.reject(new Error(`got '${e}', expected 1`))); }); it('Some should resolve a promise with .promiseOrElse', () => { return Some(1) .promiseOrElse() .then(v => v === 1 ? v : Promise.reject(new Error(`got '${v}', expected 1`))); }); it('None should reject with .promiseOrElse', () => { return None() .promiseOrElse(() => 1) .catch(e => e === 1 ? Promise.resolve(e) : Promise.reject(new Error(`got '${e}', expected 1`))); }); it('should never be the result of .and if Some', () => { assert.equal(Some(1).and(Some(2)).unwrap(), 2); assert.ok(None().and(Some(2)).isNone()); assert.ok(Some(1).and(None()).isNone()); assert.ok(None().and(None()).isNone()); }); it('.and should promote non-Maybe to Some', () => { assert.ok(Some(1).and(2).isSome()); assert.equal(Some(1).and(2).unwrap(), 2); }); it('should be used with andThen if Some', () => { assert.equal(Some(1).andThen((v) => Some(v*2)).unwrap(), 2); assert.ok(None().andThen((v) => Some(v*2)).isNone()); assert.ok(Some(1).andThen((v) => None()).isNone()); assert.ok(None().andThen((v) => None()).isNone()); }); it('.andThen should promote non-Maybe to Some', () => { assert.ok(Some(1).andThen(() => 2).isSome()); assert.equal(Some(1).andThen(() => 2).unwrap(), 2); }); it('.or', () => { assert.equal(Some(1).or(Some(2)).unwrap(), 1); assert.equal(None().or(Some(2)).unwrap(), 2); assert.equal(Some(1).or(None()).unwrap(), 1); assert.ok(None().or(None()).isNone()); }); it('.or should promote non-Maybe to Some', () => { assert.ok(None().or(1).isSome()); assert.equal(None().or(1).unwrap(), 1); }); it('.orElse', () => { assert.equal(Some(1).orElse(() => Some(2)).unwrap(), 1); assert.equal(None().orElse(() => Some(2)).unwrap(), 2); assert.equal(Some(1).orElse(() => None()).unwrap(), 1); assert.ok(None().orElse(() => None()).isNone()); }); it('.orElse should promote non-Maybe to Some', () => { assert.ok(None().orElse(() => 1).isSome()); assert.equal(None().orElse(() => 1).unwrap(), 1); }); it('.filter should none-ify false-y returns', () => { assert.ok(None().filter(x => true).isNone()); assert.equal(Some(1).filter(x => true).unwrap(), 1); assert.ok(Some(1).filter(x => false).isNone()); }); it('.equals should work', () => { assert.ok(None().equals(None())); assert.ifError(None().equals(Some())); assert.ok(Some(1).equals(Some(1))); assert.ifError(Some(1).equals(Some(2))); }); describe('Maybe.all', () => { it('should make an empty Some for an empty array', () => { var a = []; assert.ok(Maybe.all(a).isSome()); assert.deepEqual(Maybe.all(a).unwrap(), []); }); it('should pass through 1 Some in an array', () => { var ma = Maybe.all([Some(1)]); assert.ok(ma.isSome()); assert.deepEqual(ma.unwrap(), [1]); }); it('should pass through n Somes in an array and preserve order', () => { var ma2 = Maybe.all([Some(1), Some(2)]); assert.ok(ma2.isSome()); assert.deepEqual(ma2.unwrap(), [1, 2]); assert.deepEqual(Maybe.all([Some(1), Some(2), Some(3)]).unwrap(), [1, 2, 3]); }); it('should accept non-Maybe input as Some', () => { assert.ok(Maybe.all([0]).isSome()); assert.deepEqual(Maybe.all([0]).unwrap(), [0]); assert.deepEqual(Maybe.all([0, Some(0)]).unwrap(), [0, 0]); }); it('should be None if any inputs are None', () => { assert(Maybe.all([None()]).isNone()); assert(Maybe.all([None(), None()]).isNone()); assert(Maybe.all([Some(1), None()]).isNone()); assert(Maybe.all([None(), Some(1)]).isNone()); assert(Maybe.all([None(), 1]).isNone()); assert(Maybe.all([1, None()]).isNone()); }); it('regression: should not flatten arrays', () => { assert.deepEqual(Maybe.all([[]]).unwrap(), [[]]); assert.deepEqual(Maybe.all([Some(1), Some([2])]).unwrap(), [1, [2]]); }); }); describe('Maybe.undefined', () => { it('Should wrap any non-undefined value in Some', () => { assert(Maybe.undefined(1).isSome()); assert.equal(Maybe.undefined(1).unwrap(), 1); assert(Maybe.undefined(0).isSome()); assert.equal(Maybe.undefined(0).unwrap(), 0); assert(Maybe.undefined(null).isSome()); }); it('Should return None() for undefined', () => { assert(Maybe.undefined(undefined).isNone()); assert(Maybe.undefined().isNone()); }); }); describe('Maybe.null', () => { it('Should wrap any non-null value in Some', () => { assert(Maybe.null(1).isSome()); assert.equal(Maybe.null(1).unwrap(), 1); assert(Maybe.null(0).isSome()); assert.equal(Maybe.null(0).unwrap(), 0); assert(Maybe.null().isSome()); }); it('Should return None() for null', () => { assert(Maybe.null(null).isNone()); }); }); describe('Maybe.nan', () => { it('should wrap any non-nan in Some', () => { assert(Maybe.nan(1).isSome()); assert.equal(Maybe.nan(1).unwrap(), 1); assert(Maybe.nan().isSome()); }); it('should return None() for nan', () => { assert(Maybe.nan(NaN).isNone()); }); }); }); describe('Result', () => { it('should map to Ok or Err callbacks', () => { assert.equal(Result.match(Ok(1), {Ok: (v) => v, Err: (e) => e}), 1); assert.equal(Result.match(Err(2), {Ok: (v) => v, Err: (e) => e}), 2); }); it('should self-identify', () => { assert.ok(Ok(1).isOk()); assert.ok(Err(2).isErr()); }); it('should should anti-self-other-identify', () => { assert.equal(Ok(1).isErr(), false); assert.equal(Err(2).isOk(), false); }); it('.Ok should use a Result given to it', () => { assert.equal(Ok(Ok(1)).unwrap(), 1); assert.equal(Ok(Err(2)).isErr(), true); }); it('.Err should not unwrap Results given to it', () => { assert.ok(Err(Ok(1)).unwrapErr().isOk()); assert.ok(Err(Err(2)).unwrapErr().isErr()); }); it('should convert to an Option', () => { assert.ok(Ok(1).ok().isSome()); assert.equal(Ok(1).ok().unwrap(), 1); assert.ok(Err(2).ok().isNone()); }); it('should convert to an Option with .err', () => { assert.ok(Ok(1).err().isNone()); assert.ok(Err(2).err().isSome()); assert.equal(Err(2).err().unwrap(), 2); }); it('Ok should resolve a promise with .promise', () => { return Ok(1) .promise() .then(v => v === 1 ? v : Promise.reject(new Error(`got '${v}', expected 1`))); }); it('Err should reject with .promise', () => { return Err(1) .promise() .catch(e => e === 1 ? Promise.resolve(e) : Promise.reject(new Error(`got '${e}', expected 1`))); }); it('Ok should reject a promise with .promiseErr', () => { return Ok(1) .promiseErr() .catch(e => e === 1 ? Promise.resolve(e) : Promise.reject(new Error(`got '${e}', expected 1`))); }); it('Err should resolve with .promiseErr', () => { return Err(1) .promiseErr() .then(v => v === 1 ? v : Promise.reject(new Error(`got '${v}', expected 1`))); }); it('.expect', () => { assert.equal(Ok(1).expect(), 1); assert.throws(() => Err(1).expect(new Error('asdf')), Error); }); it('.and', () => { assert.ok(Ok(1).and(Ok(-1)).isOk()); assert.equal(Ok(1).and(Ok(-1)).unwrap(), -1); assert.ok(Err(2).and(Ok(-1)).isErr()); assert.equal(Err(2).and(Ok(-1)).unwrapErr(), 2); assert.ok(Ok(1).and(Err(-2)).isErr()); assert.equal(Ok(1).and(Err(-2)).unwrapErr(), -2); assert.ok(Err(2).and(Err(-2)).isErr()); assert.equal(Err(2).and(Err(-2)).unwrapErr(), 2); }); it('.and should promote non-Result to Ok', () => { assert.ok(Err(1).or(2).isOk()); assert.equal(Err(1).or(2).unwrap(), 2); }); it('.andThen', () => { var sq = (v) => Ok(v * v); assert.equal(Ok(-1).andThen(sq).unwrap(), 1); assert.ok(Err(2).andThen(sq).isErr()); }); it('.andThen should promote non-Result to Ok', () => { assert.ok(Ok(1).andThen(() => 2).isOk()); assert.equal(Ok(1).andThen(() => 2).unwrap(), 2); }); it('.or', () => { assert.ok(Ok(1).or(Ok(-1)).isOk()); assert.equal(Ok(1).or(Ok(-1)).unwrap(), 1); assert.ok(Err(2).or(Ok(-1)).isOk()); assert.equal(Err(2).or(Ok(-1)).unwrap(), -1); assert.ok(Ok(1).or(Err(-2)).isOk()); assert.equal(Ok(1).or(Err(-2)).unwrap(), 1); assert.ok(Err(2).or(Err(-2)).isErr()); assert.equal(Err(2).or(Err(-2)).unwrapErr(), -2); }); it('.or should promote non-result to Ok', () => { assert.ok(Err(1).or(2).isOk()); assert.equal(Err(1).or(2).unwrap(), 2); }); it('.orElse', () => { var timesTwo = (n) => Ok(n*2); assert.equal(Ok(1).orElse(timesTwo).unwrap(), 1); assert.equal(Err(-2).orElse(timesTwo).unwrap(), -4); }); it('.orElse should promote non-Result to Ok', () => { assert.ok(Err(1).orElse(() => 2).isOk()); assert.equal(Err(1).orElse(() => 2).unwrap(), 2); }); it('.unwrapOr', () => { assert.equal(Ok(1).unwrapOr(5), 1); assert.equal(Err(2).unwrapOr(5), 5); }); it('.unwrapOrElse', () => { var timesTwo = (n) => n*2; assert.equal(Ok(1).unwrapOrElse(timesTwo), 1); assert.equal(Err(2).unwrapOrElse(timesTwo), 4); }); it('.unwrap', () => { assert.equal(Ok(1).unwrap(), 1); try { Err(42).unwrap(); assert.fail('should have thrown when unwrapping Err()'); } catch (err) { assert.equal(err, 42); } }); it('.unwrapErr', () => { assert.equal(Err(1).unwrapErr(), 1); assert.throws(() => Ok(1).unwrapErr()); }); it('.equals should work', () => { assert.ok(Ok(1).equals(Ok(1))); assert.ifError(Ok(1).equals(Ok(2))); assert.ok(Err(1).equals(Err(1))); assert.ifError(Err(1).equals(Err(2))); assert.ifError(Ok(1).equals(Err(1))); }); describe('Result.all', () => { it('should make an empty Ok for an empty array', () => { var a = []; assert.ok(Result.all(a).isOk()); assert.deepEqual(Result.all(a).unwrap(), []); }); it('should pass through 1 Ok in an array', () => { var oa = Result.all([Ok(1)]); assert.ok(oa.isOk()); assert.deepEqual(oa.unwrap(), [1]); }); it('should pass through n Oks in an array and preserve order', () => { var oa2 = Result.all([Ok(1), Ok(2)]); assert.ok(oa2.isOk()); assert.deepEqual(oa2.unwrap(), [1, 2]); assert.deepEqual(Result.all([Ok(1), Ok(2), Ok(3)]).unwrap(), [1, 2, 3]); }); it('should accept non-Result input as Ok', () => { assert.ok(Result.all([0]).isOk()); assert.deepEqual(Result.all([0]).unwrap(), [0]); assert.deepEqual(Result.all([0, Ok(0)]).unwrap(), [0, 0]); }); it('should be the first Err if any inputs are Err', () => { assert(Result.all([Err()]).isErr()); assert(Result.all([Err(9), Err(8)]).isErr()); assert(Result.all([Ok(1), Err(9)]).isErr()); assert(Result.all([Err(9), Ok(1)]).isErr()); assert(Result.all([Err(9), 1]).isErr()); assert(Result.all([1, Err(9)]).isErr()); assert.equal(Result.all([Err(9), Err(8)]).unwrapErr(), 9); assert.equal(Result.all([0, Err(9)]).unwrapErr(), 9); }); it('regression: should not flatten arrays', () => { assert.deepEqual(Result.all([[]]).unwrap(), [[]]); assert.deepEqual(Result.all([Ok(1), Ok([2])]).unwrap(), [1, [2]]); }); }); describe('Result.try', () => { it('should return a Some(retVal) for functions that don\'t throw', () => { const retVal = {}; const res = Result.try(() => retVal); assert(res.isOk()); assert(res.unwrap() === retVal); }); it('should return a None(err) for functions that throw', () => { const err = new Error('an error'); const thrower = () => { throw err; } assert(Result.try(thrower).isErr()); assert(Result.try(thrower).unwrapErr() === err); }); }); }); describe('compatibility', () => { describe('immutablejs', () => { it('should not be destroyed by Map or .fromJS', () => { const U = Union({A: null}); const m = Immutable.Map({ a: U.A() }); assert(m.get('a').equals(U.A())); const fjs = Immutable.fromJS({ a: U.A() }); assert(fjs.get('a').equals(U.A())); }); it('.equals should recurse into immutablejs structures', () => { const U = Union({A: null}); const immuInUnion = U.A(Immutable.List([1])); assert.ok(immuInUnion.equals(U.A(Immutable.List([1]))), 'union .equals recurses into immutable .equals'); assert.ifError(immuInUnion.equals(U.A(Immutable.List([2])))); }); it('immutable .equals should recurse into results structures', () => { const U = Union({A: null}); const unionInImmu = Immutable.List([U.A(1)]); assert.ok(unionInImmu.equals(Immutable.List([U.A(1)]))); assert.ifError(unionInImmu.equals(Immutable.List([U.A(2)]))); }); it('regression: should pass .equals for date instances', () => { const U = Union({ A: null }); const a = U.A(new Date('2016-01-01')); const b = U.A(new Date('2016-01-01')); assert.ok(a.equals(b)); }); }); });