Skip to content

Commit 3b90e7f

Browse files
rqrqrqrqmslipper
authored andcommitted
support for prefix option (#265)
* add basic tests for prefix option (#263) * add tests for prefix+namespace oprions (#263) * skip failing test for prefix (#263) * rewrite utils to accept options object istead of namespace (#263) * add skipped tests for flattenUtils `prefix` option (#263) * fixup! add skipped tests for flattenUtils `prefix` option (#263) * implement support for `prefix` option in flattenUtils (#263) * implement prefix option for createActions (#263) * add test for separator inside prefix (#263) * fix unflatten implementation (#263)
1 parent 1b26d0f commit 3b90e7f

File tree

5 files changed

+229
-23
lines changed

5 files changed

+229
-23
lines changed

src/__tests__/createActions-test.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,115 @@ describe('createActions', () => {
295295
meta: { username: 'yangmillstheory', message: 'Hello World' }
296296
});
297297
});
298+
299+
it('should create prefixed actions if `prefix` option exists', () => {
300+
const actionCreators = createActions(
301+
{
302+
APP: {
303+
COUNTER: {
304+
INCREMENT: amount => ({ amount }),
305+
DECREMENT: amount => ({ amount: -amount }),
306+
SET: undefined
307+
},
308+
NOTIFY: (username, message) => ({ message: `${username}: ${message}` })
309+
},
310+
LOGIN: username => ({ username })
311+
},
312+
'ACTION_ONE',
313+
'ACTION_TWO',
314+
{ prefix: 'my-awesome-feature' },
315+
);
316+
317+
expect(actionCreators.app.counter.increment(1)).to.deep.equal({
318+
type: 'my-awesome-feature/APP/COUNTER/INCREMENT',
319+
payload: { amount: 1 }
320+
});
321+
322+
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
323+
type: 'my-awesome-feature/APP/COUNTER/DECREMENT',
324+
payload: { amount: -1 }
325+
});
326+
327+
expect(actionCreators.app.counter.set(100)).to.deep.equal({
328+
type: 'my-awesome-feature/APP/COUNTER/SET',
329+
payload: 100
330+
});
331+
332+
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
333+
type: 'my-awesome-feature/APP/NOTIFY',
334+
payload: { message: 'yangmillstheory: Hello World' }
335+
});
336+
337+
expect(actionCreators.login('yangmillstheory')).to.deep.equal({
338+
type: 'my-awesome-feature/LOGIN',
339+
payload: { username: 'yangmillstheory' }
340+
});
341+
342+
expect(actionCreators.actionOne('one')).to.deep.equal({
343+
type: 'my-awesome-feature/ACTION_ONE',
344+
payload: 'one'
345+
});
346+
347+
expect(actionCreators.actionTwo('two')).to.deep.equal({
348+
type: 'my-awesome-feature/ACTION_TWO',
349+
payload: 'two'
350+
});
351+
});
352+
353+
it('should properly handle `prefix` and `namespace` options provided together', () => {
354+
const actionCreators = createActions(
355+
{
356+
APP: {
357+
COUNTER: {
358+
INCREMENT: amount => ({ amount }),
359+
DECREMENT: amount => ({ amount: -amount }),
360+
SET: undefined
361+
},
362+
NOTIFY: (username, message) => ({ message: `${username}: ${message}` })
363+
},
364+
LOGIN: username => ({ username })
365+
},
366+
'ACTION_ONE',
367+
'ACTION_TWO',
368+
{
369+
prefix: 'my-awesome-feature',
370+
namespace: '--'
371+
},
372+
);
373+
374+
expect(actionCreators.app.counter.increment(1)).to.deep.equal({
375+
type: 'my-awesome-feature--APP--COUNTER--INCREMENT',
376+
payload: { amount: 1 }
377+
});
378+
379+
expect(actionCreators.app.counter.decrement(1)).to.deep.equal({
380+
type: 'my-awesome-feature--APP--COUNTER--DECREMENT',
381+
payload: { amount: -1 }
382+
});
383+
384+
expect(actionCreators.app.counter.set(100)).to.deep.equal({
385+
type: 'my-awesome-feature--APP--COUNTER--SET',
386+
payload: 100
387+
});
388+
389+
expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
390+
type: 'my-awesome-feature--APP--NOTIFY',
391+
payload: { message: 'yangmillstheory: Hello World' }
392+
});
393+
394+
expect(actionCreators.login('yangmillstheory')).to.deep.equal({
395+
type: 'my-awesome-feature--LOGIN',
396+
payload: { username: 'yangmillstheory' }
397+
});
398+
399+
expect(actionCreators.actionOne('one')).to.deep.equal({
400+
type: 'my-awesome-feature--ACTION_ONE',
401+
payload: 'one'
402+
});
403+
404+
expect(actionCreators.actionTwo('two')).to.deep.equal({
405+
type: 'my-awesome-feature--ACTION_TWO',
406+
payload: 'two'
407+
});
408+
});
298409
});

