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