Skip to content

Commit

Permalink
feat: add dispatch (#407)
Browse files Browse the repository at this point in the history
* feat: add dispatch

Ref #66

* style: remove exclusive test

* style(dispatch): use should sentences in tests

* refactor(dispatch): avoid creating function during runtime

* test(dispatch): add test for returning undefined

* refactor(dispatch): make accumulator more sensible

* test(dispach): add test for falsy function dispatches

* refactor(dispatch): make tests pass

* refactor(dispatch): do not handle side effects in any way

* refactor(dispatch): simplify the composition

* docs(dispatch): add documentation + typings

* test(dispatch): correct the tests

* test(dispatch): add test for implicit returns
  • Loading branch information
char0n authored and Undistraction committed Mar 6, 2018
1 parent b2b904c commit fb5aa08
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 0 deletions.
72 changes: 72 additions & 0 deletions src/dispatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
sort,
comparator,
prop,
pipe,
head,
curryN,
reduce,
reduced,
curry,
ifElse,
} from 'ramda';

/**
* Can be used as a way to compose multiple invokers together to form polymorphic functions,
* or functions that exhibit different behaviors based on their argument(s).
* Consumes dispatching functions and keep trying to invoke each in turn, until a non-nil value is returned.
*
* Accepts a list of dispatching functions and returns a new function.
* When invoked, this new function is applied to some arguments,
* each dispatching function is applied to those same arguments until one of the
* dispatching functions returns a non-nil value.
*
* @func dispatch
* @memberOf RA
* @since {@link https://char0n.github.io/ramda-adjunct/2.6.0|v2.6.0}
* @category Function
* @sig [((a, b, ...) -> x1), ((a, b, ...) -> x2), ...] -> x1 | x2 | ...
* @param {!Array} functions A list of functions
* @return {*|undefined} Returns the first not-nil value, or undefined if either an empty list is provided or none of the dispatching functions returns a non-nil value
* @see {@link RA.isNotNil}
* @example
*
* // returns first non-nil value
* const stubNil = () => null;
* const stubUndefined = () => undefined;
* const addOne = v => v + 1;
* const addTwo = v => v + 2;
*
* RA.dispatch([stubNil, stubUndefined, addOne, addTwo])(1); //=> 2
*
* // acts as a switch
* const fnSwitch = RA.dispatch([
* R.ifElse(RA.isString, s => `${s}-join`, RA.stubUndefined),
* R.ifElse(RA.isNumber, n => n + 1, RA.stubUndefined),
* R.ifElse(RA.isDate, R.T, RA.stubUndefined),
* ]);
* fnSwitch(1); //=> 2
*/
import isNotNil from './isNotNil';
import isNonEmptyArray from './isNonEmptyArray';
import stubUndefined from './stubUndefined';

const byArity = comparator((a, b) => a.length > b.length);

const getMaxArity = pipe(sort(byArity), head, prop('length'));

const iteratorFn = curry((args, accumulator, fn) => {
const result = fn(...args);

return isNotNil(result) ? reduced(result) : accumulator;
});

const dispatch = functions => {
const arity = getMaxArity(functions);

return curryN(arity, (...args) =>
reduce(iteratorFn(args), undefined, functions)
);
};

export default ifElse(isNonEmptyArray, dispatch, stubUndefined);
12 changes: 12 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,18 @@ declare namespace RamdaAdjunct {
*/
appendFlipped<T>(list: T[], val: any): T[];

/**
* Can be used as a way to compose multiple invokers together to form polymorphic functions,
* or functions that exhibit different behaviors based on their argument(s).
* Consumes dispatching functions and keep trying to invoke each in turn, until a non-nil value is returned.
*
* Accepts a list of dispatching functions and returns a new function.
* When invoked, this new function is applied to some arguments,
* each dispatching function is applied to those same arguments until one of the
* dispatching functions returns a non-nil value.
*/
dispatch(functions: Function[]): Function;

/**
* Identity type.
*/
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export { default as rejectP } from './rejectP';
export { default as Y } from './Y';
export { default as seq } from './seq';
export { default as sequencing } from './seq';
export { default as dispatch } from './dispatch';
// List
export { default as mapIndexed } from './mapIndexed';
export { default as reduceIndexed } from './reduceIndexed';
Expand Down
97 changes: 97 additions & 0 deletions test/dispatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as R from 'ramda';
import { assert } from 'chai';
import sinon from 'sinon';

import * as RA from '../src';
import eq from './shared/eq';

describe('dispatch', function() {
it('should return first non-nil value', function() {
const nullStub = sinon.stub().returns(null);
const undefinedStub = sinon.stub().returns(undefined);
const zeroStub = sinon.stub().returns(0);
const positiveNumberStub = sinon.stub().returns(1);

const actual = RA.dispatch([
nullStub,
undefinedStub,
zeroStub,
positiveNumberStub,
])('test');

assert.strictEqual(actual, 0);
assert.isTrue(nullStub.calledOnceWithExactly('test'));
assert.isTrue(undefinedStub.calledOnceWithExactly('test'));
assert.isTrue(zeroStub.calledOnceWithExactly('test'));
assert.isTrue(positiveNumberStub.notCalled);
});

it('should return curried function with max arity', function() {
const fn = RA.dispatch([R.divide, R.identity]);

eq(fn.length, 2);
});

it('should act as switch', function() {
const isString = sinon.stub().returns(false);
const stringDispatch = sinon.stub().returns(undefined);
const isNumber = sinon.stub().returns(true);
const numberDispatch = sinon.stub().returns(true);
const isDate = sinon.stub().returns(false);
const dateDispatch = sinon.stub().returns(false);

const fnSwitch = RA.dispatch([
R.ifElse(isString, stringDispatch, RA.stubUndefined),
R.ifElse(isNumber, numberDispatch, RA.stubUndefined),
R.ifElse(isDate, dateDispatch, RA.stubUndefined),
]);
fnSwitch(1);

assert.isTrue(isString.calledOnceWithExactly(1));
assert.isTrue(stringDispatch.notCalled);
assert.isTrue(isNumber.calledOnceWithExactly(1));
assert.isTrue(numberDispatch.calledOnceWithExactly(1));
assert.isTrue(isDate.notCalled);
assert.isTrue(dateDispatch.notCalled);
});

context('when dispatched function throws', function() {
context('the error', function() {
specify('should bubble up', function() {
const configuredDispatch = RA.dispatch([
() => {
throw new Error();
},
R.always(1),
]);

assert.throws(() => configuredDispatch('test'));
});
});
});

context('when empty array provided as input', function() {
specify('should return undefined', function() {
eq(RA.dispatch([]), undefined);
});
});

context('when all dispatched functions returns nil', function() {
specify('should return undefined', function() {
const configuredDispatch = RA.dispatch([RA.stubUndefined, RA.stubNull]);

eq(configuredDispatch(), undefined);
});
});

context(
'when all dispatched functions have implicit return statement',
function() {
specify('should return undefined', function() {
const configuredDispatch = RA.dispatch([() => {}, () => {}]);

eq(configuredDispatch(), undefined);
});
}
);
});

0 comments on commit fb5aa08

Please sign in to comment.