// external dependencies
import isFunction from 'lodash/isFunction';
import isPlainObject from 'lodash/isPlainObject';
import isString from 'lodash/isString';
import noop from 'lodash/noop';
import {
createAction as createReduxAction,
handleActions,
} from 'redux-actions';
// selectors
import {getIdentityValue} from './selectors';
// constants
import {
ERROR_TYPES,
STATUS,
} from './constants';
// utils
import {
testMetaHandler,
testParameter,
testReducerHandler,
} from './utils';
let moduleCache = {};
/**
* @module state
*/
/**
* @private
*
* @function asyncActionStatusCreator
*
* @description
* create action that will dispatch with the error status as meta
*
* @param {string} status status to provide for the action
* @returns {function(): {status: string}}
*/
export const asyncActionStatusCreator = (status) => () => ({
status,
});
/**
* @private
*
* @function createNamespacedName
*
* @description
* create the namespaced version of the action name
*
* @param {string} namespace namespace of the module
* @param {string} name name of the action
* @returns {string}
*/
export const createNamespacedName = (namespace, name) => `${namespace}/${name}`;
/**
* @private
*
* @function getCreateAction
*
* @description
* get the create action creator for a given namespace
*
* @param {string} namespace namespace action will reside in
* @returns {function}
*/
export const getCreateAction = (namespace) =>
/**
* @function createAction
*
* @description
* action creator helper that will return the redux-actions
* action build based on its parameters
*
* @example
* import {
* createModule
* } from 'arco';
*
* const module = createModule('foo');
*
* const action = module.createAction('SET_NAME');
*
* @param {string} name name of the action
* @param {function} [payloadCreator=getIdentityValue] method to handle the passing of the payload
* @param {function|null} [metaCreator=null] method to handle any additional metadata
* @returns {function}
*/
(name, payloadCreator = getIdentityValue, metaCreator = null) => {
testParameter(name, isString, 'Name of action must be a string.', ERROR_TYPES.TYPE);
testParameter(payloadCreator, isFunction, 'Payload handler must be a function.', ERROR_TYPES.TYPE);
testParameter(payloadCreator, isFunction, 'Payload handler must be a function.', ERROR_TYPES.TYPE);
testParameter(metaCreator, testMetaHandler, 'meta handler must be a function.', ERROR_TYPES.TYPE);
const constantName = createNamespacedName(namespace, name);
const action = createReduxAction(constantName, payloadCreator, metaCreator);
moduleCache[namespace].actions[name] = {
action,
constantName,
};
return action;
};
/**
* @private
*
* @function getCreateAsyncAction
*
* @description
* get the create async action creator for a given namespace
*
* @param {string} namespace namespace action will reside in
* @returns {function}
*/
export const getCreateAsyncAction = (namespace) => {
const createAction = getCreateAction(namespace);
/**
* @function createAsyncAction
*
* @description
* async action creator helper that creates a distinct action for each
* status with the status passed via payload, and injects the functions
* for each
*
* @example
* import {
* createModule,
* get
* } from 'arco';
*
* const module = createModule('foo');
*
* const action = module.createAsyncAction('GET_STUFF', (lifecycle, otherData) => {
* const {
* onError,
* onRequest,
* onSuccess
* } = lifecycle;
*
* return (dispatch) => {
* dispatch(onRequest(otherData));
* // otherData is passed as payload, PENDING is passed as status in meta
*
* return get('/foo')
* .then((data) => {
* dispatch(onSuccess(data));
* // data is passed as payload, SUCCESS is passed as status in meta
* })
* .catch((error) => {
* dispatch(onError(error));
* // error is passed as payload, ERROR is passed as status in meta
* });
* };
* });
*
* @param {string} name name of the action
* @param {function} payloadHandler method to handle the passing of the payload
* @returns {function}
*/
return (name, payloadHandler) => {
testParameter(name, isString, 'Name of action must be a string.', ERROR_TYPES.TYPE);
testParameter(payloadHandler, isFunction, 'Payload handler must be a function.', ERROR_TYPES.TYPE);
const onError = createAction(name, getIdentityValue, asyncActionStatusCreator(STATUS.ERROR));
const onRequest = createAction(name, getIdentityValue, asyncActionStatusCreator(STATUS.PENDING));
const onSuccess = createAction(name, getIdentityValue, asyncActionStatusCreator(STATUS.SUCCESS));
const lifecycle = {
onError,
onRequest,
onSuccess,
};
const action = (...args) => payloadHandler(lifecycle, ...args);
//in case you want different handlers in the reducer for each action status
action.onError = onError;
action.onRequest = onRequest;
action.onSuccess = onSuccess;
const actionName = createNamespacedName(namespace, name);
/**
* set the toString to return the name passed, so it will work
* with createReducer
*
* @returns {string}
*/
action.toString = () => actionName;
moduleCache[namespace].actions[name].action = action;
return action;
};
};
/**
* @private
*
* @function getCreateReducer
*
* @description
* get the reducer creator for a given namespace
*
* @param {string} namespace namespace reducer will reside in
* @returns {function}
*/
export const getCreateReducer = (namespace) =>
/**
* @function createReducer
*
* @description
* reducer creator that will accept the initialState and the handler of that
* function, either as standard function or as redux-actions map
*
* @example
* import {
* getCreateReducer
* } from 'arco';
*
* import module, {
* setName
* } from './actions';
*
* const INITIAL_STATE = {
* name: ''
* };
*
* const createReducer = getCreateReducer('namespace');
*
* // use the handleActions method from redux-actions
* createReducer(INITIAL_STATE, (state, {
* [setName](state, {payload}) {
* return {
* ...state,
* name: payload
* };
* }
* });
*
* // or use the traditional reducer function method, which requires converting the actions toString
* createReducer(INITIAL_STATE, (state, {payload, type}) => {
* switch (type) {
* case `${setName}`:
* return {
* ...state,
* name: payload
* };
*
* default:
* return state;
* }
* });
*
* @param {Object} initialState initial state to hydrate store with
* @param {function} handler method to handle state updates
* @returns {function}
*/
(initialState, handler) => {
let reducer;
testParameter(handler, testReducerHandler, 'Reducer must either be an object or a function.', ERROR_TYPES.TYPE);
if (isFunction(handler)) {
reducer = (state = initialState, action) => handler(state, action);
} else if (isPlainObject(handler)) {
reducer = handleActions(handler, initialState);
}
moduleCache[namespace].reducer = reducer;
reducer.namespace = namespace;
return reducer;
};
/**
* @function createModule
*
* @description
* create a module which has actions and a reducer, and has create methods for them
*
* @example
* import {
* createModule
* } from 'arco';
*
* const appModule = createModule('app');
*
* @param {string} namespace namespace for the module
* @returns {Object}
*/
export const createModule = (namespace) => {
testParameter(namespace, isString, 'Namespace provided must be a string.', ERROR_TYPES.TYPE);
if (moduleCache[namespace]) {
throw new ReferenceError(`Namespace ${namespace} is already in use.`, ERROR_TYPES.REFERENCE);
}
moduleCache[namespace] = {
actions: {},
reducer: noop,
};
const createAction = getCreateAction(namespace);
const createAsyncAction = getCreateAsyncAction();
const createReducer = getCreateReducer(namespace);
return {
createAction,
createAsyncAction,
createReducer,
namespace,
};
};
/**
* @function getModules
*
* @description
* get the module for the given namespace, or all modules if none
*
* @example
* import {
* getModules
* } from 'arco';
*
* const allModules = getModules();
* const appModule = getModules('app');
*
* @param {string} namespace namespace of module to retrieve
* @returns {Object}
*/
export const getModules = (namespace) => {
if (isString(namespace)) {
return moduleCache[namespace];
}
return moduleCache;
};
export default createModule;