struct/object.ts

/**
 * Struct - type declarations.
 *
 * @module struct
 * @license Apache-2.0
 * @copyright Mat. 2018-present
 */

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/prefer-function-type */

import type { Fun, JSAnyArrObj, JSAnyObj } from "../type/defs";
import { curry } from "../func/curry";
import { flow } from "../func/combinators";
import { btquote } from "../utils/misc";
import { isFunction, isObject } from "../type/check";




/**
 * Do the deep-copy of any JavaScript object
 * that doesn't contain functions.
 *
 * @function clone
 * @param {JSAnyArrObj} o
 * @returns {JSAnyArrObj}
 */
export const clone = flow(
    JSON.stringify,
    JSON.parse,
) as (o: JSAnyArrObj) => JSAnyArrObj;




/**
 * Construct `Object` from the result of `Object.entries()` call.
 *
 * ```
 * entries = [[k1, v1], ..., [kn, vn]]
 * ```
 *
 * Imitates Python's `dict()`.
 *
 * @function dict
 * @param {Array.<Array>} entries
 * @returns {Object}
 */
export function dict<T> (
    entries: readonly [PropertyKey, T][],
): { [k in PropertyKey]?: T; } {
    return entries.reduce(
        (acc, [k, v]) => ({ ...acc, [k]: v }), {},
    );
}




/**
 * Map (iteration) on objects - shallow.
 *
 * - `o` - `Object` to enumerate on.
 * - `f` - `Function` to call on each key, params:
 *     - `this` - bound to the enumerated object,
 *     - `kv` - current `[key, value]` array,
 *
 * `f` should return `[key, value]` array.
 *
 * @function objectMap
 * @param o Object
 * @param f Function
 * @returns Mapped object
 */
export const objectMap = curry((o: JSAnyObj, f: Fun) => {
    if (!isObject(o) || !isFunction(f)) throw new TypeError(
        "struct.objectMap() expected object and function, " +
        `got ${btquote(o)} and ${btquote(f)}`,
    );
    return dict(Object.entries(o).map((kv => f.call(o, kv))));
}) as {
    /* specialized-case overload (output keys related to input keys) */
    <
        In extends JSAnyObj,
        Keys extends keyof In,
        Out = any,
    >(
        o: In,
        f: (kv: [Keys, In[Keys]]) => [Keys, Out]
    ): { [k in Keys]: Out; };
    /* specialized-case - curried */
    <In extends JSAnyObj>(
        o: In
    ): {
        <Keys extends keyof In, Out = any>(
            f: (kv: [Keys, In[Keys]]) => [Keys, Out]
        ): { [k in Keys]: Out; };
    };
    /* general-case overload (output keys not related to input keys) */
    <
        In extends JSAnyObj,
        Keys extends keyof In,
        Out = any,
    >(
        o: In,
        f: (kv: [Keys, In[Keys]]) => [PropertyKey, Out]
    ): { [k in PropertyKey]?: Out; };
    /* general-case - curried */
    <In extends JSAnyObj>(
        o: In
    ): {
        <Keys extends keyof In, Out = any>(
            f: (kv: [Keys, In[Keys]]) => [PropertyKey, Out]
        ): { [k in PropertyKey]?: Out; };
    };
};




/**
 * Reduce (fold) on objects - shallow.
 *
 * - `o` - `Object` to enumerate on.
 * - `f` - `Function` to call on each key, params:
 *     - `this` - bound to the enumerated object,
 *     - `acc` - accumulated value,
 *     - `kv` - current `[key, value]` array,
 * - `init` - accumulated value initializer,
 *
 * `f` should return value of the same type as `init`.
 *
 * @function objectReduce
 * @param o Object
 * @param f Function
 * @param init T
 * @returns T
 */
export const objectReduce = curry((o: JSAnyObj, f: Fun, init: unknown) => {
    if (!isObject(o) || !isFunction(f)) throw new TypeError(
        "struct.objectReduce() expected object and function, " +
        `got ${btquote(o)} and ${btquote(f)}`,
    );
    return Object.entries(o).reduce((acc, kv) => f.call(o, acc, kv), init);
}) as {
    /* uncurried */
    <
        In extends JSAnyObj,
        Keys extends keyof In,
        Out,
    >(
        o: In,
        f: (acc: Out, kv: [Keys, In[Keys]]) => Out,
        init: Out,
    ): Out;
    /* curried */
    <In extends JSAnyObj>(o: In): {
        <Keys extends keyof In, Out>(
            f: (acc: Out, kv: [Keys, In[Keys]]) => Out
        ): {
            (init: Out): Out;
        };
    };
};




/**
 * When `o == { a: "b", c: "d" }`
 * then `swap(o) == { b: "a", d: "c" }`.
 *
 * @function swap
 * @param {JSAnyObj} o
 * @returns {JSAnyObj}
 */
export const swap = (
    o: JSAnyObj,
): JSAnyObj => objectMap(o) (([k, v]) => [v, k]);