src/__tests__/flattenUtils-test.js

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,53 @@ describe('namespacing actions', () => {
6565
LOGIN: username => ({ username })
6666
};
6767

68-
expect(flattenActionMap(actionMap, '-')).to.deep.equal({
68+
expect(flattenActionMap(actionMap, { namespace: '-' })).to.deep.equal({
6969
'APP-COUNTER-INCREMENT': actionMap.APP.COUNTER.INCREMENT,
7070
'APP-COUNTER-DECREMENT': actionMap.APP.COUNTER.DECREMENT,
7171
'APP-NOTIFY': actionMap.APP.NOTIFY,
7272
LOGIN: actionMap.LOGIN
7373
});
7474
});
75+
76+
it('should handle prefix option', () => {
77+
const actionMap = {
78+
APP: {
79+
COUNTER: {
80+
INCREMENT: amount => ({ amount }),
81+
DECREMENT: amount => ({ amount: -amount })
82+
},
83+
NOTIFY: (username, message) => ({ message: `${username}: ${message}` })
84+
},
85+
LOGIN: username => ({ username })
86+
};
87+
88+
expect(flattenActionMap(actionMap, { prefix: 'my' })).to.deep.equal({
89+
'my/APP/COUNTER/INCREMENT': actionMap.APP.COUNTER.INCREMENT,
90+
'my/APP/COUNTER/DECREMENT': actionMap.APP.COUNTER.DECREMENT,
91+
'my/APP/NOTIFY': actionMap.APP.NOTIFY,
92+
'my/LOGIN': actionMap.LOGIN
93+
});
94+
});
95+
96+
it('should handle prefix + namespace options', () => {
97+
const actionMap = {
98+
APP: {
99+
COUNTER: {
100+
INCREMENT: amount => ({ amount }),
101+
DECREMENT: amount => ({ amount: -amount })
102+
},
103+
NOTIFY: (username, message) => ({ message: `${username}: ${message}` })
104+
},
105+
LOGIN: username => ({ username })
106+
};
107+
108+
expect(flattenActionMap(actionMap, { namespace: '-', prefix: 'my' })).to.deep.equal({
109+
'my-APP-COUNTER-INCREMENT': actionMap.APP.COUNTER.INCREMENT,
110+
'my-APP-COUNTER-DECREMENT': actionMap.APP.COUNTER.DECREMENT,
111+
'my-APP-NOTIFY': actionMap.APP.NOTIFY,
112+
'my-LOGIN': actionMap.LOGIN
113+
});
114+
});
75115
});
76116

77117
describe('unflattenActionCreators', () => {
@@ -97,7 +137,7 @@ describe('namespacing actions', () => {
97137
'APP--COUNTER--DECREMENT': amount => ({ amount: -amount }),
98138
'APP--NOTIFY': (username, message) => ({ message: `${username}: ${message}` }),
99139
LOGIN: username => ({ username })
100-
}, '--');
140+
}, { namespace: '--' });
101141

