Source: selectors.js

// external dependencies
import get from 'lodash/get';
import isArray from 'lodash/isArray';
import isFunction from 'lodash/isFunction';
import isPlainObject from 'lodash/isPlainObject';
import {
  createSelector as createReselectSelector,
  createSelectorCreator,
} from 'reselect';

// utils
import {testParameter} from './utils';

// constants
import {ERROR_TYPES} from './constants';

/**
 * @module selectors
 */

/**
 * @private
 *
 * @function createIdentitySelector
 *
 * @description
 * create selector to retrieve identity based on deeply-nested values
 *
 * @param {function|string} property property string to convert to nested path
 * @returns {function(Object): *}
 */
export const createIdentitySelector = (property) => {
  if (isFunction(property)) {
    return property;
  }

  return (passedState) => get(passedState, property);
};

/**
 * @private
 *
 * @function getIdentityValue
 *
 * @description
 * pass-through function to return the value passed to it
 *
 * @param {*} value value to pass through
 * @returns {*}
 */
export const getIdentityValue = (value) => value;

/**
 * @private
 *
 * @function getSelectorGenerator
 *
 * @description
 * get the generator for the selector based on the customMemozer being a function or not
 *
 * @param {function} customMemoizer memoizer function to use instead of the default
 * @param {Object} options additional options to use when creating the selector generator
 * @returns {function}
 */
export const getSelectorGenerator = (customMemoizer, options) =>
  isFunction(customMemoizer) ? createSelectorCreator(customMemoizer, ...options) : createReselectSelector;

/**
 * @private
 *
 * @function getStructuredValue
 *
 * @description
 * build a structured value to return for structured selectors
 *
 * @param {Array<string>} keys array of keys to use for values in structured selector
 * @returns {function(Array<*>): Object}
 */
export const getStructuredValue = (keys) => (...values) =>
  keys.reduce((structuredValue, key, keyIndex) => {
    structuredValue[key] = values[keyIndex];

    return structuredValue;
  }, {});

/**
 * @private
 *
 * @function getStandardSelector
 *
 * @description
 * get the standard selector type (single value)
 *
 * @param {Array<string>} paths array of strings denoting nested paths of values in state
 * @param {function} selectorGenerator method to use for generating selector
 * @param {function} getValue method to use for computing the value to return
 * @returns {function}
 */
export const getStandardSelector = (paths, selectorGenerator, getValue) => {
  const selectors = paths.map(createIdentitySelector);

  return selectorGenerator(selectors, getValue);
};

/* eslint-disable valid-jsdoc */
/**
 * @private
 *
 * @function getStructuredSelector
 *
 * @description
 * get the structured selector based on the properties passed
 *
 * @param {Array<string>} keys array of keys to use for values in structured selector
 * @param {Array<string>} paths array of strings denoting nested paths to use for values in structured selector
 * @param {function} selectorGenerator method to use for generating selector
 * @returns {function}
 */
/* eslint-enable */
export const getStructuredSelector = ({keys, paths}, selectorGenerator) => {
  if (keys.length !== paths.length) {
    throw new ReferenceError('Keys and properties arrays must be the same length.');
  }

  const selectors = paths.map(createIdentitySelector);

  return selectorGenerator(selectors, getStructuredValue(keys));
};

/**
 * @function createSelector
 *
 * @description
 * based on the array of properties and the reducer passed
 * create a selector
 *
 * @example
 * import {
 *  createSelector
 * } from 'arco';
 *
 * const hasBaz = createSelector(['foo.bar[0].baz'], (baz) => {
 *  return !!baz;
 * });
 *
 * hasBaz({foo: {bar: [{ baz: 'Here!'}]}}); // true
 * hasBaz({foo: {bar: [{ baz: 'Here!'}]}}); // true, pulled from cache
 *
 * @param {Array<string>|{keys: Array<string>, paths: Array<string>}} properties properties to retrieve from state
 * @param {function} [getComputedValue=getIdentityValue] method for getting the computed value from the properties
 * @param {function} [customMemoizer=null] custom memoizer function to use in place of the default
 * @param {Object} [customMemoizerOptions={}] additional options for using the custom memoizer option
 * @returns {function}
 */
export const createSelector = (
  properties = [],
  getComputedValue = getIdentityValue,
  customMemoizer = null,
  customMemoizerOptions = {}
) => {
  const selectorGenerator = getSelectorGenerator(customMemoizer, customMemoizerOptions);

  if (isPlainObject(properties)) {
    return getStructuredSelector(properties, selectorGenerator);
  }

  testParameter(
    properties,
    isArray,
    'Properties passed must be either an object of keys and paths or an array of paths.',
    ERROR_TYPES.TYPE
  );
  testParameter(getComputedValue, isFunction, 'Computed value passed must be a function.', ERROR_TYPES.TYPE);

  return getStandardSelector(properties, selectorGenerator, getComputedValue);
};

export default createSelector;