const copyArrayWith = (key, value) => obj => { const result = obj == null ? [] : [...obj]; result[key] = value; return result; }; const copyMapWith = (key, value) => obj => { const result = obj == null ? new Map() : new Map(obj); result.set(key, value); return result; }; const copyObjectWith = (key, value) => obj => { const result = obj == null ? {} : {...obj}; result[key] = value; return result; }; const isMapKey = key => typeof key === 'object' && key.isKeyInMap === true; export const keyInMap = key => ({isKeyInMap: true, key}); export const kim = keyInMap; const copyWith = (key, value) => typeof key === 'number' ? copyArrayWith(key, value) : isMapKey(key) ? copyMapWith(key.key, value) : copyObjectWith(key, value); function parseKeysInto (keyDescriptions, keys) { keyDescriptions.forEach(each => { if (typeof each === 'number' || isMapKey(each)) keys.push(each); else if (Array.isArray(each)) parseKeysInto(each, keys); else keys.push(...each.toString().split('.')); }); return keys; } const getKey = (x, key) => x == null ? undefined : isMapKey(key) ? x.get(key.key) : x[key]; const getKeyWithCheck = (x, key) => x == null ? null : isMapKey(key) ? x.has(key.key) ? {value: x.get(key.key)} : null : x.hasOwnProperty(key) ? {value: x[key]} : null; const chain = list => step => stop => (function worker (index) { return index >= list.length ? stop : step(list[index], worker(index + 1)); })(0); const id = x => x; const modifier = (key, value, valueModifier) => { const modified = valueModifier(value); return value === modified ? id : copyWith(key, modified); }; const atPathBeginningWith = prefix => (...keyDescriptions) => { const keys = parseKeysInto(keyDescriptions, [...prefix]), atCurrentPath = atPathBeginningWith(keys), keyChain = chain(keys), putChain = keyChain((key, next) => x => modifier(key, getKey(x, key), next)(x)), mapChain = keyChain((key, next) => x => { const valueWithCheck = getKeyWithCheck(x, key); if (valueWithCheck == null) return x; return modifier(key, valueWithCheck.value, next)(x); }); return Object.assign(atCurrentPath, { get: obj => keys.reduce(getKey, obj), put: val => putChain(() => val), map: mapChain }); }; export const atPath = atPathBeginningWith([]); export const pathWorkshop = (keys, fn = x => x) => (obj, {result = obj, resultKeys = keys, diff} = {}) => { keys.forEach((key, index) => { const value = fn(atPath(key).get(obj)); if (typeof value === "undefined") return; const modifier = atPath(resultKeys[index]).put(value); const oldResult = result; result = modifier(oldResult); if (result !== oldResult) { diff = modifier(diff); } }); return {result, diff}; };