Source: state.js

// 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;