102142
expect(actionMap.login('yangmillstheory')).to.deep.equal({ username: 'yangmillstheory' });
103143
expect(actionMap.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
@@ -106,5 +146,37 @@ describe('namespacing actions', () => {
106146
expect(actionMap.app.counter.increment(100)).to.deep.equal({ amount: 100 });
107147
expect(actionMap.app.counter.decrement(100)).to.deep.equal({ amount: -100 });
108148
});
149+
150+
it('should unflatten a flattened action map with prefix', () => {
151+
const actionMap = unflattenActionCreators({
152+
'my/feature/APP/COUNTER/INCREMENT': amount => ({ amount }),
153+
'my/feature/APP/COUNTER/DECREMENT': amount => ({ amount: -amount }),
154+
'my/feature/APP/NOTIFY': (username, message) => ({ message: `${username}: ${message}` }),
155+
'my/feature/LOGIN': username => ({ username })
156+
}, { prefix: 'my/feature' });
157+
158+
expect(actionMap.login('test')).to.deep.equal({ username: 'test' });
159+
expect(actionMap.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
160+
message: 'yangmillstheory: Hello World'
161+
});
162+
expect(actionMap.app.counter.increment(100)).to.deep.equal({ amount: 100 });
163+
expect(actionMap.app.counter.decrement(100)).to.deep.equal({ amount: -100 });
164+
});
165+
166+
it('should unflatten a flattened action map with custom namespace and prefix', () => {
167+
const actionMap = unflattenActionCreators({
168+
'my--feature--APP--COUNTER--INCREMENT': amount => ({ amount }),
169+
'my--feature--APP--COUNTER--DECREMENT': amount => ({ amount: -amount }),
170+
'my--feature--APP--NOTIFY': (username, message) => ({ message: `${username}: ${message}` }),
171+
'my--feature--LOGIN': username => ({ username })
172+
}, { namespace: '--', prefix: 'my--feature' });
173+
174+
expect(actionMap.login('test')).to.deep.equal({ username: 'test' });
175+
expect(actionMap.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({
176+
message: 'yangmillstheory: Hello World'
177+
});
178+
expect(actionMap.app.counter.increment(100)).to.deep.equal({ amount: 100 });
179+
expect(actionMap.app.counter.decrement(100)).to.deep.equal({ amount: -100 });
180+
});
109181
});
110182
});

src/createActions.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import invariant from 'invariant';
1111
import arrayToObject from './arrayToObject';
1212
import {
1313
flattenActionMap,
14-
unflattenActionCreators
14+
unflattenActionCreators,
15+
defaultNamespace
1516
} from './flattenUtils';
1617

