/**
* Data structure manipulation tools.
*
* @module struct
* @license Apache-2.0
* @copyright Mat. 2018-present
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import type { AnyKey, Fun, JSAnyObj } from "../type/defs";
import { append } from "../array/list";
import { intersection } from "../array/set";
import { inc } from "../math/arithmetic";
import { quote } from "../string/transform";
import { space } from "../string/consts";
import {
isArray,
isBoolean,
isNumber,
isObject,
isString,
} from "../type/check";
/**
* Simple, basic data type (leaf). Serializable. Non recursive type.
*/
export type BasicData =
| boolean
| number
| string;
/**
* Check if value is of `BasicData` type. Non recursive check.
*
* @function isBasicData
* @param {unknown} c
* @returns {Boolean}
*/
export const isBasicData = (c: unknown): c is BasicData =>
isString(c) || isNumber(c) || isBoolean(c);
/**
* Check if value is of `BasicData` of `undefined` type. Non recursive check.
*
* @function isBasicDataOrUndefined
* @param {unknown} c
* @returns {Boolean | undefined}
*/
export const isBasicDataOrUndefined = (
c: unknown,
): c is BasicData | undefined =>
typeof c === "undefined" || isBasicData(c);
/**
* Apply `path` to an object `o`. Return element reachable through
* that `path` or `def` value.
*
* Example:
*
* ```
* access({ a: { b: [10, { c: 42 }] } }, ["a", "b", 1, "c"]) === 42
* ```
*
* @function access
* @param {InputType} input
* @param [path=[]]
* @param [def]
* @returns {OutputType | undefined}
*/
export function access<InputType> (
input: InputType,
): InputType;
export function access<InputType, OutputType> (
input: InputType,
path: readonly DataIndex<AnyKey>[],
): OutputType | undefined;
export function access<InputType, DefaultType, OutputType> (
input: InputType,
path: readonly DataIndex<AnyKey>[],
def: DefaultType,
): DefaultType | OutputType;
export function access<InputType, DefaultType, OutputType> (
input: InputType,
path: readonly DataIndex<AnyKey>[] = [],
def?: DefaultType,
): DefaultType | OutputType | undefined {
try {
return (
path.reduce((acc: InputType | undefined, p: AnyKey) => {
if (isObject(acc) || isArray(acc)) {
return (
acc as Record<AnyKey, unknown>
)[p] as InputType | undefined;
}
return undefined;
}, input) as OutputType
) ?? def;
} catch {
return def;
}
}
/**
* Safe version of standard JavaScript Object.assign();
* Throws when `base` and `ext` have conflicting keys - prevents
* accidental overwrite.
*
* @function assign
* @param {Object} base
* @param {Object} ext
* @returns {Object} base
*/
export function assign<B extends JSAnyObj, E extends JSAnyObj> (
base: B, ext: E,
): (B & E) {
const overlap = intersection(
Object.keys(base), Object.keys(ext),
);
if (overlap.length === 0) {
return Object.assign(base, ext);
} else {
throw new TypeError([
"struct.assign() - conflicting keys:",
overlap.map(x => quote(x)).join(", "),
].join(space()));
}
}
/**
* All "atomic" types.
*/
export type Atom =
| BasicData
| symbol
| bigint
| RegExp
| Fun
| null
| undefined;
/**
* Array - mutually recursive with Data (array node).
*/
export type DataArray<
T = BasicData,
ObjectPropType extends AnyKey = string,
> = Data<T, ObjectPropType>[];
/**
* Object - mutually recursive with Data (object node).
*/
export type DataObject<
T = BasicData,
PropType extends AnyKey = string,
> = {
[property in PropType]?: Data<T, PropType>;
};
/**
* Recursive data type (leaf or node).
*/
export type Data<
T = BasicData,
ObjectPropType extends AnyKey = string,
> =
| T
| DataArray<T, ObjectPropType>
| DataObject<T, ObjectPropType>;
/**
* Node-indexing type.
*/
export type DataIndex<
PropType extends AnyKey = string | number,
> = PropType;
/**
* Rewrite part of an object (first argument) reachable through passed path
* (second argument) with provided value (third argument). Creates new object
* with new data and references to all unchanged parts of the old object.
* This function implements copy-on-write semantics.
*
* @function rewrite
* @param {Data} o
* @param {DataIndex} path
* @param {Data} v
* @returns {Data}
*/
export function rewrite<
T = BasicData,
PropType extends AnyKey = string,
> (
o: Data<T, PropType>,
[h, ...t]: readonly DataIndex<PropType | number>[],
v: Data<T, PropType>,
): Data<T, PropType> {
if (!h || !(isObject(o) || isArray(o))) return v;
if (isObject(o)) {
const data = o;
if (!isString(h) || !(h in data))
throw new TypeError("struct.rewrite<object> - wrong path");
return {
...data,
[h]: rewrite(data[h], t, v),
};
} else {
const data = o as DataArray<T, PropType>;
if (!isNumber(h) || !(h in data))
throw new TypeError("struct.rewrite<array> - wrong path");
return append(data.slice(0, h)) ([
rewrite(data[h], t, v),
...data.slice(inc(h)),
]);
}
}