/* eslint-disable new-cap */
import { flatten } from 'flat';
import {
__,
adjust,
allPass,
always,
any,
anyPass,
complement,
compose,
concat,
cond,
contains,
converge,
curry,
defaultTo,
equals,
filter,
find,
flip,
fromPairs,
gt,
gte,
head,
identity,
ifElse,
isEmpty,
isNil,
join,
keys,
last,
lensIndex,
lensPath,
lt,
lte,
map,
merge,
nthArg,
objOf,
path as rPath,
pathSatisfies,
pick,
pickBy,
pluck,
propEq,
propOr,
reduce,
reject,
replace,
splitEvery,
subtract,
T,
tap,
test,
toLower,
toPairs,
toString,
trim,
type,
unapply,
values,
zipObj,
} from 'ramda';
/**
* @module constants
* @description Functions that always return the same value
*/
/**
* Curried function that takes the string name of a data type per
* [ramda#type]{@link http://ramdajs.com/0.21.0/docs/#type},
* a value of any type, and returns a boolean to indicate
* whether or not the value is of specified type
*
* @function
* @param {String} typeName name of the type to look for
* @param {*} testVal any value of any type
* @return {Boolean} Whether or not the value is of the specified type
*
* @example
* const isString = typeIs('String')
* const isObject = typeIs('Object')
*
* const insistentString = 'i AM a string'
* const obj = { value: insistentString}
*
* isString(insistentString) //=> true
* isString(obj) //=> false
*
* isObject(insistentString) //=> false
* isObject(obj) //=> true
*/
export const typeIs = typeName => compose(equals(typeName), type);
/**
* Returns an empty string
*
* @function
* @return {String} Empty string
* @example
* emptyString() //=> ''
* emptyString('test') //=> ''
* emptyString(true) //=> ''
*/
export const emptyString = always('');
/**
* Returns an empty object
*
* @function
* @return {Object} Empty object
* @example
* emptyObject() //=> {}
* emptyObject('test') //=> {}
* emptyObject(true) //=> {}
*/
export const emptyObject = always({});
/**
* Returns an empty array
*
* @function
* @return {Array} Empty array
* @example
* emptyArray() //=> []
* emptyArray('test') //=> []
* emptyArray(true) //=> []
*/
export const emptyArray = always([]);
// /**
// * @module defaults
// * @description Return default values if the arguments passed to them are undefined or null
// */
export const defaultToEmptyArray = defaultTo([]);
export const defaultToEmptyObject = defaultTo({});
export const defaultToEmptyString = defaultTo('');
// /**
// * @module propertyDefaults
// * @description Return either their first argument or the specified default
// */
export const getPropOrEmptyObjectFunction = propOr(emptyObject);
export const getPropOrEmptyString = propOr('');
/**
* @module argIndex
* @description Return only the specified index of argument array
*/
/**
* Returns the first argument it is invoked with
*
* @function
* @param {...*} args any number of arguments
* @return {*} 0 index of arguments array
*
* @example
* firstArgument(1) //=> 1
* firstArgument(1, 2) //=> 1
* firstArgument(1, 2, 3) //=> 1
*/
export const firstArgument = nthArg(0);
/**
* Returns the second argument it is invoked with
*
* @function
* @param {...*} args any number of arguments
* @return {*} 1 index of arguments array
*
* @example
* secondArgument(1) //=> undefined
* secondArgument(1, 2) //=> 2
* secondArgument(1, 2, 3) //=> 2
*/
export const secondArgument = nthArg(1);
/**
* @module existence
* @description Check various conditions around whether values are falsey or truthy
*/
/**
* Check whether a value of any type is Empty, Null, or undefined
*
* @function
* @param {*} val any value
* @return {Boolean} whether the value is null, undefined, or empty in terms of
* [ramda#empty]{@link http://ramdajs.com/0.21.0/docs/#isEmpty}
*
* @example
* isNilOrEmpty({ test: 'test'}) //=> false
* isNilOrEmpty([1]) //=> false
*
* isNilOrEmpty({}) //=> true
* isNilOrEmpty([]) //=> true
* isNilOrEmpty('') //=> true
* isNilOrEmpty(undefined) //=> true
* isNilOrEmpty(null) //=> true
*/
export const isNilOrEmpty = anyPass([isNil, isEmpty]);
/**
* Checks if value is not empty
*
* @param {*} val any value
* @return {Boolean} true if the value is anything but an empty object or array
*
* isNotEmpty(null) //=> true
* isNotEmpty(undefined) //=> true
*
* isNotEmpty('anything else') //=> true
* isNotEmpty([]) //=> false
* isNotEmpty({}) //=> false
*/
export const isNotEmpty = complement(isEmpty);
/**
* Checks if any value is both
* - not undefined
* - not null
*
* @param {*} val any value
* @return {Boolean} true if the value is niether null or undefined
*
* exists(null) //=> false
* exists(undefined) //=> false
*
* exists('anything else') //=> true
* exists([]) //=> true
* exists({}) //=> true
*/
export const exists = complement(isNil);
/**
* Check if a key exists at a deep path
*
* @function
* @param {String[]} path list of strings used as path to prop
* @param {Object} obj the object to analyze
* @return {Boolean} True if the given object has a key at the given path that is
* niether null or undefined
*
* @example
* const obj = { one: { two: { three: 'here I am' } } }
*
* hasDeep(['one', 'two', 'three'], obj) //=> true
* hasDeep(['one', 'two', 'fish'], obj) //=> false
*/
export const hasDeep = pathSatisfies(exists);
/**
* @module number
* @description Equality and other predicate checks
*/
export const GT = flip(gt);
export const GTE = flip(gte);
export const LT = flip(lt);
export const LTE = flip(gte);
/**
* Takes a low number, and a high number, and a test value and returns true if the
* test value is between the range established by first and second args
*
* @function
* @param {Number} low bottom of range
* @param {Number} high top of range
* @param {Number} test checks whether this val is within range
* @return {Boolean}
*
* @example
* const 1thru10 = between(1, 10);
* const 1thru10(9) //=> true
* const 1thru10(10) //=> true
* const 1thru10(11) //=> false
* const 1thru10(1) //=> true
* const 1thru10(0) //=> false
*/
export const between = curry((l, h, x) => allPass([gte(__, l), lte(__, h)])(x));
const minus = flip(subtract);
const parseInt16 = flip(parseInt)(16);
const maybeShift = ifElse(GT(127), minus(256), identity);
const parseByte = compose(maybeShift, parseInt16);
export const parseHexBinary = compose(map(parseByte), splitEvery(2));
/**
* @module object
* @description Advanced object inspection and property filtering
*/
const pickDeepRaw = (pathToProp, pickList, data) => compose(
objOf(last(pathToProp)),
isEmpty(pickList) ? identity : pick(pickList),
rPath(pathToProp),
)(data);
/**
* Return a whitelisted set of keys from nested object path
*
* @function
* @param {String[]} pathToProp list of strings used as path to prop
* @param {String[]} pickList list of property names to pick
* @param {Object} obj object to pick properties from
* @return {Object} clone of the object at the specified path of the original object
* with all but the specified keys removed
*
* @example
* const obj = {
* one: {
* two: {
* three: {
* animal: {
* type: 'fish',
* name: 'mark',
* game: 'polo',
* },
* },
* },
* },
* }
* const path = ['one', 'two', 'three', 'animal']
* const props = ['name', 'game']
* pickDeep(path, props, obj) //=> { name: 'mark', game: 'polo' }
*/
export const pickDeep = curry(pickDeepRaw);
const mapKeysRaw = (fn, obj) => {
const applyFn = compose(fromPairs, map(adjust(fn, 0)), toPairs);
return applyFn(obj);
};
/**
* Takes a function (g) and an object and returns an object where each key is
* the result of invoking g with that key
*
* @function
* @param {Function} fn function applied to each key
* @param {Object} data object to map keys from
* @return {Object}
*
* @example
* const obj { a: 1, b: 2, c: 3 }
*
* const upper = key => key.toUpperCase()
* const upperCaseKeys = mapKeys(upper)
* const upperCaseKeys(obj)
* //=> { A: 1, B: 2, C: 3 }
*/
export const mapKeys = curry(mapKeysRaw);
const keyContains =
str => compose(contains(str), secondArgument);
const allKeysContainingRaw =
(str, obj) => compose(pickBy(keyContains(str)), flatten)(obj);
/**
* Takes a string and an object, and returns a flattened copy of the object with
* any key in the object that contains the specified string regardless
* of the depth each key is located at within the object
*
* @function
* @param {String} str string to search for in each key
* @param {Object} obj object to search for keys in
* @return {Object} Object with keys that contain the specified string
*
* @example
* const obj = {
* a1: 'something',
* a2: 'something else',
* a3: { b: { dragon: true } }
* }
*
* allKeysContaining('rag', obj) //=> { 'a.b.dragon': true }
*/
export const allKeysContaining = curry(allKeysContainingRaw);
const anyPropSatisfiesRaw =
(predicate, obj) => compose(any(predicate), values)(obj);
/**
* Takes a predicate and an object and returns true if the value of any of
* the objects properties pass the predicate
*
* @function
* @param {Function} predicate pass or fail each key's value
* @param {Object} obj object to analyze
* @return {Boolean} true if any key's value passes the predicate
*/
export const anyPropSatisfies = curry(anyPropSatisfiesRaw);
/**
* Creates a new object with the own properties of the provided object, but the
* keys renamed according to the keysMap object as `{oldKey: newKey}`.
* When some key is not found in the keysMap, then it's passed as-is.
*
* @sig {a: b} -> {a: *} -> {b: *}
*/
export const renameKeys = curry(
(keysMap, obj) => reduce((acc, key) => {
acc[keysMap[key] || key] = obj[key];
return acc;
}, {}, keys(obj))
);
const makeLens = cond([
[typeIs('Number'), lensIndex],
[typeIs('String'), unapply(lensPath)],
[T, always('invalid')],
]);
/**
* Takes a list of string prop names, and returns an object where each key
* is a lens for its respective prop
*
* @function
* @param {string[]} propNames list of property names
* @return {Object} map of lenses
*/
export const makeLenses = converge(zipObj, [identity, map(makeLens)]);
/**
* @module list
* @description Operations on lists of objects
*/
/**
* Takes two lists of values, returns true if all the values
* in the first array are present in the second array
*
* @function
* @param {Array} checkArr list of values to check for
* @param {Array} searchArr list of values to search in
* @return {Boolean}
*
* @example
* const vals = [1, 2, 3]
*
* const containsVals = containsAll(vals)
* containsVals([1, 2, 3, 4, 5]) //=> true
* containsVals([1, 2, 4, 5, 6]) // false
*/
export const containsAll = compose(allPass, map(contains));
/**
* Higher order function to apply a property matching predicate to list
* transformation functions like filter or reject
*
* @function
* @param {Function} func function to apply prop matching predicate to
* @param {String} key key to match with the specified value
* @param {*} val value to match on the specified key
* @param {Object[]} list list against which to apply function
* @return {Object[]} a list derived from original list according to func
*/
const applyByProp = curry(
(func, key, val, list) =>
func(propEq(key, val), defaultToEmptyArray(list)),
);
/**
* Curried function to filter a list of objects according to the value of
* a specified property
*
* @function
* @param {String} key key to match with the specified value
* @param {*} val value to match on the specified key
* @param {Object[]} list against which to apply function
* @return {Object[]} filtered list of objects
*
* @example
* const friends = [
* { name: 'trogdor', type: 'dragon' },
* { name: 'booseph', type: 'dragon' },
* { name: 'kitty', type: 'kitty' },
* ]
*
* filterByProp('type', 'dragon', friends)
* //=> [ { name: 'trogdor', type: 'dragon' }, { name: 'booseph', type: 'dragon' } ]
*/
export const filterByProp = applyByProp(filter);
/**
* Curried function to filter a list of objects by id property
*
* @function
* @param {*} val value to match against id key
* @param {Object[]} list list to filter
* @return {Object[]} filtered list of objects
*
* @example
* const friends = [
* { name: 'trogdor', id: 'dragon' },
* { name: 'booseph', id: 'dragon' },
* { name: 'kitty', id: 'kitty' },
* ]
*
* filterById('dragon', friends)
* //=> [ { name: 'trogdor', id: 'dragon' }, { name: 'booseph', id: 'dragon' } ]
*/
export const filterById = filterByProp('id');
/**
* Curried function to filter a list of objects by name property
*
* @function
* @param {*} val value to match against name key
* @param {Object[]} list list to filter
* @return {Object[]} filtered list of objects
*
* @example
* const friends = [
* { name: 'trogdor', type: 'dragon' },
* { name: 'trogdor', type: 'giant-dragon' },
* { name: 'kitty', type: 'kitty' },
* ]
*
* filterByName('trogdor', friends)
* //=> [ { name: 'trogdor', type: 'dragon' }, { name: 'trogdor', type: 'giant-dragon' } ]
*/
export const filterByName = filterByProp('name');
/**
* Curried function to find the first object in a list where the value of
* a specified property matches the given value
*
* @function
* @param {String} key key to match with the specified value
* @param {*} val value to match on the specified key
* @param {Object[]} list list of objects to search in
* @return {Object} the first object in list where the given property matches the
* given value
*
* @example
* const friends = [
* { name: 'trogdor', name: 'dragon' },
* { name: 'booseph', name: 'dragon' },
* { name: 'kitty', name: 'kitty' },
* ]
*
* findByProp('name', 'trogdor', friends)
* //=> [ { name: 'trogdor', name: 'dragon' } ]
*/
export const findByProp = applyByProp(find);
// lookups for common property names
export const findById = findByProp('id');
export const findByName = findByProp('name');
/**
* Curried function to drop items from a list of objects according to the
* value of a specified property
*
* @function
* @param {String} key key to match with the specified value
* @param {String} val value to match on the specified key
* @return {Function}
*/
export const dropByProp = applyByProp(reject);
// rejectors for common property names
export const dropById = dropByProp('id');
export const dropByName = dropByProp('name');
const mergeListsByPropRaw = (prop, source, search) => {
const buildPredicate = compose(anyPass, map(propEq(prop)), pluck(prop));
const predicate = buildPredicate(source);
const matches = filter(predicate, search);
const mergeWithMatch = x => merge(x, findByProp(prop, x[prop], matches));
const mergeElement = ifElse(predicate, mergeWithMatch, always(undefined));
return map(mergeElement, source);
};
/**
* Takes a property name, a source list and a search list, returns the result of merging each
* element from the source list with the first object from the search list where the value
* of the given property is equal
*
* @function
* @param {String} prop name of property merge by
* @param {Object[]} source array to search for matches in
* @param {Object[]} search array to project result from
* @return {Object[] list of objects that contain all properties from each list
* where the given property was equal
*
* @example
* const sourceArr = [{ id: 1, likes: 'gibbons' }, { id: 3, likes: 'pasta' }]
* const searchArr = [
* { id: 1, firstName: 'Bob', lastName: 'Franklin' },
* { id: 2, firstName: 'Rob', lastName: 'Lob' },
* { id: 3, firstName: 'Tob', lastName: 'Lob' },
* ]
*
* mergeListsByProp('id', sourceArr, searchArr)
* //=> [
* // { id: 1, likes: 'gibbons', firstName: 'Bob', lastName: 'Franklin' },
* // { id: 3, likes: 'pasta', firstName: 'Tob', lastName: 'Lob' },
* //]
*/
export const mergeListsByProp = curry(mergeListsByPropRaw);
/**
* @module string
* @description Helpers for common string manipulation
*/
const regex = x => new RegExp(x);
const mapTests = map(compose(test, regex));
/**
* Takes a list of strings, and returns an object where each string is used
* as a key to expose a simple regex pattern
*
* @function
* @param {string[]} tests strings to base simple regex patterns on
* @return {Object}
*
* @example
* const patterns = makeRegexs(['a', 'b'])
* patterns.b('best') //=> true
* patterns.b('rest') //=> false
*/
export const makeRegexs = converge(zipObj, [identity, mapTests]);
const headOfSecondArg = (char, arr) => head(arr);
/**
* Returns a curried function that checks its first argument matches the first
* character of its second argument
*
* @function
* @param {String} matchString string to match against
* @param {String} testString string to test
* @return {Boolean} true if the first character of testString is
* equal to the matchString
*
* @example
*
* const one = 'one'
* firstCharIs('o', one) //=> true
* firstCharIs('t', one) //=> false
*
* const two = 'two'
* firstCharIs('t', two) //=> true
* firstCharIs('o', two) //=> false
*
*/
export const firstCharIs = converge(equals, [identity, headOfSecondArg]);
/**
* Produces a new string by adding the first argument to the end of the second
*
* @function
* @param {String} a string to add
* @param {String} b stirng to begin with
* @return {String} concatenated result
*
* @example
* const insultSomeone = appendStr(' are bad at golf')
*
* insultSomeone('you')
* //=> 'you are bad at golf'
*/
export const appendStr = flip(concat);
const processSnakeCaps = replace(/([a-z\d])([A-Z]+)/g, '$1_$2');
const insertUnderscores = replace(/[-\s]+/g, '_');
/**
* Converts text to snake case
*
* @function
* @param {String} str string to convert
* @return {String} snkae cased string
*
* @example
* snakeify('MozTransform')
* // => 'moz_transform'
*/
export const snakeify = compose(
toLower,
insertUnderscores,
processSnakeCaps,
trim,
);
const capitalize = (match, c) => (c ? c.toUpperCase() : '');
const processCamelCaps = replace(/[-_\s]+(.)?/g, capitalize);
/**
* Converts text to camel case
*
* @function
* @param {String} str string to convert
* @return {String} camel cased string
*
* @example
* camelize('moz_transform')
* // => 'MozTransform'
*/
export const camelize = compose(processCamelCaps, trim);
const insertCommaEveryThree = replace(/\B(?=(\d{3})+(?!\d))/g, ',');
const convertToString = ifElse(typeIs('String'), identity, toString);
/**
* Takes a string or integer and returns a stringified version with comma insertion
*
* @function
* @param {(String|Number)} val number to format
* @return {String} formatted string
*
* @example
* insertCommasInNumber('20000') //=> '20,000'
* insertCommasInNumber(2000) //=> '2,000'
* insertCommasInNumber(200) //=> '200'
*/
export const insertCommasInNumber = compose(
insertCommaEveryThree,
convertToString,
);
const joinParamPairs = map(join('='));
const joinParamSets = join('&');
/**
* Takes an array of tuples where the first element is a param name and the
* second element is a param value, returns a standard url querystring
*
* @function
* @param {Array[]} tuples pairs of param names and values
* @return {String} standard url querystring
*
* @example
* const tuples = [
* ['param1', 'value1'],
* ['param2', 'value2'],
* ]
*
* buildQueryString(tuples) = //=> 'param1=value1¶m2=value'
*/
export const buildQueryString = compose(joinParamSets, joinParamPairs);
/**
* @module debugging
* @description Helpers for debugging functional js
*/
/**
* Simple logger for debugging function composition without breaking data flow
*
* @function
* @param {*} val any value
*/
export const check = tap(console.log);
/**
* Logger for debugging function composition without breaking data flow,
* pretty-priints objects and arrays
*
* @function
* @param {*} val any value
*/
export const prettyCheck = tap(val => console.log(JSON.stringify(val, null, 2)));
export default {
typeIs,
buildQueryString,
emptyString,
emptyObject,
emptyArray,
defaultToEmptyArray,
defaultToEmptyObject,
defaultToEmptyString,
getPropOrEmptyObjectFunction,
getPropOrEmptyString,
firstArgument,
secondArgument,
isNilOrEmpty,
exists,
hasDeep,
pickDeep,
mapKeys,
allKeysContaining,
anyPropSatisfies,
renameKeys,
filterByProp,
filterById,
filterByName,
findByProp,
findById,
findByName,
dropByProp,
dropById,
dropByName,
mergeListsByProp,
snakeify,
camelize,
insertCommasInNumber,
check,
};