Source: obj.js

'use strict';

/**
 * Object utilities for immutable operations
 * @module obj
 */

/**
 * Creates an object with a single key-value pair.
 * @memberof module:obj
 * @param {string} k - The key
 * @param {*} v - The value
 * @returns {Object} An object with the single key-value pair
 * @example
 * keyValue('name', 'John') // => { name: 'John' }
 */
const keyValue = (k, v) => ({[k]: v});

/**
 * Checks if a value is a plain object literal (not an array, null, or other object type).
 * @memberof module:obj
 * @param {*} o - The value to check
 * @returns {boolean} True if the value is a plain object literal
 * @example
 * isLiteral({}) // => true
 * isLiteral([]) // => false
 * isLiteral(null) // => false
 */
const isLiteral = o => typeof o === 'object'
	&& o !== null && o.constructor === Object;

/**
 * Creates a shallow clone of an object, preserving its prototype.
 * @memberof module:obj
 * @param {Object} o - The object to clone
 * @returns {Object} A shallow clone of the object
 * @example
 * const obj = { a: 1, b: 2 };
 * const cloned = clone(obj);
 * cloned.a = 3;
 * console.log(obj.a) // => 1 (original unchanged)
 */
const clone = o => Object.assign(Object.create(Object.getPrototypeOf(o) || {}), o);

/**
 * Gets a nested value from an object using a path.
 * @memberof module:obj
 * @param {Object} o - The source object
 * @param {string|Array<string>} p - The path (string key or array of keys for nested access)
 * @returns {*} The value at the path, or undefined if not found
 * @example
 * sub({ a: { b: { c: 1 } } }, ['a', 'b', 'c']) // => 1
 * sub({ a: 1 }, 'a') // => 1
 * sub({ a: 1 }, 'b') // => undefined
 */
const sub = (o, p) => (o === undefined || o === null) ? undefined : (p instanceof Array)
	? p.length > 1 ? sub(o[p[0]], p.slice(1)) : o[p[0]]
	: o[p];

/**
 * Immutably updates a value in an object at a given path.
 * For nested paths, creates intermediate objects as needed.
 * If the value at the path is a literal object and v is also a literal object, they are merged.
 * @memberof module:obj
 * @param {Object} o - The source object
 * @param {string|Array<string>} k - The path (string key or array of keys for nested updates)
 * @param {*} v - The value to set
 * @returns {Object} A new object with the value updated at the path
 * @example
 * patch({ a: 1 }, 'b', 2) // => { a: 1, b: 2 }
 * patch({ a: { b: 1 } }, ['a', 'c'], 2) // => { a: { b: 1, c: 2 } }
 * patch({ a: { b: 1 } }, 'a', { c: 2 }) // => { a: { b: 1, c: 2 } } (merged)
 */
const patch = (o, k, v) => Object.assign(clone(o),
	(k instanceof Array)
		? keyValue(k[0], (k.length > 1)
			? patch(o[k[0]] || {}, k.slice(1), v)
			: isLiteral(o[k[0]]) && Object.assign(clone(o[k[0]]), v) || v)
		: keyValue(k, isLiteral(o[k]) && Object.assign(clone(o[k]), v) || v)
);

/**
 * Reduces an object to a single value by iterating over its keys.
 * Similar to Array.reduce, but for objects.
 * @memberof module:obj
 * @param {Object} o - The object to reduce
 * @param {Function} reduceFn - The reducer function (accumulator, key, value, index, originalObject) => newAccumulator
 * @param {*} [initial] - The initial accumulator value (defaults to first property value)
 * @returns {*} The final accumulated value
 * @example
 * reduce({ a: 1, b: 2, c: 3 }, (sum, key, val) => sum + val, 0) // => 6
 */
const reduce = (o, reduceFn, initial) => Object.keys(o)
	.reduce(
		(accumulator, key, index) => reduceFn(accumulator, key, o[key], index, o),
		typeof initial === 'undefined'
			? o[Object.keys(o)[0]] // to confirm with the array reduce spec in this case the firts property is used
			: initial
	);

/**
 * Immutably maps over an object's values.
 * @memberof module:obj
 * @param {Object} o - The source object
 * @param {Function} mapFn - The mapping function (key, value, index, originalObject) => newValue
 * @returns {Object} A new object with mapped values
 * @example
 * map({ a: 1, b: 2 }, (k, v) => v * 2) // => { a: 2, b: 4 }
 */
const map = (o, mapFn) => reduce(o,
	(o2, key, value, index) => patch(o2, key, mapFn(key, value, index, o)),
	{}
);

/**
 * Immutably filters an object's properties based on a predicate.
 * @memberof module:obj
 * @param {Object} o - The source object
 * @param {Function} filterFn - The filter predicate (key, value, index, originalObject) => boolean
 * @returns {Object} A new object with only properties that pass the predicate
 * @example
 * filter({ a: 1, b: 2, c: 3 }, (k, v) => v > 1) // => { b: 2, c: 3 }
 */
const filter = (o, filterFn) => reduce(o,
	(o2, key, value, index) => filterFn(key, value, index, o)
		? patch(o2, key, value)
		: o2,
	{}
);

/**
 * Recursively traverses an object tree and applies a function to leaf values.
 * Non-literal values (primitives, arrays, etc.) are transformed by the function.
 * @memberof module:obj
 * @param {Object} tree - The object tree to traverse
 * @param {Function} fn - The function to apply to leaf values (key, value, index, parent) => newValue
 * @returns {Object} A new object tree with transformed leaf values
 * @example
 * traverse({ a: { b: 1 }, c: 2 }, (k, v) => v * 2) // => { a: { b: 2 }, c: 4 }
 */
const traverse = (tree, fn) => Object.keys(tree).reduce((o, key, index) =>
	patch(o, key, isLiteral(tree[key])
		? traverse(tree[key], fn)
		: fn(key, tree[key], index, tree)
	), {}
);

/**
 * Chains multiple method calls on an object.
 * @memberof module:obj
 * @param {Object} o - The object to call methods on
 * @param {Array<Array>} chain - Array of [methodName, ...args] tuples
 * @returns {*} The result of the final method call
 * @example
 * chainCall([1, 2, 3], [['map', x => x * 2], ['filter', x => x > 2]]) // => [4, 6]
 */
const chainCall = (o, chain) => chain.reduce(
	(o, [prop, ...args]) => o[prop](...args),
	o
);

/**
 * Pattern matching utility that returns a value based on matching a value to cases.
 * Supports nested array paths for partial matching and a 'default' case.
 * @memberof module:obj
 * @param {*} value - The value to match (can be a string, array path, etc.)
 * @param {Object} cases - Object with cases to match against (supports 'default' key)
 * @returns {*} The matched case value, or false if no match
 * @example
 * _switch('a', { a: 1, b: 2, default: 0 }) // => 1
 * _switch('c', { a: 1, b: 2, default: 0 }) // => 0
 * _switch(['a', 'b'], { a: { b: 1 } }) // => 1
 */
const _switch = (value, cases) =>
	sub(cases, value) && sub(cases, value)['default'] || sub(cases, value)
	|| (value instanceof Array)
		&& value.length > 1 && _switch(value.slice(0, value.length - 1), cases)
	|| cases['default'] || false;

module.exports = {
	keyValue,
	isLiteral,
	clone,
	sub,
	patch,
	reduce,
	map,
	filter,
	traverse,
	chainCall,
	switch: _switch
};