diff --git a/README.md b/README.md index a3f91949..e8c1594b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you don’t use [npm](https://www.npmjs.com), you may grab the latest [UMD](h import { createAction } from 'redux-actions'; ``` -Wraps an action creator so that its return value is the payload of a Flux Standard Action. +Wraps an action creator so that its return value is the payload of a Flux Standard Action. `payloadCreator` must be a function, `undefined`, or `null`. If `payloadCreator` is `undefined` or `null`, the identity function is used. @@ -89,22 +89,24 @@ createAction('ADD_TODO')('Use Redux'); `metaCreator` is an optional function that creates metadata for the payload. It receives the same arguments as the payload creator, but its result becomes the meta field of the resulting action. If `metaCreator` is undefined or not a function, the meta field is omitted. -### `createActions(?actionsMap, ?...identityActions)` +### `createActions(?actionMap, ?...identityActions)` ```js import { createActions } from 'redux-actions'; ``` -Returns an object mapping action types to action creators. The keys of this object are camel-cased from the keys in `actionsMap` and the string literals of `identityActions`; the values are the action creators. +Returns an object mapping action types to action creators. The keys of this object are camel-cased from the keys in `actionMap` and the string literals of `identityActions`; the values are the action creators. -`actionsMap` is an optional object with action types as keys, and whose values **must** be either +`actionMap` is an optional object and a recursive data structure, with action types as keys, and whose values **must** be either - a function, which is the payload creator for that action - an array with `payload` and `meta` functions in that order, as in [`createAction`](#createactiontype-payloadcreator--identity-metacreator) - `meta` is **required** in this case (otherwise use the function form above) +- an `actionMap` `identityActions` is an optional list of positional string arguments that are action type strings; these action types will use the identity payload creator. + ```js const { actionOne, actionTwo, actionThree } = createActions({ // function form; payload creator defined inline @@ -136,6 +138,42 @@ expect(actionThree(3)).to.deep.equal({ }); ``` +If `actionMap` has a recursive structure, its leaves are used as payload and meta creators, and the action type for each leaf is the combined path to that leaf: + +```js +const actionCreators = createActions({ + APP: { + COUNTER: { + INCREMENT: [ + amount => ({ amount }), + amount => ({ key: 'value', amount }) + ], + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: [ + (username, message) => ({ message: `${username}: ${message}` }), + (username, message) => ({ username, message }) + ] + } +}); + +expect(actionCreators.app.counter.increment(1)).to.deep.equal({ + type: 'APP/COUNTER/INCREMENT', + payload: { amount: 1 }, + meta: { key: 'value', amount: 1 } +}); +expect(actionCreators.app.counter.decrement(1)).to.deep.equal({ + type: 'APP/COUNTER/DECREMENT', + payload: { amount: -1 } +}); +expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({ + type: 'APP/NOTIFY', + payload: { message: 'yangmillstheory: Hello World' }, + meta: { username: 'yangmillstheory', message: 'Hello World' } +}); +``` +When using this form, you can pass an object with key `namespace` as the last positional argument, instead of the default `/`. + ### `handleAction(type, reducer | reducerMap = Identity, defaultState)` ```js @@ -155,7 +193,7 @@ handleAction('FETCH_DATA', { }, defaultState); ``` -If either `next()` or `throw()` are `undefined` or `null`, then the identity function is used for that reducer. +If either `next()` or `throw()` are `undefined` or `null`, then the identity function is used for that reducer. If the reducer argument (`reducer | reducerMap`) is `undefined`, then the identity function is used. @@ -187,9 +225,9 @@ const reducer = handleActions({ }, { counter: 0 }); ``` -### `combineActions(...actionTypes)` +### `combineActions(...types)` -Combine any number of action types or action creators. `actionTypes` is a list of positional arguments which can be action type strings, symbols, or action creators. +Combine any number of action types or action creators. `types` is a list of positional arguments which can be action type strings, symbols, or action creators. This allows you to reduce multiple distinct actions with the same reducer. diff --git a/package.json b/package.json index 51e25a77..4c224da5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-actions", - "version": "1.2.2", + "version": "2.0.0", "description": "Flux Standard Action utlities for Redux", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/__tests__/combineActions-test.js b/src/__tests__/combineActions-test.js index 735591c4..bdf5fa27 100644 --- a/src/__tests__/combineActions-test.js +++ b/src/__tests__/combineActions-test.js @@ -13,12 +13,9 @@ describe('combineActions', () => { it('should accept action creators and action type strings', () => { const { action1, action2 } = createActions('ACTION_1', 'ACTION_2'); - expect(() => combineActions('ACTION_1', 'ACTION_2')) - .not.to.throw(Error); - expect(() => combineActions(action1, action2)) - .not.to.throw(Error); - expect(() => combineActions(action1, action2, 'ACTION_3')) - .not.to.throw(Error); + expect(() => combineActions('ACTION_1', 'ACTION_2')).not.to.throw(Error); + expect(() => combineActions(action1, action2)).not.to.throw(Error); + expect(() => combineActions(action1, action2, 'ACTION_3')).not.to.throw(Error); }); it('should return a stringifiable object', () => { diff --git a/src/__tests__/createActions-test.js b/src/__tests__/createActions-test.js index 2f9c23f3..95139030 100644 --- a/src/__tests__/createActions-test.js +++ b/src/__tests__/createActions-test.js @@ -11,13 +11,6 @@ describe('createActions', () => { }); it('should throw an error when given bad payload creators', () => { - expect( - () => createActions({ ACTION_1: {} }) - ).to.throw( - Error, - 'Expected function, undefined, or array with payload and meta functions for ACTION_1' - ); - expect( () => createActions({ ACTION_1: () => {}, @@ -106,16 +99,16 @@ describe('createActions', () => { }); it('should honor special delimiters in action types', () => { - const { 'p/actionOne': pActionOne, 'q/actionTwo': qActionTwo } = createActions({ + const { p: { actionOne }, q: { actionTwo } } = createActions({ 'P/ACTION_ONE': (key, value) => ({ [key]: value }), 'Q/ACTION_TWO': (first, second) => ([first, second]) }); - expect(pActionOne('value', 1)).to.deep.equal({ + expect(actionOne('value', 1)).to.deep.equal({ type: 'P/ACTION_ONE', payload: { value: 1 } }); - expect(qActionTwo('value', 2)).to.deep.equal({ + expect(actionTwo('value', 2)).to.deep.equal({ type: 'Q/ACTION_TWO', payload: ['value', 2] }); @@ -185,7 +178,7 @@ describe('createActions', () => { }); }); - it('should create actions from an actions map and action types', () => { + it('should create actions from an action map and action types', () => { const { action1, action2, action3, action4 } = createActions({ ACTION_1: (key, value) => ({ [key]: value }), ACTION_2: [ @@ -212,4 +205,108 @@ describe('createActions', () => { payload: 4 }); }); + + it('should create actions from a namespaced action map', () => { + const actionCreators = createActions({ + APP: { + COUNTER: { + INCREMENT: amount => ({ amount }), + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: (username, message) => ({ message: `${username}: ${message}` }) + }, + LOGIN: username => ({ username }) + }, 'ACTION_ONE', 'ACTION_TWO'); + + expect(actionCreators.app.counter.increment(1)).to.deep.equal({ + type: 'APP/COUNTER/INCREMENT', + payload: { amount: 1 } + }); + expect(actionCreators.app.counter.decrement(1)).to.deep.equal({ + type: 'APP/COUNTER/DECREMENT', + payload: { amount: -1 } + }); + expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({ + type: 'APP/NOTIFY', + payload: { message: 'yangmillstheory: Hello World' } + }); + expect(actionCreators.login('yangmillstheory')).to.deep.equal({ + type: 'LOGIN', + payload: { username: 'yangmillstheory' } + }); + expect(actionCreators.actionOne('one')).to.deep.equal({ + type: 'ACTION_ONE', + payload: 'one' + }); + expect(actionCreators.actionTwo('two')).to.deep.equal({ + type: 'ACTION_TWO', + payload: 'two' + }); + }); + + it('should create namespaced actions with payload creators in array form', () => { + const actionCreators = createActions({ + APP: { + COUNTER: { + INCREMENT: [ + amount => ({ amount }), + amount => ({ key: 'value', amount }) + ], + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: [ + (username, message) => ({ message: `${username}: ${message}` }), + (username, message) => ({ username, message }) + ] + } + }); + + expect(actionCreators.app.counter.increment(1)).to.deep.equal({ + type: 'APP/COUNTER/INCREMENT', + payload: { amount: 1 }, + meta: { key: 'value', amount: 1 } + }); + expect(actionCreators.app.counter.decrement(1)).to.deep.equal({ + type: 'APP/COUNTER/DECREMENT', + payload: { amount: -1 } + }); + expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({ + type: 'APP/NOTIFY', + payload: { message: 'yangmillstheory: Hello World' }, + meta: { username: 'yangmillstheory', message: 'Hello World' } + }); + }); + + it('should create namespaced actions with a chosen namespace string', () => { + const actionCreators = createActions({ + APP: { + COUNTER: { + INCREMENT: [ + amount => ({ amount }), + amount => ({ key: 'value', amount }) + ], + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: [ + (username, message) => ({ message: `${username}: ${message}` }), + (username, message) => ({ username, message }) + ] + } + }, { namespace: '--' }); + + expect(actionCreators.app.counter.increment(1)).to.deep.equal({ + type: 'APP--COUNTER--INCREMENT', + payload: { amount: 1 }, + meta: { key: 'value', amount: 1 } + }); + expect(actionCreators.app.counter.decrement(1)).to.deep.equal({ + type: 'APP--COUNTER--DECREMENT', + payload: { amount: -1 } + }); + expect(actionCreators.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({ + type: 'APP--NOTIFY', + payload: { message: 'yangmillstheory: Hello World' }, + meta: { username: 'yangmillstheory', message: 'Hello World' } + }); + }); }); diff --git a/src/__tests__/handleActions-test.js b/src/__tests__/handleActions-test.js index 3c6a6e08..5ecaa349 100644 --- a/src/__tests__/handleActions-test.js +++ b/src/__tests__/handleActions-test.js @@ -184,4 +184,56 @@ describe('handleActions', () => { counter: 7 }); }); + + it('should work with namespaced actions', () => { + const { + app: { + counter: { + increment, + decrement + }, + notify + } + } = createActions({ + APP: { + COUNTER: { + INCREMENT: [ + amount => ({ amount }), + amount => ({ key: 'value', amount }) + ], + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: [ + (username, message) => ({ message: `${username}: ${message}` }), + (username, message) => ({ username, message }) + ] + } + }); + + // note: we should be using combineReducers in production, but this is just a test + const reducer = handleActions({ + [combineActions(increment, decrement)]: ({ counter, message }, { payload: { amount } }) => ({ + counter: counter + amount, + message + }), + + [notify]: ({ counter, message }, { payload }) => ({ + counter, + message: `${message}---${payload.message}` + }) + }, { counter: 0, message: '' }); + + expect(reducer({ counter: 3, message: 'hello' }, increment(2))).to.deep.equal({ + counter: 5, + message: 'hello' + }); + expect(reducer({ counter: 10, message: 'hello' }, decrement(3))).to.deep.equal({ + counter: 7, + message: 'hello' + }); + expect(reducer({ counter: 10, message: 'hello' }, notify('me', 'goodbye'))).to.deep.equal({ + counter: 10, + message: 'hello---me: goodbye' + }); + }); }); diff --git a/src/__tests__/namespaceActions-test.js b/src/__tests__/namespaceActions-test.js new file mode 100644 index 00000000..2b61a604 --- /dev/null +++ b/src/__tests__/namespaceActions-test.js @@ -0,0 +1,110 @@ +import { flattenActionMap, unflattenActionCreators } from '../namespaceActions'; +import { expect } from 'chai'; + +describe('namespacing actions', () => { + describe('flattenActionMap', () => { + it('should flatten an action map with the default namespacer', () => { + const actionMap = { + APP: { + COUNTER: { + INCREMENT: amount => ({ amount }), + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: (username, message) => ({ message: `${username}: ${message}` }) + }, + LOGIN: username => ({ username }) + }; + + expect(flattenActionMap(actionMap)).to.deep.equal({ + 'APP/COUNTER/INCREMENT': actionMap.APP.COUNTER.INCREMENT, + 'APP/COUNTER/DECREMENT': actionMap.APP.COUNTER.DECREMENT, + 'APP/NOTIFY': actionMap.APP.NOTIFY, + LOGIN: actionMap.LOGIN + }); + }); + + it('should do nothing to an already flattened map', () => { + const actionMap = { + INCREMENT: amount => ({ amount }), + DECREMENT: amount => ({ amount: -amount }), + LOGIN: username => ({ username }) + }; + + expect(flattenActionMap(actionMap)).to.deep.equal(actionMap); + }); + + it('should be case-sensitive', () => { + const actionMap = { + app: { + counter: { + increment: amount => ({ amount }), + decrement: amount => ({ amount: -amount }) + }, + notify: (username, message) => ({ message: `${username}: ${message}` }) + }, + login: username => ({ username }) + }; + + expect(flattenActionMap(actionMap)).to.deep.equal({ + 'app/counter/increment': actionMap.app.counter.increment, + 'app/counter/decrement': actionMap.app.counter.decrement, + 'app/notify': actionMap.app.notify, + login: actionMap.login + }); + }); + + it('should use a custom namespace string', () => { + const actionMap = { + APP: { + COUNTER: { + INCREMENT: amount => ({ amount }), + DECREMENT: amount => ({ amount: -amount }) + }, + NOTIFY: (username, message) => ({ message: `${username}: ${message}` }) + }, + LOGIN: username => ({ username }) + }; + + expect(flattenActionMap(actionMap, '-')).to.deep.equal({ + 'APP-COUNTER-INCREMENT': actionMap.APP.COUNTER.INCREMENT, + 'APP-COUNTER-DECREMENT': actionMap.APP.COUNTER.DECREMENT, + 'APP-NOTIFY': actionMap.APP.NOTIFY, + LOGIN: actionMap.LOGIN + }); + }); + }); + + describe('unflattenActionCreators', () => { + it('should unflatten a flattened action map and camel-case keys', () => { + const actionMap = unflattenActionCreators({ + 'APP/COUNTER/INCREMENT': amount => ({ amount }), + 'APP/COUNTER/DECREMENT': amount => ({ amount: -amount }), + 'APP/NOTIFY': (username, message) => ({ message: `${username}: ${message}` }), + LOGIN: username => ({ username }) + }); + + expect(actionMap.login('yangmillstheory')).to.deep.equal({ username: 'yangmillstheory' }); + expect(actionMap.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({ + message: 'yangmillstheory: Hello World' + }); + expect(actionMap.app.counter.increment(100)).to.deep.equal({ amount: 100 }); + expect(actionMap.app.counter.decrement(100)).to.deep.equal({ amount: -100 }); + }); + + it('should unflatten a flattened action map with custom namespace', () => { + const actionMap = unflattenActionCreators({ + 'APP--COUNTER--INCREMENT': amount => ({ amount }), + 'APP--COUNTER--DECREMENT': amount => ({ amount: -amount }), + 'APP--NOTIFY': (username, message) => ({ message: `${username}: ${message}` }), + LOGIN: username => ({ username }) + }, '--'); + + expect(actionMap.login('yangmillstheory')).to.deep.equal({ username: 'yangmillstheory' }); + expect(actionMap.app.notify('yangmillstheory', 'Hello World')).to.deep.equal({ + message: 'yangmillstheory: Hello World' + }); + expect(actionMap.app.counter.increment(100)).to.deep.equal({ amount: 100 }); + expect(actionMap.app.counter.decrement(100)).to.deep.equal({ amount: -100 }); + }); + }); +}); diff --git a/src/arrayToObject.js b/src/arrayToObject.js new file mode 100644 index 00000000..de6c3768 --- /dev/null +++ b/src/arrayToObject.js @@ -0,0 +1,4 @@ +export default (array, callback) => array.reduce( + (partialObject, element) => callback(partialObject, element), + {} +); diff --git a/src/camelCase.js b/src/camelCase.js index d8214897..e07ffcd1 100644 --- a/src/camelCase.js +++ b/src/camelCase.js @@ -11,4 +11,4 @@ function camelCase(string) { , ''); } -export default actionType => actionType.split(namespacer).map(camelCase).join(namespacer); +export default type => type.split(namespacer).map(camelCase).join(namespacer); diff --git a/src/combineActions.js b/src/combineActions.js index 6a6a6df2..1befd71a 100644 --- a/src/combineActions.js +++ b/src/combineActions.js @@ -7,15 +7,15 @@ import invariant from 'invariant'; export const ACTION_TYPE_DELIMITER = '||'; -function isValidActionType(actionType) { - return isString(actionType) || isFunction(actionType) || isSymbol(actionType); +function isValidActionType(type) { + return isString(type) || isFunction(type) || isSymbol(type); } -function isValidActionTypes(actionTypes) { - if (isEmpty(actionTypes)) { +function isValidActionTypes(types) { + if (isEmpty(types)) { return false; } - return actionTypes.every(isValidActionType); + return types.every(isValidActionType); } export default function combineActions(...actionsTypes) { diff --git a/src/createActions.js b/src/createActions.js index 7557cbc0..9ee7f5be 100644 --- a/src/createActions.js +++ b/src/createActions.js @@ -1,52 +1,84 @@ -import identity from 'lodash/identity'; import camelCase from './camelCase'; +import identity from 'lodash/identity'; import isPlainObject from 'lodash/isPlainObject'; import isArray from 'lodash/isArray'; +import last from 'lodash/last'; import isString from 'lodash/isString'; +import defaults from 'lodash/defaults'; import isFunction from 'lodash/isFunction'; import createAction from './createAction'; import invariant from 'invariant'; +import arrayToObject from './arrayToObject'; +import { + defaultNamespace, + flattenActionMap, + unflattenActionCreators +} from './namespaceActions'; -export default function createActions(actionsMap, ...identityActions) { +export default function createActions(actionMap, ...identityActions) { + function getFullOptions() { + const partialOptions = isPlainObject(last(identityActions)) + ? identityActions.pop() + : {}; + return defaults(partialOptions, { namespace: defaultNamespace }); + } + const { namespace } = getFullOptions(); invariant( identityActions.every(isString) && - (isString(actionsMap) || isPlainObject(actionsMap)), + (isString(actionMap) || isPlainObject(actionMap)), 'Expected optional object followed by string action types' ); - if (isString(actionsMap)) { - return fromIdentityActions([actionsMap, ...identityActions]); + if (isString(actionMap)) { + return actionCreatorsFromIdentityActions([actionMap, ...identityActions]); } - return { ...fromActionsMap(actionsMap), ...fromIdentityActions(identityActions) }; + return { + ...actionCreatorsFromActionMap(actionMap, namespace), + ...actionCreatorsFromIdentityActions(identityActions) + }; } -function isValidActionsMapValue(actionsMapValue) { - if (isFunction(actionsMapValue)) { - return true; - } else if (isArray(actionsMapValue)) { - const [payload = identity, meta] = actionsMapValue; +function actionCreatorsFromActionMap(actionMap, namespace) { + const flatActionMap = flattenActionMap(actionMap, namespace); + const flatActionCreators = actionMapToActionCreators(flatActionMap); + return unflattenActionCreators(flatActionCreators, namespace); +} - return isFunction(payload) && isFunction(meta); +function actionMapToActionCreators(actionMap) { + function isValidActionMapValue(actionMapValue) { + if (isFunction(actionMapValue)) { + return true; + } else if (isArray(actionMapValue)) { + const [payload = identity, meta] = actionMapValue; + return isFunction(payload) && isFunction(meta); + } + return false; } - return false; -} -function fromActionsMap(actionsMap) { - return Object.keys(actionsMap).reduce((actionCreatorsMap, type) => { - const actionsMapValue = actionsMap[type]; + return arrayToObject(Object.keys(actionMap), (partialActionCreators, type) => { + const actionMapValue = actionMap[type]; invariant( - isValidActionsMapValue(actionsMapValue), + isValidActionMapValue(actionMapValue), 'Expected function, undefined, or array with payload and meta ' + `functions for ${type}` ); - const actionCreator = isArray(actionsMapValue) - ? createAction(type, ...actionsMapValue) - : createAction(type, actionsMapValue); - return { ...actionCreatorsMap, [camelCase(type)]: actionCreator }; - }, {}); + const actionCreator = isArray(actionMapValue) + ? createAction(type, ...actionMapValue) + : createAction(type, actionMapValue); + return { ...partialActionCreators, [type]: actionCreator }; + }); } -function fromIdentityActions(identityActions) { - return fromActionsMap(identityActions.reduce( - (actionsMap, actionType) => ({ ...actionsMap, [actionType]: identity }) - , {})); +function actionCreatorsFromIdentityActions(identityActions) { + const actionMap = arrayToObject( + identityActions, + (partialActionMap, type) => ({ ...partialActionMap, [type]: identity }) + ); + const actionCreators = actionMapToActionCreators(actionMap); + return arrayToObject( + Object.keys(actionCreators), + (partialActionCreators, type) => ({ + ...partialActionCreators, + [camelCase(type)]: actionCreators[type] + }) + ); } diff --git a/src/handleAction.js b/src/handleAction.js index c74e108e..83456fc6 100644 --- a/src/handleAction.js +++ b/src/handleAction.js @@ -7,11 +7,11 @@ import includes from 'lodash/includes'; import invariant from 'invariant'; import { ACTION_TYPE_DELIMITER } from './combineActions'; -export default function handleAction(actionType, reducer = identity, defaultState) { - const actionTypes = actionType.toString().split(ACTION_TYPE_DELIMITER); +export default function handleAction(type, reducer = identity, defaultState) { + const types = type.toString().split(ACTION_TYPE_DELIMITER); invariant( !isUndefined(defaultState), - `defaultState for reducer handling ${actionTypes.join(', ')} should be defined` + `defaultState for reducer handling ${types.join(', ')} should be defined` ); invariant( isFunction(reducer) || isPlainObject(reducer), @@ -23,8 +23,8 @@ export default function handleAction(actionType, reducer = identity, defaultStat : [reducer.next, reducer.throw].map(aReducer => (isNil(aReducer) ? identity : aReducer)); return (state = defaultState, action) => { - const { type } = action; - if (!type || !includes(actionTypes, type.toString())) { + const { type: actionType } = action; + if (!actionType || !includes(types, actionType.toString())) { return state; } diff --git a/src/namespaceActions.js b/src/namespaceActions.js new file mode 100644 index 00000000..918027d1 --- /dev/null +++ b/src/namespaceActions.js @@ -0,0 +1,57 @@ +import camelCase from './camelCase'; +import isPlainObject from 'lodash/isPlainObject'; + +const defaultNamespace = '/'; + +function flattenActionMap( + actionMap, + namespace = defaultNamespace, + partialFlatActionMap = {}, + partialFlatActionType = '' +) { + function connectNamespace(type) { + return partialFlatActionType + ? `${partialFlatActionType}${namespace}${type}` + : type; + } + + Object.getOwnPropertyNames(actionMap).forEach(type => { + const nextNamespace = connectNamespace(type); + const actionMapValue = actionMap[type]; + + if (!isPlainObject(actionMapValue)) { + partialFlatActionMap[nextNamespace] = actionMap[type]; + } else { + flattenActionMap(actionMap[type], namespace, partialFlatActionMap, nextNamespace); + } + }); + return partialFlatActionMap; +} + +function unflattenActionCreators(flatActionCreators, namespace = defaultNamespace) { + function unflatten( + flatActionType, + partialNestedActionCreators = {}, + partialFlatActionTypePath = [], + ) { + const nextNamespace = camelCase(partialFlatActionTypePath.shift()); + if (partialFlatActionTypePath.length) { + if (!partialNestedActionCreators[nextNamespace]) { + partialNestedActionCreators[nextNamespace] = {}; + } + unflatten( + flatActionType, partialNestedActionCreators[nextNamespace], partialFlatActionTypePath + ); + } else { + partialNestedActionCreators[nextNamespace] = flatActionCreators[flatActionType]; + } + } + + const partialNestedActionCreators = {}; + Object + .getOwnPropertyNames(flatActionCreators) + .forEach(type => unflatten(type, partialNestedActionCreators, type.split(namespace))); + return partialNestedActionCreators; +} + +export { flattenActionMap, unflattenActionCreators };