Skip to content

Commit

Permalink
Merge pull request #17 from ardoq/qualifiers
Browse files Browse the repository at this point in the history
Namespaces
  • Loading branch information
tlaundal authored Sep 27, 2019
2 parents 1733de5 + 30c7ca8 commit 5232135
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 41 deletions.
6 changes: 3 additions & 3 deletions examples/simpleActions.tests.ts → examples/actions.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { map } from 'rxjs/operators';
import { actionCreator, ExtractPayload } from 'rxbeach';
import { ofType, extractPayload } from 'rxbeach/operators';

describe('example', function() {
describe('simple actions', function() {
export default function actionExamples() {
describe('actions', function() {
const voidAction = actionCreator('[test] void action');
const primitiveAction = actionCreator<number>('[test] primitive action');
type Payload = { foo: number };
Expand Down Expand Up @@ -45,4 +45,4 @@ describe('example', function() {
const payload_assignable_to_extracted: Extracted = (null as any) as Payload;
const extracted_assignable_to_payload: Payload = (null as any) as Extracted;
});
});
}
7 changes: 7 additions & 0 deletions examples/index.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import actionExamples from './actions.tests';
import namespaceExamples from './namespace.tests';

describe('examples', function() {
actionExamples();
namespaceExamples();
});
108 changes: 108 additions & 0 deletions examples/namespace.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { equal, deepEqual } from 'assert';
import { of, Subject } from 'rxjs';
import { reduce } from 'rxjs/operators';
import {
actionCreator,
namespaceActionCreator,
namespaceActionDispatcher,
} from 'rxbeach';
import { withNamespace } from 'rxbeach/operators';
import { AnyAction, mockAction } from 'rxbeach/internal';

export default function namespaceExamples() {
describe('namespaces', function() {
const testAction = actionCreator<number>('[test] primitive action');
const namespaceA = 'A';
const namespaceB = 'B';

describe('namespacing action creators', function() {
const testActionA = namespaceActionCreator(namespaceA, testAction);
const testActionB = namespaceActionCreator(namespaceB, testAction);
const actionObjectA = testActionA(1);
const actionObjectB = testActionB(2);

let lastActionNamespaceA: AnyAction | undefined;
let lastActionNamespaceB: AnyAction | undefined;
let sumAllNamespaces: number | undefined;
this.afterEach(async function() {
lastActionNamespaceA = undefined;
lastActionNamespaceB = undefined;
sumAllNamespaces = undefined;
});

this.beforeEach(async function() {
const action$ = of(actionObjectA, actionObjectB);
const actionA$ = action$.pipe(withNamespace(namespaceA));
const actionB$ = action$.pipe(withNamespace(namespaceB));
const sum$ = action$.pipe(reduce((a, b) => a + (b.payload || 0), 0));

lastActionNamespaceA = await actionA$.toPromise();
lastActionNamespaceB = await actionB$.toPromise();
sumAllNamespaces = await sum$.toPromise();
});

it('can filter namespace A', async function() {
equal(lastActionNamespaceA, actionObjectA);
});
it('can filter namespace B', async function() {
equal(lastActionNamespaceB, actionObjectB);
});
it('dispatches to main action$', async function() {
equal(sumAllNamespaces, 3);
});
});

describe('namespacing action dispatchers', function() {
let lastActionNamespaceA: AnyAction | undefined;
let lastActionNamespaceB: AnyAction | undefined;
let sumAllNamespaces: number | undefined;
this.afterEach(function() {
lastActionNamespaceA = undefined;
lastActionNamespaceB = undefined;
sumAllNamespaces = undefined;
});

this.beforeEach(async function() {
const action$ = new Subject<AnyAction>();
const dispatchAction = action$.next.bind(action$);

const dispatchA = namespaceActionDispatcher(namespaceA, dispatchAction);
const dispatchB = namespaceActionDispatcher(namespaceB, dispatchAction);

const a_p = action$.pipe(withNamespace(namespaceA)).toPromise();
const b_p = action$.pipe(withNamespace(namespaceB)).toPromise();
const sum_p = action$
.pipe(reduce((a: any, b: any) => a + (b.payload || 0), 0))
.toPromise();

dispatchA(testAction(1));
dispatchB(testAction(2));
action$.complete();

const [a, b, sum] = await Promise.all([a_p, b_p, sum_p]);

lastActionNamespaceA = a;
lastActionNamespaceB = b;
sumAllNamespaces = sum;
});

it('applies namespace A', function() {
deepEqual(
lastActionNamespaceA,
mockAction(testAction.type, namespaceA, 1)
);
});

it('applies namespace B', function() {
deepEqual(
lastActionNamespaceB,
mockAction(testAction.type, namespaceB, 2)
);
});

it('dispatches to root action$', function() {
equal(sumAllNamespaces, 3);
});
});
});
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"private": true,
"license": "MIT",
"scripts": {
"test": "ts-mocha --paths -p tsconfig.json src/**/*.tests.ts examples/**/*.tests.ts",
"test": "ts-mocha --paths -p tsconfig.json src/*.tests.ts src/**/*.tests.ts examples/**/*.tests.ts",
"build": "tsc -p tsconfig.json",
"lint": "eslint --ext .ts src examples"
},
Expand Down
4 changes: 1 addition & 3 deletions src/actionCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ export function actionCreator(type: string): UnknownActionCreator {
const action = (payload?: any) => ({
type,
payload,
meta: {
qualifiers: [],
},
meta: {},
});
action.type = type;

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export {
} from './types/helpers';

export { actionCreator } from './actionCreator';

export { namespaceActionCreator, namespaceActionDispatcher } from './namespace';
2 changes: 1 addition & 1 deletion src/internal/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { actionWithoutPayload, actionWithPayload } from './testUtils';
export { mockAction } from './testUtils';
export {
VoidPayload,
AnyAction,
Expand Down
27 changes: 11 additions & 16 deletions src/internal/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { ActionWithPayload, ActionWithoutPayload } from 'types/Action';
import { Action } from 'rxbeach';
import { VoidPayload } from 'rxbeach/internal';

export const actionWithoutPayload = (
export const mockAction = <P = VoidPayload>(
type: string,
qualifiers: string[] = []
): ActionWithoutPayload => ({
meta: { qualifiers },
type,
});

export const actionWithPayload = <P>(
type: string,
payload: P,
qualifiers: string[] = []
): ActionWithPayload<P> => ({
...actionWithoutPayload(type, qualifiers),
payload,
});
namespace?: string,
payload?: P
): Action<P> =>
({
meta: { namespace },
type,
payload,
} as Action<P>);
51 changes: 51 additions & 0 deletions src/namespace.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { deepEqual } from 'assert';
import {
namespaceActionCreator,
ActionDispatcher,
namespaceActionDispatcher,
} from 'rxbeach';
import { mockAction, AnyAction } from 'rxbeach/internal';

describe('namespace', function() {
describe('namespaceActionCreator', function() {
const type = 'action type';
const namespace = 'new namespace';
const actionCreator = (payload: number) =>
mockAction(type, 'old namespace', payload);
actionCreator.type = type;

const namespacedActionCreator = namespaceActionCreator(
namespace,
actionCreator
);

const actionObject = namespacedActionCreator(12);

it('Should create actions with namespace', function() {
deepEqual(actionObject, mockAction(type, namespace, 12));
});
});

describe('namespaceActionDispatcher', function() {
let dispatchedAction: AnyAction | undefined;
const parentDispatcher: ActionDispatcher = action =>
(dispatchedAction = action);

const namespace = 'new namespace';
const childDispatcher = namespaceActionDispatcher(
namespace,
parentDispatcher
);

const actionObject = mockAction('action', 'old namespace');

childDispatcher(actionObject);

it('Should invoke the parent dispatcher with namespaced actions', function() {
deepEqual(dispatchedAction, {
payload: undefined,
...mockAction(actionObject.type, namespace),
});
});
});
});
60 changes: 60 additions & 0 deletions src/namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ActionCreator, ActionDispatcher } from 'rxbeach';
import { UnknownAction } from 'rxbeach/internal';

const _namespaceAction = (namespace: string, action: UnknownAction) => ({
type: action.type,
payload: action.payload,
meta: {
...action.meta,
namespace,
},
});

/**
* Decorate an action creator so the created actions have the given namespace
*
* The given namespace will replace existing namespaces on the action objects.
*
* In contrast to `namespaceActionDispatcher`, the function returned by this
* function creates an action object instead of dispatching it.
*
* @see namespaceActionDispatcher
* @param namespace The namespace to set for the created actions
* @param actionCreator The action creator to decorate
* @returns An action creator that creates actions using the passed action
* creator, and sets the given namespace
*/
export const namespaceActionCreator = <Payload>(
namespace: string,
actionCreator: ActionCreator<Payload>
): ActionCreator<Payload> => {
const creator = (payload?: any) =>
_namespaceAction(namespace, actionCreator(payload));
creator.type = actionCreator.type;

return creator as ActionCreator<Payload>;
};

/**
* Decorate an action dispatcher so it dispatches namespaced actions
*
* The given namespace is set as the namespace for each action dispatched with
* this function, before the action is dispatched with the action dispatcher
* from the arguments.
*
* In contrast to `namespaceActionCreator`, the function returned by this
* function dispatches the action, instead of creating it.
*
* @see namespaceActionCreator
* @param parentDispatcher The dispatcher the returned action dispatcher will
* dispatch to
* @param namespace The namespace that will be set for each action before they
* are passed on to the parent dispatcher
* @returns An action dispatcher that sets the namespace before passing the
* actions to the parent dispatcher
*/
export const namespaceActionDispatcher = (
namespace: string,
parentDispatcher: ActionDispatcher
): ActionDispatcher => action =>
parentDispatcher(_namespaceAction(namespace, action));
2 changes: 1 addition & 1 deletion src/operators/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { ofType, extractPayload } from './operators';
export { ofType, extractPayload, withNamespace } from './operators';
45 changes: 31 additions & 14 deletions src/operators/operators.tests.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { equal, deepEqual } from 'assert';
import { of, OperatorFunction } from 'rxjs';
import { tap, reduce } from 'rxjs/operators';
import { reduce } from 'rxjs/operators';
import { ActionWithPayload, ActionWithoutPayload } from 'rxbeach';
import { extractPayload, ofType } from 'rxbeach/operators';
import { actionWithPayload, actionWithoutPayload } from 'rxbeach/internal';
import { mockAction } from 'rxbeach/internal';
import { withNamespace } from './operators';

const pipeActionWithPayload = <P, R>(
payload: P,
pipe: OperatorFunction<ActionWithPayload<P>, R>
): Promise<R> =>
of(actionWithPayload('', payload))
of(mockAction('', '', payload) as ActionWithPayload<P>)
.pipe(pipe)
.toPromise();

Expand All @@ -35,16 +36,15 @@ describe('operators', function() {
const targetType = 'Correct type';
const otherType = 'Wrong type';

await of<ActionWithoutPayload>(
actionWithoutPayload(targetType),
actionWithoutPayload(otherType),
actionWithoutPayload(targetType)
const res = await of<ActionWithoutPayload>(
mockAction(targetType),
mockAction(otherType),
mockAction(targetType)
)
.pipe(
ofType(targetType),
tap(action => equal(action.type, targetType))
)
.pipe(ofType(targetType))
.toPromise();

equal(res.type, targetType);
});

it('Should filter multiple action types', async function() {
Expand All @@ -53,9 +53,9 @@ describe('operators', function() {
const otherType = 'Wrong type';

const collectedTypes = await of<ActionWithoutPayload>(
actionWithoutPayload(targetType1),
actionWithoutPayload(otherType),
actionWithoutPayload(targetType2)
mockAction(targetType1),
mockAction(otherType),
mockAction(targetType2)
)
.pipe(
ofType(targetType1, targetType2),
Expand All @@ -66,4 +66,21 @@ describe('operators', function() {
deepEqual(collectedTypes, [targetType1, targetType2]);
});
});

describe('withNamespace', function() {
it('Should filter actions by namespace', async function() {
const actionType = 'actionType';
const namespace = 'namespace';

const res = await of<ActionWithoutPayload>(
mockAction(actionType),
mockAction(actionType, namespace),
mockAction(actionType)
)
.pipe(withNamespace(namespace))
.toPromise();

deepEqual(res, mockAction(actionType, namespace));
});
});
});
Loading

0 comments on commit 5232135

Please sign in to comment.