UNPKG

54.8 kBJavaScriptView Raw
1/**
2 * Created by Andy Likuski on 2017.02.26
3 * Copyright (c) 2017 Andy Likuski
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 *
7 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 *
11 */
12
13/*
14* Utility functions, which rely heavily on Ramda, data.task, and Ramda Fantasy types
15* These functions should all be pure. Equivalent throwing functions can
16* be found in throwingFunctions. throwingFunctions should only be used for coding errors,
17* such as improper function arguments or an invalid path within object.
18* I/O Errors should be handled using Maybe or Result and calling functions should expect them.
19* An I/O Error should always result in a valid state of an application, even if that valid
20* state is to show an error message, retryTask I/O, etc.
21*/
22
23import * as R from 'ramda';
24import * as Rm from 'ramda-maybe';
25import * as Result from 'folktale/result';
26
27// https://stackoverflow.com/questions/17843691/javascript-regex-to-match-a-regex
28const regexToMatchARegex = /\/((?![*+?])(?:[^\r\n\[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*\])+)\/((?:g(?:im?|mi?)?|i(?:gm?|mg?)?|m(?:gi?|ig?)?)?)/;
29
30
31/**
32 * We use this instead of isObject since it's possible to have something that is an object without
33 * a prototype to make it an object type
34 * @param {*} obj value to test
35 * @return {boolean} True if R.is(Object, obj) or is not null and it a typeof 'object'
36 */
37export const isObject = obj => {
38 return R.is(Object, obj) || (obj !== null && typeof obj === 'object');
39};
40
41/**
42 * Return an empty string if the given entity is falsy
43 * @param {Object} entity The entity to check
44 * @returns {String|Object} The empty string or the Object
45 * @sig orEmpty:: a -> a
46 * :: a -> String
47 */
48export const orEmpty = entity => entity || '';
49
50/**
51 * Removed null or undefined items from an iterable
52 * @param [a] items Items that might have falsy values to remove
53 * @returns The compacted items
54 * @sig compact:: [a] -> [a]
55 * @sig compact:: {k,v} -> {k,v}
56 */
57export const compact = R.reject(R.isNil);
58
59/**
60 * Remove empty strings
61 * @param [a] items Items that might have empty or null strings to remove
62 * @returns The compacted items
63 * @sig compactEmpty:: [a] -> [a]
64 */
65export const compactEmpty = R.reject(R.either(R.isNil, R.isEmpty));
66
67/**
68 * Convert an empty value to null
69 * @param a item Item that might be empty according to isEmpty
70 * @returns the
71 * @sig emptyToNull:: a -> a
72 * :: a -> null
73 */
74export const emptyToNull = R.when(R.isEmpty, () => null);
75
76/**
77 * Join elements, first remove null items and empty strings. If the result is empty make it null
78 * @param connector Join connector string
79 * @param [a] items Items to join that might be removed
80 * @sig compactEmpty:: [a] -> String
81 * :: [a] -> null
82 */
83export const compactJoin = R.compose(
84 (connector, items) => R.pipe(compactEmpty, R.join(connector), emptyToNull)(items)
85);
86
87/**
88 * Creates a partial mapping function that expects an iterable and maps each item of the iterable to the given property
89 * @param {String} prop The prop of each object to map
90 * @param {[Object]} items The objects to map
91 * @returns {[Object]} The mapped objects
92 * @sig mapProp :: String -> [{k, v}] -> [a]
93 */
94export const mapProp = R.curry((prop, objs) => R.pipe(R.prop, R.map)(prop)(objs));
95
96/**
97 * Creates a partial function that maps an array of objects to an object keyed by the given prop of the object's
98 * of the array, valued by the item. If the item is not an array, it leaves it alone, assuming it is already indexed
99 * @param {String} prop The prop of each object to use as the key
100 * @param {[Object]} items The items to map
101 * @returns {Object} The returned object
102 * @sig mapPropValueAsIndex:: String -> [{k, v}] -> {j, {k, v}}
103 * @sig mapPropValueAsIndex:: String -> {k, v} -> {k, v}
104 */
105export const mapPropValueAsIndex = R.curry((prop, obj) =>
106 R.when(
107 Array.isArray,
108 R.pipe(R.prop, R.indexBy)(prop)
109 )(obj));
110
111/**
112 * Merges a list of objects by the given key and returns the values, meaning all items that are
113 * duplicate prop key value are removed from the final list
114 * @returns {[Object]} The items without the duplicates
115 * @sig removeDuplicateObjectsByProp:: String -> [{k, v}] -> [{k, v}]
116 */
117export const removeDuplicateObjectsByProp = R.curry((prop, list) =>
118 R.pipe(
119 mapPropValueAsIndex(prop),
120 R.values
121 )(list)
122);
123
124/**
125 * Returns the id of the given value if it is an object or the value itself if it is not.
126 * @param {Object|String|Number} objOrId
127 * @returns {String|Number} an id
128 * @sig idOrIdFromObj:: a -> a
129 * :: {k, v} -> a
130 */
131export const idOrIdFromObj = R.when(
132 objOrId => (typeof objOrId === 'object') && objOrId !== null,
133 R.prop('id')
134);
135
136/**
137 * Deep merge values that are objects but not arrays
138 * based on https://github.com/ramda/ramda/pull/1088
139 * @params {Object} l the 'left' side object to merge
140 * @params {Object} r the 'right' side object to merge
141 * @type {Immutable.Map<string, V>|__Cursor.Cursor|List<T>|Map<K, V>|*}
142 * @returns {Object} The deep-merged object
143 * @sig mergeDeep:: (<k, v>, <k, v>) -> <k, v>
144 */
145export const mergeDeep = R.mergeWith((l, r) => {
146 // If either (hopefully both) items are arrays or not both objects
147 // accept the right value
148 return (
149 // If either is a function take the last
150 (R.is(Function, l) || R.is(Function, r)) ||
151 // If either is an array take the last
152 (l && l.concat && Array.isArray(l)) ||
153 (r && r.concat && Array.isArray(r))) ||
154 !(R.is(Object, l) && R.is(Object, r)) ?
155 r :
156 mergeDeep(l, r); // tail recursive
157});
158
159/**
160 * mergeDeep any number of objects
161 * @params {[Object]} objs Array of objects to reduce
162 * @returns {Object} The deep-merged objects
163 */
164export const mergeDeepAll = R.reduce(mergeDeep, {});
165
166/**
167 * Deep merge values with a custom function that are objects
168 * based on https://github.com/ramda/ramda/pull/1088
169 * @params {Function} fn The merge function left l, right r:: l -> r -> a
170 * @params {Object} left the 'left' side object to merge
171 * @params {Object} right the 'right' side object to morge
172 * @returns {Object} The deep-merged objeck
173 * @sig mergeDeep:: (<k, v>, <k, v>) -> <k, v>
174 */
175export const mergeDeepWith = R.curry((fn, left, right) => R.mergeWith((l, r) => {
176 // If either (hopefully both) items are arrays or not both objects
177 // accept the right value
178 return (
179 (l && l.concat && Array.isArray(l)) ||
180 (r && r.concat && Array.isArray(r))
181 ) ||
182 !(R.all(isObject))([l, r]) ||
183 R.any(R.is(Function))([l, r]) ?
184 fn(l, r) :
185 mergeDeepWith(fn, l, r); // tail recursive
186})(left, right));
187
188/**
189 * Merge Deep that concats arrays of matching keys
190 * @params {Object} left the 'left' side object to merge
191 * @params {Object} right the 'right' side object to morge
192 * @returns {Object} The deep-merged object
193 * @sig mergeDeep:: (<k, v>, <k, v>) -> <k, v>
194 */
195export const mergeDeepWithConcatArrays = R.curry((left, right) => mergeDeepWith((l, r) => {
196 return R.cond(
197 [
198 [R.all(R.allPass([R.identity, R.prop('concat'), Array.isArray])), R.apply(R.concat)],
199 [R.complement(R.all)(isObject), R.last],
200 [R.T, R.apply(mergeDeepWithConcatArrays)] // tail recursive
201 ]
202 )([l, r]);
203})(left, right));
204
205/**
206 * Merge Deep and also apply the given function to array items with the same index
207 * @params {Function} fn The merge function left l, right r, string k:: l -> r -> k -> a
208 * @params {Object} left the 'left' side object to merge
209 * @params {Object} right the 'right' side object to morge
210 * @returns {Object} The deep-merged object
211 * @sig mergeDeepWithRecurseArrayItems:: (<k, v>, <k, v>, k) -> <k, v>
212 */
213export const mergeDeepWithRecurseArrayItems = R.curry((fn, left, right) => R.cond(
214 [
215 // Arrays
216 [R.all(R.allPass([R.identity, R.prop('concat'), Array.isArray])),
217 ([l, r]) => {
218 return R.zipWith((a, b) => mergeDeepWithRecurseArrayItems(fn, a, b), l, r);
219 }
220 ],
221 // Primitives
222 [R.complement(R.all)(isObject),
223 ([l, r]) => {
224 return fn(l, r);
225 }],
226 // Objects
227 [R.T, ([l, r]) => {
228 return R.mergeWith(mergeDeepWithRecurseArrayItems(fn), l, r);
229 }]
230 ]
231 )([left, right])
232);
233
234/**
235 * Like mergeDeepWithRecurseArrayItems but merges array items with a function itemMatchBy that determines
236 * if an item from each array of left and right represents the same objects. The right object's array
237 * items are returned but any left object item matching by itemMatchBy is deep merged with the matching right item.
238 * There is no merge function for primitives, r is always returned
239 * @params {Function} fn The item matching function, Arrays deep items in left and right. Called with the
240 * item and current key/index
241 * are merged. For example
242 * item => R.when(isObject, R.propOr(v, 'id'))(item)
243 * would match on id if item is an object and has an id
244 * @params {Object} left the 'left' side object to merge
245 * @params {Object} right the 'right' side object to merge
246 * @params {String} [key] Optional key or index of the parent object/array item
247 * @returns {Object} The deep-merged object
248 */
249export const mergeDeepWithRecurseArrayItemsByRight = R.curry((itemMatchBy, left, right) => {
250 return _mergeDeepWithRecurseArrayItemsByRight(itemMatchBy, null, left, right, null);
251});
252
253/**
254 * Like mergeDeepWithRecurseArrayItemsByRight but takes mergeObject to merge objects, rather than just taking the
255 * right value. Primitives still resolve to the right value. the mergeObject function should handle internal array
256 * merging by recursing on this function or similar
257 * @params {Function} fn The item matching function, Arrays deep items in left and right. Called with the
258 * item and current key/index
259 * are merged. For example
260 * item => R.when(isObject, R.propOr(v, 'id'))(item)
261 * would match on id if item is an object and has an id
262 * @params {Function} itemMatchBy Expects the left and right object that need to be merged
263 * @params {Function} mergeObject Expects left and right when they are objects. mergeObject typically recurses
264 * with mergeDeepWithRecurseArrayItemsByAndMergeObjectByRight on each item of left and right after doing something
265 * special. This function was designed with the idea of being used in Apollo InMemory Cache Type Policy merge functions
266 * to continue calling Apollowing merge function on objects, which in tern delegates back to this function
267 * @params {Object} left the 'left' side object to merge
268 * @params {Object} right the 'right' side object to merge
269 * @params {String} [key] Optional key or index of the parent object/array item
270 * @returns {Object} The deep-merged object
271 */
272export const mergeDeepWithRecurseArrayItemsByAndMergeObjectByRight = R.curry((itemMatchBy, mergeObject, left, right) => {
273 return _mergeDeepWithRecurseArrayItemsByRight(itemMatchBy, mergeObject, left, right, null);
274});
275
276export const _mergeDeepWithRecurseArrayItemsByRight = (itemMatchBy, mergeObject, left, right, key) => {
277 return R.cond(
278 [
279 // Arrays
280 [([l, r]) => {
281 return R.any(R.allPass([R.identity, R.prop('concat'), Array.isArray]))([l, r]);
282 },
283 ([l, r]) => {
284 // Null case, return the only array
285 if (!l || !r) {
286 return l || r;
287 }
288 // Create a lookup of l items
289 const lItemsByValue = R.indexBy(li => itemMatchBy(li, key), l || []);
290 // Map each item of r
291 return R.addIndex(R.map)(
292 (rItem, i) => {
293 // If the lookup of the r item matches one of l items' itemMatchBy value,
294 // recurse with both items. Else just return r
295 const rItemValue = itemMatchBy(rItem, key);
296 const hasMatchingLItem = R.has(rItemValue, lItemsByValue);
297 return R.when(
298 () => hasMatchingLItem,
299 () => {
300 // Pass the index as a key
301 return _mergeDeepWithRecurseArrayItemsByRight(
302 itemMatchBy,
303 mergeObject,
304 R.prop(rItemValue, lItemsByValue),
305 rItem,
306 i
307 );
308 }
309 )(rItem);
310 },
311 r || []
312 );
313 }
314 ],
315 // Primitives. If either is an object skip, it means 1 is null
316 [R.none(isObject),
317 ([l, r]) => {
318 return r;
319 }
320 ],
321 // Objects and nulls
322 [R.T,
323 ([l, r]) => {
324 return mergeObject ? mergeObject(l, r) : R.mergeWithKey(
325 (kk, ll, rr) => {
326 return _mergeDeepWithRecurseArrayItemsByRight(
327 itemMatchBy,
328 mergeObject,
329 ll,
330 rr,
331 kk
332 );
333 },
334 l || {},
335 r || {}
336 );
337 }
338 ]
339 ]
340 )([left, right]);
341};
342
343
344/**
345 * mergeDeepWithRecurseArrayItems but passes obj as left and right so fn is called on every key
346 * @params {Function} fn The merge function left l, right r, string k:: l -> r -> k -> a
347 * @params {Object} left the 'left' side object to merge
348 * @params {Object} right the 'right' side object to morge
349 * @returns {Object} The deep-merged object
350 * @sig mergeDeepWithRecurseArrayItems:: (<k, v>, <k, v>, k) -> <k, v>
351 */
352export const applyDeep = R.curry((fn, obj) => mergeDeepWithRecurseArrayItems(fn, obj, obj));
353
354
355/**
356 * Merge Deep and also apply the given function to array items with the same index.
357 * This adds another function that maps the object results to something else after the objects are recursed upon
358 * @params {Function} fn The merge function left l, right r, string k:: l -> r -> k -> a
359 * @params {Function} applyObj Function called with the current key and the result of each recursion that is an object.
360 * @params {Object} left the 'left' side object to merge
361 * @params {Object} right the 'right' side object to morge
362 * @returns {Object} The deep-merged object
363 * @sig mergeDeepWithRecurseArrayItems:: (<k, v>, <k, v>, k) -> <k, v>
364 */
365export const mergeDeepWithRecurseArrayItemsAndMapObjs = R.curry((fn, applyObj, left, right) =>
366 _mergeDeepWithRecurseArrayItemsAndMapObjs(fn, applyObj, null, left, right)
367);
368
369/**
370 * Same as mergeDeepWithRecurseArrayItemsAndMapObjs but sends the same left and right value so fn is called on every key
371 * of ob * @params {Function} fn The merge function left l, right r, string k:: l -> r -> k -> a
372 * @params {Function} applyObj Function called with the current key and the result of each recursion that is an object.
373 * @params {Object} left the 'left' side object to merge
374 * @params {Object} right the 'right' side object to morge
375 * @returns {Object} The deep-merged object
376 * @sig applyDeepAndMapObjs:: (<k, v>, <k, v>, k) -> <k, v>j
377 */
378export const applyDeepAndMapObjs = R.curry((fn, applyObj, obj) =>
379 mergeDeepWithRecurseArrayItemsAndMapObjs(fn, applyObj, obj, obj)
380);
381
382const _mergeDeepWithRecurseArrayItemsAndMapObjs = R.curry((fn, applyObj, key, left, right) => {
383 return R.cond(
384 [
385 // Arrays
386 [R.all(Array.isArray),
387 // Recurse on each array item. We pass the key without the index
388 lr => {
389 return R.apply(
390 R.zipWith(
391 (l, r) => R.compose(
392 // For array items, take key and the result and call the applyObj func, but only if res is an Object
393 v => R.when(
394 // When it's an object and not an array call applyObj
395 // typeof x === 'object' check because sometimes values that are objects are not returning true
396 R.both(
397 vv => typeof vv === 'object',
398 R.complement(R.is)(Array)
399 ),
400 res => applyObj(key, res)
401 )(v),
402 ([kk, ll, rr]) => _mergeDeepWithRecurseArrayItemsAndMapObjs(fn, applyObj, kk, ll, rr)
403 )([key, l, r])
404 )
405 )(lr);
406 }
407 ],
408 // Primitives: call the function with left and right as the first two args and key as the last
409 [R.complement(R.all)(x => isObject(x)), lr => {
410 return fn(...lr, key);
411 }],
412 // Always leave functions alone.
413 [lr => R.all(R.is(Function), lr), ([l, _]) => {
414 return l;
415 }],
416 // Objects
417 [R.T,
418 lr => {
419 return R.apply(
420 R.mergeWithKey(
421 (k, l, r) => R.compose(
422 // Take key and the result and call the applyObj func, but only if res is an Object
423 v => R.when(
424 // When it's an object and not an array call applyObj
425 // typeof x === 'object' check because sometimes values that are objects are not returning true
426 R.both(
427 x => typeof x === 'object',
428 R.complement(R.is)(Array)
429 ),
430 res => applyObj(k, res)
431 )(v),
432 // First recurse on l and r
433 ([kk, ll, rr]) => R.apply(_mergeDeepWithRecurseArrayItemsAndMapObjs(fn, applyObj), [kk, ll, rr])
434 )([k, l, r])
435 ),
436 lr
437 );
438 }
439 ]
440 ]
441 )([left, right]);
442 }
443);
444
445
446/**
447 * http://stackoverflow.com/questions/40011725/point-free-style-capitalize-function-with-ramda
448 * Capitalize the first letter
449 * @param {String} str The string to capitalize
450 * @returns {String} The capitalized string
451 * @sig capitalize:: String -> String
452 */
453export const capitalize = str => R.compose(
454 R.join(''),
455 R.juxt([R.compose(R.toUpper, R.head), R.tail])
456)(str);
457
458/**
459 * http://stackoverflow.com/questions/40011725/point-free-style-capitalize-function-with-ramda
460 * Lowercase the first letter
461 * @param {String} str The string to lowercase
462 * @returns {String} The capitalized string
463 * @sig capitalize:: String -> String
464 */
465export const lowercase = str => R.compose(
466 R.join(''),
467 R.juxt([R.compose(R.toLower, R.head), R.tail])
468)(str);
469
470/**
471 * From https://github.com/substack/camelize/blob/master/index.js
472 * @param {String} str The string to camelCase
473 * @returns {String} The camel-cased string
474 */
475export const camelCase = str =>
476 R.toLower(str).replace(
477 /[_.-](\w|$)/g,
478 (_, x) => x.toUpperCase()
479 );
480
481/**
482 * Merge a list of objects using the given concat function
483 * [{k: v}] → {k: v}
484 * @returns {Object} The merged object
485 * @sig mergeAllWithKey:: (String → a → a → a) → [{a}] → {a}
486 */
487export const mergeAllWithKey = R.curry((fn, [head, ...rest]) =>
488 R.mergeWithKey( // call mergeWithKey on two objects at a time
489 fn,
490 head || {}, // first object is always the head
491 R.ifElse( // second object is the merged object of the recursion
492 R.isEmpty, // if no rest
493 () => R.empty({}), // end case empty object
494 mergeAllWithKey(fn) // else recurse with the rest
495 )(rest)
496 )
497);
498
499/**
500 * Get a required path or return a helpful Error if it fails
501 * @param {String} path A lensPath, e.g. ['a', 'b'] or ['a', 2, 'b']
502 * @param {Object} obj The object to inspect
503 * @returns {Result} Result the resolved value or an Error
504 * @sig reqPath:: String -> {k: v} → Result
505 */
506export const reqPath = R.curry((path, obj) => {
507 return R.compose(
508 R.ifElse(
509 // If path doesn't resolve
510 maybe => maybe.isNothing,
511 // Create a useful Error message
512 () => Result.Error({
513 resolved: R.reduceWhile(
514 // Stop if the accumulated segments can't be resolved
515 (segments, segment) => R.not(R.isNil(R.path(R.concat(segments, [segment]), obj))),
516 // Accumulate segments
517 (segments, segment) => R.concat(segments, [segment]),
518 [],
519 path
520 ),
521 path: path
522 }),
523 // Return the resolved value
524 res => Result.Ok(res.value)
525 ),
526 // Try to resolve the value using the path and obj, returning Maybe
527 Rm.path(path)
528 )(obj);
529});
530
531/**
532 * Expects a prop path and returns a function expecting props,
533 * which resolves the prop indicated by the string. Returns Result.Error if there is not match
534 * @param {String} str dot-separated prop path
535 * @param {Object} props Object to resolve the path in
536 * @return {Result} Result.Ok with the resolved value or Result.Error if the path doesn't exist or the
537 * value is null
538 */
539export const reqStrPath = R.curry((str, props) => reqPath(R.split('.', str), props));
540
541/**
542 * Expects a prop path and returns a function expecting props,
543 * which resolves the prop indicated by the string. If not match is found it returns undefined
544 * @param {String} str dot-separated prop path
545 * @param {Object} props Object to resolve the path in
546 * @return {Object} The resolved object or undefined
547 */
548export const strPath = R.curry((str, props) => {
549 return R.view(R.lensPath(R.split('.', str)), props);
550});
551
552/**
553 * Like strPath but defaults the given value
554 * @param {Object} defaultValue. Default value if value is null undefined.
555 * @param {String} str dot-separated prop path
556 * @param {Object} props Object to resolve the path in
557 * @return {function(*=)}
558 */
559export const strPathOr = R.curry((defaultValue, str, props) => {
560 const result = R.view(R.lensPath(R.split('.', str)), props);
561 return R.when(
562 R.isNil,
563 R.always(defaultValue)
564 )(result);
565});
566
567export const strPathOrNullOk = R.curry((defaultValue, str, props) => {
568 const segments = R.split('.', str);
569 const result = R.view(R.lensPath(R.init(segments)), props);
570 return R.ifElse(
571 R.isNil,
572 R.always(defaultValue),
573 r => {
574 try {
575 return R.when(v => typeof v === 'undefined', () => defaultValue)(R.prop(R.last(segments), r));
576 } catch {
577 return defaultValue;
578 }
579 }
580 )(result);
581});
582
583/**
584 * Returns true if the given string path is non-null
585 * @param {String} str dot-separated prop path
586 * @param {Object} props Object to resolve the path in
587 * @returns {Boolean} true
588 */
589export const hasStrPath = R.curry((str, props) =>
590 R.complement(R.isNil)(
591 R.view(R.lensPath(R.split('.', str)), props)
592 )
593);
594
595/**
596 * Uses reqPath to resolve the path of an object and compares it to val
597 * @param {String} path A lensPath, e.g. ['a', 'b'] or ['a', 2, 'b']
598 * @param {*} val The val to do an equality check on
599 * @param {Object} obj The object to inspect
600 * @returns {Result} Result the resolved value or an Error
601 * @sig reqPath:: String -> {k: v} → Result
602 */
603export const reqPathPropEq = R.curry((path, val, obj) =>
604 // If the reqPath is valid map it to a comparison with val
605 reqPath(path, obj).map(R.equals(val))
606);
607
608/**
609 * From the cookbook: https://github.com/ramda/ramda/wiki/Cookbook#map-keys-of-an-object
610 * Maps keys according to the given function
611 * @returns {Object} The mapped keys of the object
612 * @sig mapKeys :: (String -> String) -> Object -> Object
613 */
614export const mapKeys = R.curry(
615 (fn, obj) => R.compose(
616 R.fromPairs,
617 pairs => R.map(
618 // Apply fn to index 0 of pair
619 pair => R.adjust(0, fn, pair),
620 pairs
621 ),
622 R.toPairs
623 )(obj)
624);
625
626
627/**
628 * Uses a lens to map keys that are embedded in a data structure
629 * The lens must indicate an object whose keys shall be mapped
630 * @returns {Object} Object with the keys indicated by the given lens mapped
631 * @sig mapKeysForLens :: Lens -> ((String -> String) -> Object -> Object
632 */
633export const mapKeysForLens = R.curry((lens, fn, obj) =>
634 // Sets the lens-focused objects to a new object with keys mapped according to the function
635 R.set(lens, mapKeys(fn, R.view(lens, obj)), obj)
636);
637
638/**
639 * Converts default to desired name. Used for requiring defaults
640 * @param {String} keyName The desired rename of 'default'
641 * @param {Object} module A required module with a default key
642 * @returns {Object} The import module with key default changed to keyName
643 * @sig mapDefault :: String -> <k,v> -> <k,v>
644 */
645export const mapDefault = (keyName, module) => mapKeys(key => key === 'default' ? keyName : key, module);
646/**
647 * Converts default to desired name and prefixes others.
648 * Used for requiring defaults and renaming others
649 * @param {String} defaultName The desired rename of 'default'
650 * @param {String} prefix The desired prefix for others. Camel Case maintained
651 * @param {Object} module A required module with a default key
652 * @returns {Object} The import module with key default changed to keyName
653 * @sig mapDefault :: String -> <k,v> -> <k,v>
654 */
655export const mapDefaultAndPrefixOthers = (defaultName, prefix, module) =>
656 mapKeys(
657 key => (key === 'default') ? defaultName : `${prefix}${capitalize(key)}`,
658 module
659 );
660
661/**
662 * Maps an object with a function that returns pairs and create and object therefrom
663 * Like R.mapObjIndexed, the function's first argument is the value of each item, and the seconds is the key if
664 * iterating over objects
665 * @params {Functor} f The mapping function
666 * @params {Container} container Anything that can be mapped
667 * @returns {Object} The mapped pairs made into key values
668 * @sig mapKeysAndValues :: Functor F = (a -> [b,c]) -> F -> <k,v>
669 */
670export const mapKeysAndValues = R.curry((f, container) => R.fromPairs(mapObjToValues(f, container)));
671
672/**
673 * https://github.com/ramda/ramda/wiki/Cookbook
674 * Filter objects with values and keys. Null objs return as null
675 * @param {Function} pred (value, key) => True|False
676 * @param {Object} obj The object to filter
677 * @returns {Object} The filtered object
678 * @sig filterWithKeys:: (v -> k -> True|False) -> <k,v> -> <k,v>
679 */
680export const filterWithKeys = R.curry((pred, obj) => {
681 if (!obj) {
682 return obj;
683 }
684 return R.compose(
685 R.fromPairs,
686 R.filter(pair => R.apply(pred, R.reverse(pair))),
687 R.toPairs
688 )(obj);
689});
690
691/**
692 * Transforms the keys of the given object with the given func
693 * @param {Function} func The mapping function that expects the key
694 * @param {Object} The object to map
695 * @returns {Object} The object with transformed keys and the original values
696 */
697export const transformKeys = R.curry((func, obj) =>
698 R.compose(
699 R.fromPairs,
700 R.map(([key, value]) =>
701 [func(key), value]),
702 R.toPairs
703 )(obj)
704);
705
706/**
707 * Renames the key of the object specified by the lens
708 * @param {Function} lens A ramda lens that points to the object containing the key, not the key itself.
709 * Use R.lensPath([]) to operate directly on obj
710 * @param {String} from Key to rename
711 * @param {String} to New name for the key
712 * @param {Object} obj Object to traverse with the lens
713 */
714export const renameKey = R.curry((lens, from, to, obj) => R.over(
715 lens,
716 target => mapKeys(
717 R.when(R.equals(from), R.always(to)),
718 target),
719 obj));
720
721/**
722 * Duplicates the key of the object specified by the lens and key, to the given list of keys or single key.
723 * A duplicate of the value at key will be added at each of the toKeys using R.clone
724 * @param {Function} lens A ramda lens that points to the object containing the key, not the key itself
725 * @param {String} key Key to duplicate the value of
726 * @param {String|[String]} toKeys Array of new keys to make. New keys overwrite existing keys
727 * @param {Object} obj Object to traverse with the lens
728 */
729export const duplicateKey = R.curry((lens, key, toKeys, obj) => R.over(
730 lens,
731 // convert the target of the lens to a merge of the target with copies of target[key]
732 target => R.merge(
733 target,
734 R.fromPairs(
735 R.map(
736 toKey => [toKey, R.clone(target[key])],
737 toArrayIfNot(toKeys)
738 )
739 )
740 ),
741 // take the lens of this obj
742 obj)
743);
744
745/**
746 * Converts a scalar value (!Array.isArray) to array an array
747 * @param {*|[*]} arrayOrScalar An array or scalar
748 * @returns {[*]} The scalar as an array or the untouched array
749 */
750export const toArrayIfNot = arrayOrScalar => {
751 return R.unless(Array.isArray, Array.of)(arrayOrScalar);
752};
753
754/**
755 * Like duplicateKey but removes the original key
756 * @param {Function} lens A ramda lens that points to the object containing the key, not the key itself
757 * @param {String} key Key to duplicate the value of
758 * @param [{String}] toKeys Array of new keys to make. New keys overwrite existing keys
759 * @param {Object} obj Object to traverse with the lens
760 */
761export const moveToKeys = R.curry((lens, key, toKeys, obj) => R.over(
762 lens,
763 // convert the target of the lens to a merge of the target with copies of target[key]
764 // and with target[key] itself removed
765 target => R.merge(
766 R.omit([key], target),
767 R.fromPairs(
768 R.map(
769 toKey => [toKey, R.clone(target[key])],
770 toKeys
771 )
772 )
773 ),
774 // take the lens of this obj
775 obj)
776);
777
778/**
779 * Like R.find but expects only one match and works on both arrays and objects
780 * Note this still iterates through the whole list or object, so this should be rewritten to quit when one is found
781 * @param {Function} predicate
782 * @param {Array|Object} obj Container that should only match once with predicate
783 * @returns {Result} Result.Error if no matches or more than one, otherwise Result.Ok with the single matching item in an array/object
784 */
785export const findOne = R.curry((predicate, obj) =>
786 R.ifElse(
787 // If path doesn't resolve
788 items => R.equals(R.length(R.keys(items)), 1),
789 // Return the resolved single key/value object
790 items => Result.Ok(items),
791 // Create a useful Error message
792 items => Result.Error({
793 all: obj,
794 matching: items
795 })
796 )(R.filter(predicate, obj))
797);
798
799/**
800 * Version of find one that accepts all items of the given Container
801 * @param {Array|Object} obj Container
802 * @returns {Result} Result.Error if no items or more than one, otherwise Result.Ok with the single item in an array/object
803 */
804export const onlyOne = findOne(R.T);
805
806/**
807 * Like onlyOne but extracts the value
808 * @param {Array|Object|Result} obj Container that should have one value to extract. This currently expects
809 * and array or object or Result, but it could be expanded to take Result, Maybe, or any other container where
810 * the value can be extracted. Types like Tasks and Streams can't extract, I suppose
811 * @returns {Result} Result.Error if no items or more than one, otherwise Result.Ok with the single value
812 */
813export const onlyOneValue = R.compose(
814 // Use R.map to operate on the value of Result without extracting it
815 R.map(R.head),
816 R.map(R.values),
817 findOne(R.T)
818);
819
820/**
821 * Curryable Maps container values to the key and values of an object, applying f to each to make the value
822 * @param {Function} f Transforms each item to a value
823 * @param {Array} list Container to map to an object.
824 * @sig mapToObjValue:: Functor a => (b -> c) -> <b, c>
825 */
826export const mapToObjValue = R.curry((f, obj) => R.compose(R.fromPairs, R.map(v => [v, f(v)]))(obj));
827
828
829/**
830 * Finds an item that matches all the given props in params
831 * @param {Object} params object key values to match
832 * @param {Object|Array} items Object or Array that can produce values to search
833 * @returns {Result} An Result.Ok containing the value or an Result.Error if no value is found
834 */
835export const findOneValueByParams = (params, items) => {
836 return findOne(
837 // Compare all the eqProps against each item
838 R.allPass(
839 // Create a eqProps for each prop of params
840 R.map(prop => R.eqProps(prop, params),
841 R.keys(params)
842 )
843 ),
844 R.values(items)
845 ).map(R.head);
846};
847
848/**
849 * Returns the items matching all param key values
850 * @param {Object} params Key values to match to items
851 * @param {Object|Array} items Object with values of objects or array of objects that can produce values to search
852 * @returns {[Object]} items that pass
853 */
854export const findByParams = (params, items) => {
855 return R.filter(
856 // Compare all the eqProps against each item
857 R.allPass(
858 // Create a eqProps for each prop of params
859 R.map(prop => R.eqProps(prop, params),
860 R.keys(params)
861 )
862 ),
863 items
864 );
865};
866
867/**
868 * Returns the first mapped item that is not null
869 * @param {Function} f The mapping function
870 * @param {[*]} items The items
871 * @returns {*} The first mapped item value that is not nil
872 */
873export const findMapped = (f, items) => {
874 return R.reduceWhile(
875 R.isNil,
876 (_, i) => f(i),
877 null,
878 items
879 );
880};
881
882/**
883 * Converts the given value to an always function (that ignores all arguments) unless already a function
884 * @param {Function} maybeFunc A function or something else
885 * @return {Function} a function that always returns the non funcion value of maybeFunc, or maybeFunc
886 * itself if maybeFunc is a functon
887 */
888export const alwaysFunc = maybeFunc => R.unless(R.is(Function), R.always)(maybeFunc);
889
890/**
891 * Map the object with a function accepting a key, value, and the obj but return just the mapped values,
892 * not the object
893 * @param {Function} f Expects value, key, and obj
894 * @param {Object} obj The object to map
895 * @return {[Object]} Mapped values
896 */
897export const mapObjToValues = R.curry((f, obj) => {
898 return R.values(R.mapObjIndexed(f, obj));
899});
900
901/**
902 * Filter the given object and return the values, discarding the keys
903 * @param {Function} f Receives each value and key
904 * @param {Object} obj The object to filter
905 * @return {Object} The filtered object values
906 */
907export const filterObjToValues = R.curry((f, obj) => {
908 return R.values(filterWithKeys(f, obj));
909});
910/**
911 * Like mapObjToValues but chains the values when an array is returned for each mapping
912 * @param {Function} f Expects key, value, and obj
913 * @param {Object} obj The object to chain
914 * @return {[Object]} Mapped flattened values
915 */
916export const chainObjToValues = R.curry((f, obj) => {
917 return R.chain(R.identity, mapObjToValues(f, obj));
918});
919
920
921/**
922 *
923 * This is a deep fromPairs. If it encounters a two element array it assumes it's a pair if the first
924 * element is a string. If an array is not a pair it is iterated over and each element is recursed upon.
925 * The 2nd element of each pair is also recursed upon. The end case for recursion is that an element is not an array.
926 *
927 * This method doesn't currently expect objects but could be easily modified to handle them
928 * @param {[*]} deepPairs An array of deep pairs, with possibly other arrays at the top or lower levels
929 * that aren't pairs but might contain them
930 * @returns {Array|Object} All arrays of pairs are converted to objects. Arrays of non pairs are left as arrays
931 */
932export const fromPairsDeep = deepPairs => R.cond(
933 [
934 // It's array of pairs or some other array
935 [Array.isArray, list =>
936 R.ifElse(
937 // Is the first item a two element array whose first item is a string?
938 ([first]) => R.allPass(
939 [
940 Array.isArray,
941 x => R.compose(R.equals(2), R.length)(x),
942 x => R.compose(R.is(String), R.head)(x)
943 ])(first),
944 // Yes, return an object whose keys are the first element and values are the result of recursing on the second
945 l => R.compose(R.fromPairs, R.map(([k, v]) => [k, fromPairsDeep(v)]))(l),
946 // No, recurse on each array item
947 l => R.map(v => fromPairsDeep(v), l)
948 )(list)
949 ],
950 // End case, return the given value unadulterated
951 [R.T, R.identity]
952 ])(deepPairs);
953
954
955/**
956 * At the given depth, object and array values are converted to the replacementString.
957 * @param {Number} n Depth to replace at.
958 * Depth 3 converts {a: {b: {c: 1}}} to {a: {b: {c: '...'}}}
959 * Depth 2 converts {a: {b: {c: 1}}} to {a: {b: '...'}}
960 * Depth 1 converts {a: {b: {c: 1}}} to {a: '...'}
961 * Depth 0 converts {a: {b: {c: 1}}} to '...'
962 * @param {String|Function} replaceStringOrFunc String such as '...' or a unary function that replaces the value
963 * e.g. R.when(isObject, R.length(R.keys)) will count objects and arrays but leave primitives alone
964 * @param {Object} obj Object to process
965 * @returns {Object} with the above transformation. Use replaceValuesAtDepthAndStringify to get a string
966 */
967export function replaceValuesAtDepth(n, replaceStringOrFunc, obj) {
968 return R.ifElse(
969 // If we are above level 0 and we have an object
970 R.both(R.always(R.lt(0, n)), isObject),
971 // Then recurse on each object or array value
972 o => R.map(oo => replaceValuesAtDepth(n - 1, replaceStringOrFunc, oo), o),
973 // If at level 0 replace the value. If not an object or not at level 0, leave it alone
974 o => R.when(R.always(R.equals(0, n)), alwaysFunc(replaceStringOrFunc))(o)
975 )(obj);
976}
977
978/** *
979 * replaceValuesAtDepth but stringifies result
980 * @param {Number} n Depth to replace at.
981 * Depth 3 converts {a: {b: {c: 1}}} to {a: {b: {c: '...'}}}
982 * Depth 2 converts {a: {b: {c: 1}}} to {a: {b: '...'}}
983 * Depth 1 converts {a: {b: {c: 1}}} to {a: '...'}
984 * Depth 0 converts {a: {b: {c: 1}}} to '...'
985 * @param {String|Function} replaceString String such as '...' or a unary function that replaces the value
986 * e.g. R.when(isObject, R.length(R.keys)) will count objects and arrays but leave primitives alone
987 * @param {Object} obj Object to process
988 * @returns {String} after the above replacement
989 */
990export const replaceValuesAtDepthAndStringify = (n, replaceString, obj) => {
991 return JSON.stringify(replaceValuesAtDepth(n, replaceString, obj));
992};
993
994/**
995 * Convenient method to count objects and lists at the given depth but leave primitives alone
996 * @param {Number} n Depth to replace at.
997 * @param {Object} obj Object to process
998 * @returns {Object} after the above replacement
999 */
1000export const replaceValuesWithCountAtDepth = (n, obj) => {
1001 return replaceValuesAtDepth(
1002 n,
1003 R.when(
1004 isObject,
1005 o => R.compose(
1006 // Show arrays and objs different
1007 R.ifElse(R.always(Array.isArray(o)), c => `[...${c}]`, c => `{...${c}}`),
1008 R.length,
1009 R.keys)(o)
1010 ),
1011 obj);
1012};
1013
1014/**
1015 * Convenient method to count objects and lists at the given depth but leave primitives alone and stringify result
1016 * @param {Number} n Depth to replace at.
1017 * @param {Object} obj Object to process
1018 * @returns {String} after the above replacement
1019 */
1020export const replaceValuesWithCountAtDepthAndStringify = (n, obj) => {
1021 return JSON.stringify(replaceValuesWithCountAtDepth(n, obj));
1022};
1023
1024/**
1025 * Flattens an objects so deep keys and array indices become concatinated strings
1026 * E.g. {a: {b: [1, 3]}} => {'a.b.0': 1, 'a.b.1': 2}
1027 * @param {Object} obj The object to flattened
1028 * @returns {Object} The 1-D version of the object
1029 */
1030export const flattenObj = obj => {
1031 return R.fromPairs(_flattenObj({}, obj));
1032};
1033
1034/**
1035 * Flatten objs until the predicate returns false. This is called recursively on each object and array
1036 * @param {Function} predicate Expects object, returns true when we should stop flatting on the current object
1037 * @param {Object} obj The object to flatten
1038 * @return {Object} The flattened object
1039 */
1040export const flattenObjUntil = (predicate, obj) => {
1041 return R.fromPairs(_flattenObj({predicate}, obj));
1042};
1043
1044const _flattenObj = (config, obj, keys = []) => {
1045 const predicate = R.propOr(null, 'predicate', config);
1046 return R.ifElse(
1047 // If we have an object
1048 o => R.both(
1049 isObject,
1050 oo => R.when(
1051 () => predicate,
1052 ooo => R.complement(predicate)(ooo)
1053 )(oo)
1054 )(o),
1055 // Then recurse on each object or array value
1056 o => chainObjToValues((oo, k) => _flattenObj(config, oo, R.concat(keys, [k])), o),
1057 // If not an object return flat pair
1058 o => [[R.join('.', keys), o]]
1059 )(obj);
1060};
1061
1062/**
1063 * Converts a key string like 'foo.bar.0.wopper' to ['foo', 'bar', 0, 'wopper']
1064 * @param {String} keyString The dot-separated key string
1065 * @returns {[String]} The lens array containing string or integers
1066 */
1067export const keyStringToLensPath = keyString => R.map(
1068 R.when(R.compose(R.complement(R.equals)(NaN), parseInt), parseInt),
1069 R.split('.', keyString)
1070);
1071
1072/**
1073 * Undoes the work of flattenObj. Does not allow number keys to become array indices
1074 * @param {Object} obj 1-D object in the form returned by flattenObj
1075 * @returns {Object} The original
1076 */
1077export const unflattenObjNoArrays = obj => {
1078 return _unflattenObj({allowArrays: false}, obj);
1079};
1080
1081/**
1082 * Undoes the work of flattenObj
1083 * @param {Object} obj 1-D object in the form returned by flattenObj
1084 * @returns {Object} The original
1085 */
1086export const unflattenObj = obj => {
1087 return _unflattenObj({allowArrays: true}, obj);
1088};
1089
1090export const _unflattenObj = (config, obj) => {
1091 return R.compose(
1092 R.reduce(
1093 (accum, [keyString, value]) => {
1094 // Don't allow indices if allowArrays is false
1095 const itemKeyPath = R.map(
1096 key => {
1097 return R.when(
1098 () => R.not(R.prop('allowArrays', config)),
1099 k => k.toString()
1100 )(key);
1101 },
1102 keyStringToLensPath(keyString)
1103 );
1104 // Current item lens
1105 const itemLensPath = R.lensPath(itemKeyPath);
1106 // All but the last segment gives us the item container len
1107 const constainerKeyPath = R.init(itemKeyPath);
1108 const container = R.unless(
1109 // If the path has any length (not []) and the value is set, don't do anything
1110 R.both(R.always(R.length(constainerKeyPath)), R.view(R.lensPath(constainerKeyPath))),
1111 // Else we are at the top level, so use the existing accum or create a [] or {}
1112 // depending on if our item key is a number or not
1113 x => R.defaultTo(
1114 R.ifElse(
1115 v => R.both(() => R.prop('allowArrays', config), R.is(Number))(v),
1116 R.always([]),
1117 R.always({})
1118 )(R.head(itemKeyPath))
1119 )(x)
1120 )(accum);
1121 // Finally set the container at the itemLensPath
1122 return R.set(
1123 itemLensPath,
1124 value,
1125 container
1126 );
1127 },
1128 // null initial value
1129 null
1130 ),
1131 R.toPairs
1132 )(obj);
1133};
1134
1135/**
1136 * Does something to every object
1137 * @param {Function} func Called on each object that is the child of another object. Called with the key that
1138 * points at it from the parent object and the object itself
1139 * @param {Object} obj The object to process
1140 */
1141export const overDeep = R.curry((func, obj) => mergeDeepWithRecurseArrayItemsAndMapObjs(
1142 // We are using a mergeDeepWithRecurseArrayItemsAndMapObjs but we only need the second function
1143 (l, r, k) => l,
1144 func,
1145 // Use obj twice so that all keys match and get called with the merge function
1146 obj,
1147 obj
1148 )
1149);
1150
1151/**
1152 * Omit the given keys anywhere in a data structure. Objects and arrays are recursed and omit_deep is called
1153 * on each dictionary that hasn't been removed by omit_deep at a higher level
1154 */
1155export const omitDeep = R.curry(
1156 (omit_keys, obj) => R.compose(
1157 o => applyDeepAndMapObjs(
1158 // If k is in omit_keys return {} to force the applyObj function to call. Otherwise take l since l and r are always the same
1159 (l, r, kk) => R.ifElse(
1160 k => R.contains(k, omit_keys),
1161 R.always({}),
1162 R.always(l)
1163 )(kk),
1164 // Called as the result of each recursion. Removes the keys at any level except the topmost level
1165 (key, result) => R.omit(omit_keys, result),
1166 o
1167 ),
1168 // Omit at the top level. We have to do this because applyObj of applyDeepAndMapObjs only gets called starting
1169 // on the object of each key
1170 o => R.omit(omit_keys, o)
1171 )(obj)
1172);
1173
1174/**
1175 * Omit by the given function that is called with key and value. You can ignore the value if you only want to test the key
1176 * Objects and arrays are recursed and omit_deep is called
1177 * on each dictionary that hasn't been removed by omit_deep at a higher level
1178 * @param {Function} f Binary function accepting each key, value. Return non-nil to omit and false or nil to keep
1179 */
1180export const omitDeepBy = R.curry(
1181 (f, obj) => R.compose(
1182 o => applyDeepAndMapObjs(
1183 // If k is in omit_keys return {} to force the applyObj function to call. Otherwise take l since l and r are always the same
1184 // l and r are always the same value
1185 (l, r, kk) => R.ifElse(
1186 // Reject any function return value that isn't null or false
1187 k => R.anyPass([R.isNil, R.equals(false)])(f(k, l)),
1188 R.always(l),
1189 () => ({})
1190 )(kk),
1191 // Called as the result of each recursion. Removes the keys at any level except the topmost level
1192 (key, result) => filterWithKeys(
1193 (v, k) => R.anyPass([R.isNil, R.equals(false)])(f(k, v)),
1194 result
1195 ),
1196 o
1197 ),
1198 // Omit at the top level. We have to do this because applyObj of applyDeepAndMapObjs only gets called starting
1199 // on the object of each key
1200 // Reject any function return value that isn't null or false
1201 o => filterWithKeys(
1202 (v, k) => R.anyPass([R.isNil, R.equals(false)])(f(k, v)),
1203 o
1204 )
1205 )(obj)
1206);
1207
1208/**
1209 * Given a predicate for eliminating an item based on paths, return the paths of item that don't pass the predicate.
1210 * keyOrIndex is the object key or array index that the item came from. We use it for the eliminateItemPredicate
1211 * test. If each paths current first segment matches keyOrIndex or equals '*', we consider that path.
1212 * With the considered paths
1213 * @param {Function} eliminateItemPredicate Accepts the remaining paths and optional item as a second argument.
1214 * Returns true if the item shall be eliminated
1215 * @param {[String]} paths Paths to caculate if they match the item
1216 * @param {*} item Item to test
1217 * @param {String|Number} keyOrIndex The key or index that the item belonged to of an object or array
1218 * @return {Object} {item: the item, paths: remaining paths with first segment removed}. Or null if eliminateItemPredicate
1219 * returns true
1220 * @private
1221 */
1222const _calculateRemainingPaths = (eliminateItemPredicate, paths, item, keyOrIndex) => {
1223 // Keep paths that match keyOrIndex as the first item. Remove other paths
1224 // since they can't match item or its descendants
1225 const tailPathsStillMatchingItemPath = compact(R.map(
1226 R.compose(
1227 R.ifElse(
1228 R.compose(
1229 aKeyOrIndex => {
1230 return R.ifElse(
1231 // if keyOrIndex is a string and matches the shape of a regex: /.../[gim]
1232 possibleRegex => R.both(R.is(String), str => R.test(regexToMatchARegex, str))(possibleRegex),
1233 // Construct the regex with one or two, the expression and options (gim)
1234 provenRegex => {
1235 const args = compactEmpty(R.split('/', provenRegex));
1236 return new RegExp(...args).test(keyOrIndex);
1237 },
1238 // If aKeyOrIndex is '*' or equals keyOrIndex always return true
1239 str => R.includes(str, ['*', keyOrIndex])
1240 )(aKeyOrIndex);
1241 },
1242 R.head
1243 ),
1244 // Matches the keyOrIndex at the head. Return the tail
1245 R.tail,
1246 // Mark null to remove from list
1247 R.always(null)),
1248 // Convert path to array with string keys and number indexes
1249 keyStringToLensPath
1250 ),
1251 paths
1252 ));
1253 // For omit:
1254 // If any path matches the path to the item return null so we can throw away the item
1255 // If no path is down to zero length return the item and the paths
1256 // For pick:
1257 // If no path matches the path to the item return null so we can throw away the item
1258 // If any path is not down to zero return the item and the paths, unless item is a primitive meaning it can't match
1259 // a path
1260 return R.ifElse(
1261 tailPaths => eliminateItemPredicate(tailPaths, item),
1262 R.always(null),
1263 p => ({item: item, paths: R.map(R.join('.'), p)})
1264 )(tailPathsStillMatchingItemPath);
1265};
1266
1267// This predicate looks for any path that's zero length, meaning it matches the path to an item and we should
1268// omit that item
1269const _omitDeepPathsEliminateItemPredicate = paths => R.any(R.compose(R.equals(0), R.length), paths);
1270
1271/**
1272 * Omit matching paths in a a structure. For instance omitDeepPaths(['a.b.c', 'a.0.1']) will omit keys
1273 * c in {a: {b: c: ...}}} and 'y' in {a: [['x', 'y']]}
1274 */
1275export const omitDeepPaths = R.curry((pathSet, obj) => R.cond([
1276
1277 // Arrays
1278 [o => Array.isArray(o),
1279 list => {
1280 // Recurse on each array item that doesn't match the paths.
1281 // We pass the key without the index
1282 // If any path matches the path to the value we return the item and the matching paths
1283 const survivingItems = compact(R.addIndex(R.map)(
1284 (item, index) => _calculateRemainingPaths(_omitDeepPathsEliminateItemPredicate, pathSet, item, index),
1285 list
1286 ));
1287 return R.map(({paths, item}) => omitDeepPaths(paths, item), survivingItems);
1288 }
1289 ],
1290 // Primitives always pass.
1291 [R.complement(isObject), primitive => primitive],
1292 // Objects
1293 [R.T,
1294 o => {
1295 // Recurse on each object value that doesn't match the paths.
1296 const survivingItems = compact(R.mapObjIndexed(
1297 // If any path matches the path to the value we return the value and the matching paths
1298 // If no path matches it we know the value shouldn't be omitted so we don't recurse on it below
1299 (value, key) => _calculateRemainingPaths(_omitDeepPathsEliminateItemPredicate, pathSet, value, key),
1300 o
1301 ));
1302 // Only recurse on items from the object that are still eligible for omitting
1303 return R.map(({paths, item}) => omitDeepPaths(paths, item), survivingItems);
1304 }
1305 ]
1306 ]
1307 )(obj)
1308);
1309
1310// This eliminate predicate returns true if no path is left matching the item's path so the item should not
1311// be picked. It also returns true if the there are paths with length greater than 0
1312// but item is a primitive, meaning it can't match a path
1313const _pickDeepPathsEliminateItemPredicate = (paths, item) => {
1314 return R.either(
1315 R.compose(R.equals(0), R.length),
1316 pths => {
1317 return R.both(
1318 // Item is not an object
1319 () => R.complement(R.is)(Object, item),
1320 ps => R.any(R.compose(R.lt(0), R.length), ps)
1321 )(pths);
1322 }
1323 )(paths);
1324};
1325/**
1326 * Pick matching paths in a a structure. For instance pickDeepPaths(['a.b.c', 'a.0.1']) will pick only keys
1327 * c in {a: {b: c: ...}}} and 'y' in {a: [['x', 'y']]}.
1328 * Use * in the path to capture all array items or keys, e.g. ['a.*.c./1|3/']
1329 * to get all items 0 or 3 of c that is in all items of a, whether a is an object or array
1330 */
1331export const pickDeepPaths = R.curry((pathSet, obj) => R.cond([
1332 // Arrays
1333 [o => Array.isArray(o),
1334 list => {
1335 // Recurse on each array item that doesn't match the paths. We pass the key without the index
1336 // We pass the key without the index
1337 // If any path matches the path to the value we return the item and the matching paths
1338 const survivingItemsEachWithRemainingPaths = compact(R.addIndex(R.map)(
1339 (item, index) => {
1340 return _calculateRemainingPaths(_pickDeepPathsEliminateItemPredicate, pathSet, item, index);
1341 },
1342 list
1343 ));
1344 return R.map(
1345 R.ifElse(
1346 // If the only paths are now empty we have a match with the items path and keep the item.
1347 // Otherwise we pick recursively
1348 ({paths}) => R.all(R.compose(R.equals(0), R.length), paths),
1349 ({item}) => item,
1350 ({item, paths}) => pickDeepPaths(paths, item)
1351 ),
1352 survivingItemsEachWithRemainingPaths
1353 );
1354 }
1355 ],
1356 // Primitives never match because we'd only get here if we have pathSets remaining and no path can match a primitive
1357 [R.complement(isObject),
1358 () => {
1359 throw new Error('pickDeepPaths encountered a value that is not an object or array at the top level. This should never happens and suggests a bug in this function');
1360 }
1361 ],
1362 // Objects
1363 [R.T,
1364 o => {
1365 // Recurse on each array item that doesn't match the paths. We pass the key without the index
1366 // If any path matches the path to the value we return the value and the matching paths
1367 // If no path matches it we know the value shouldn't be picked so we don't recurse on it below
1368 const survivingItems = compact(R.mapObjIndexed(
1369 (item, key) => _calculateRemainingPaths(_pickDeepPathsEliminateItemPredicate, pathSet, item, key),
1370 o
1371 ));
1372 return R.map(
1373 R.ifElse(
1374 // If the only path is now empty we have a match with the items path and keep the item.
1375 // Otherwise we pick recursively
1376 ({item, paths}) => R.all(R.compose(R.equals(0), R.length), paths),
1377 R.prop('item'),
1378 ({item, paths}) => pickDeepPaths(paths, item)
1379 ),
1380 survivingItems
1381 );
1382 }
1383 ]
1384 ]
1385 )(obj)
1386);
1387
1388/**
1389 * splitAt that gives the split point item to both sides of the split
1390 * @param {Number} index The index
1391 * @param {String|[Object]} list A string or list
1392 * @returns {[Object]} A pair of results
1393 */
1394export const splitAtInclusive = (index, list) => {
1395 const pair = R.splitAt(index, list);
1396 return [
1397 R.concat(R.head(pair), R.slice(0, 1, R.last(pair))),
1398 R.last(pair)
1399 ];
1400};
1401
1402/**
1403 * Whether the objects are equal at the given propStr. Null objects are never equal
1404 * @param {String} stringPath Path of props separated by dots
1405 * @param {Object|Array} obj1 The object to compare to obj2 at the propStr
1406 * @param {Object|Array} obj2 The object to compare to obj1 at the propStr
1407 * @returns {Boolean} True or false
1408 */
1409export const eqStrPath = R.curry((stringPath, obj1, obj2) => {
1410 return R.apply(R.equals, R.map(strPathOr(null, stringPath), [obj1, obj2]));
1411});
1412
1413/**
1414 * Whether the objects are equal at the given strPaths. Null objects are never equal
1415 * @param [{String}] strPaths Paths of props separated by dots
1416 * @param {Object|Array} obj1 The object to compare to obj2 at the propStr
1417 * @param {Object|Array} obj2 The object to compare to obj1 at the propStr
1418 * @returns {Boolean} True or false
1419 */
1420export const eqStrPathsAll = R.curry(
1421 (strPaths, obj1, obj2) => R.all(prop => eqStrPath(prop, obj1, obj2), strPaths)
1422);