1718
export default function createActions(actionMap, ...identityActions) {
18-
const { namespace } = isPlainObject(last(identityActions))
19+
const options = isPlainObject(last(identityActions))
1920
? identityActions.pop()
2021
: {};
2122
invariant(
@@ -24,21 +25,21 @@ export default function createActions(actionMap, ...identityActions) {
2425
'Expected optional object followed by string action types'
2526
);
2627
if (isString(actionMap)) {
27-
return actionCreatorsFromIdentityActions([actionMap, ...identityActions]);
28+
return actionCreatorsFromIdentityActions([actionMap, ...identityActions], options);
2829
}
2930
return {
30-
...actionCreatorsFromActionMap(actionMap, namespace),
31-
...actionCreatorsFromIdentityActions(identityActions)
31+
...actionCreatorsFromActionMap(actionMap, options),
32+
...actionCreatorsFromIdentityActions(identityActions, options)
3233
};
3334
}
3435

35-
function actionCreatorsFromActionMap(actionMap, namespace) {
36-
const flatActionMap = flattenActionMap(actionMap, namespace);
36+
function actionCreatorsFromActionMap(actionMap, options) {
37+
const flatActionMap = flattenActionMap(actionMap, options);
3738
const flatActionCreators = actionMapToActionCreators(flatActionMap);
38-
return unflattenActionCreators(flatActionCreators, namespace);
39+
return unflattenActionCreators(flatActionCreators, options);
3940
}
4041

41-
function actionMapToActionCreators(actionMap) {
42+
function actionMapToActionCreators(actionMap, { prefix, namespace = defaultNamespace } = {}) {
4243
function isValidActionMapValue(actionMapValue) {
4344
if (isFunction(actionMapValue) || isNil(actionMapValue)) {
4445
return true;
@@ -56,19 +57,20 @@ function actionMapToActionCreators(actionMap) {
5657
'Expected function, undefined, null, or array with payload and meta ' +
5758
`functions for ${type}`
5859
);
60+
const prefixedType = prefix ? `${prefix}${namespace}${type}` : type;
5961
const actionCreator = isArray(actionMapValue)
60-
? createAction(type, ...actionMapValue)
61-
: createAction(type, actionMapValue);
62+
? createAction(prefixedType, ...actionMapValue)
63+
: createAction(prefixedType, actionMapValue);
6264
return { ...partialActionCreators, [type]: actionCreator };
6365
});
6466
}
6567

66-
function actionCreatorsFromIdentityActions(identityActions) {
68+
function actionCreatorsFromIdentityActions(identityActions, options) {
6769
const actionMap = arrayToObject(
6870
identityActions,
6971
(partialActionMap, type) => ({ ...partialActionMap, [type]: identity })
7072
);
71-
const actionCreators = actionMapToActionCreators(actionMap);
73+
const actionCreators = actionMapToActionCreators(actionMap, options);
7274
return arrayToObject(
7375
Object.keys(actionCreators),
7476
(partialActionCreators, type) => ({

src/flattenUtils.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import hasGeneratorInterface from './hasGeneratorInterface';
44
import isPlainObject from 'lodash/isPlainObject';
55
import isMap from 'lodash/isMap';
66

7-
const defaultNamespace = '/';
7+
export const defaultNamespace = '/';
88

99
function get(key, x) {
1010
return isMap(x) ? x.get(key) : x[key];
1111
}
1212

1313
const flattenWhenNode = predicate => function flatten(
1414
map,
15-
namespace = defaultNamespace,
15+
{
16+
namespace = defaultNamespace,
17+
prefix
18+
} = {},
1619
partialFlatMap = {},
1720
partialFlatActionType = ''
1821
) {
@@ -22,14 +25,22 @@ const flattenWhenNode = predicate => function flatten(
2225
: type;
2326
}
2427

28+
function connectPrefix(type) {
29+
if (partialFlatActionType || !prefix) {
30+
return type;
31+
}
32+
33+
return `${prefix}${namespace}${type}`;
34+
}
35+
2536
ownKeys(map).forEach(type => {
26-
const nextNamespace = connectNamespace(type);
37+
const nextNamespace = connectPrefix(connectNamespace(type));
2738
const mapValue = get(type, map);
2839

2940
if (!predicate(mapValue)) {
3041
partialFlatMap[nextNamespace] = mapValue;
3142
} else {
32-
flatten(mapValue, namespace, partialFlatMap, nextNamespace);
43+
flatten(mapValue, { namespace, prefix }, partialFlatMap, nextNamespace);
3344
}
3445
});
3546

@@ -41,7 +52,13 @@ const flattenReducerMap = flattenWhenNode(
4152
node => (isPlainObject(node) || isMap(node)) && !hasGeneratorInterface(node)
4253
);
4354

44-
function unflattenActionCreators(flatActionCreators, namespace = defaultNamespace) {
55+
function unflattenActionCreators(
56+
flatActionCreators,
57+
{
58+
namespace = defaultNamespace,
59+
prefix
60+
} = {}
61+
) {
4562
function unflatten(
4663
flatActionType,
4764
partialNestedActionCreators = {},
@@ -63,7 +80,11 @@ function unflattenActionCreators(flatActionCreators, namespace = defaultNamespac
6380
const nestedActionCreators = {};
6481
Object
6582
.getOwnPropertyNames(flatActionCreators)
66-
.forEach(type => unflatten(type, nestedActionCreators, type.split(namespace)));
83+
.forEach(type => {
84+
const unprefixedType = prefix ? type.replace(`${prefix}${namespace}`, '') : type;
85+
return unflatten(type, nestedActionCreators, unprefixedType.split(namespace));
86+
});
87+
6788
return nestedActionCreators;
6889
}
6990

src/handleActions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ function get(key, x) {
1010
return isMap(x) ? x.get(key) : x[key];
1111
}
1212

13-
export default function handleActions(handlers, defaultState, { namespace } = {}) {
13+
export default function handleActions(handlers, defaultState, options = {}) {
1414
invariant(
1515
isPlainObject(handlers) || isMap(handlers),
1616
'Expected handlers to be a plain object.'
1717
);
18-
const flattenedReducerMap = flattenReducerMap(handlers, namespace);
18+
const flattenedReducerMap = flattenReducerMap(handlers, options);
1919
const reducers = ownKeys(flattenedReducerMap).map(type =>
2020
handleAction(
2121
type,

0 commit comments

Comments
 (0)