@tdb/util
Version:
Shared helpers and utilities.
182 lines (162 loc) • 4.68 kB
text/typescript
import { R } from './libs';
import { compact } from './value.array';
/**
* Walks an object tree implementing a visitor callback for each item.
*/
export function walk(obj: any | any[], fn: (obj: any | any[]) => void) {
const process = (item: any) => {
fn(item);
if (R.is(Object, item) || R.is(Array, item)) {
walk(item, fn); // <== RECURSION.
}
};
if (R.is(Array, obj)) {
(obj as any[]).forEach(item => process(item));
} else {
Object.keys(obj).forEach(key => process(obj[key]));
}
}
/**
* Builds an object from the given path
* (shallow or a period seperated deep path).
*/
export function build<T>(
keyPath: string,
root: { [key: string]: any },
value?: any, // Optional. Value to set, defaults to {}.
) {
const parts = prepareKeyPath(keyPath);
const result = { ...root };
let current = result;
let path = '';
parts.forEach((key, i) => {
const isLast = i === parts.length - 1;
const hasValue = isLast && value !== undefined;
if (hasValue) {
current[key] = value;
} else {
path = path ? `${path}.${key}` : key;
const level = current[key] !== undefined ? current[key] : {};
if (!R.is(Object, level)) {
throw new Error(
`Cannot build object '${keyPath}' as it will overwrite value '${level}' at '${path}'.`,
);
}
current[key] = { ...level };
current = current[key];
}
});
return result as T;
}
/**
* Walks the given path to retrieve a value.
*/
export function pluck<T>(keyPath: string, root: { [key: string]: any }) {
const parts = prepareKeyPath(keyPath);
let current = root;
let result: any;
let index = -1;
for (const key of parts) {
if (!current) {
break;
}
index++;
const isLast = index === parts.length - 1;
if (isLast) {
result = current[key];
} else {
current = current[key];
}
}
return result as T;
}
/**
* Remove values from the given object.
*/
export function remove(
keyPath: string,
root: { [key: string]: any },
options: { type?: 'LEAF' | 'PRUNE' } = {},
) {
type KeyMap = { [key: string]: any };
const { type = 'LEAF' } = options;
const isEmptyObject = (value: any) => R.equals(value, {});
const isWildcard = (value: any) => value === '*';
const process = (
parts: string[],
obj: KeyMap,
parent?: { key: string; obj: KeyMap },
) => {
const key = parts[0];
const nextKey = parts[1];
// Process rest of path (bottom up)
if (isWildcard(nextKey) || (parts.length > 1 && R.is(Object, obj[key]))) {
process(parts.slice(1), obj[key], { key, obj }); // <== RECURSION.
}
const isDeepest = parts.length === 1;
const value = obj && obj[key];
const isEmpty = isEmptyObject(value);
if (isWildcard(key) && !isDeepest) {
// NB: This may be changed in future for operations at a higher level in the path (?) .
throw new Error(
`Wild card can only be used at end of path (error: '${keyPath}')`,
);
}
let shouldDelete = false;
if (isEmpty && type === 'PRUNE') {
shouldDelete = true;
}
if (isDeepest && isWildcard(key)) {
shouldDelete = true;
}
if (isDeepest && !R.is(Object, value)) {
shouldDelete = true;
}
if (shouldDelete) {
if (parent) {
parent.obj[parent.key] = { ...parent.obj[parent.key] };
if (isWildcard(key)) {
// Wildcard - delete all children.
switch (type) {
case 'LEAF':
parent.obj[parent.key] = {};
break;
case 'PRUNE':
delete parent.obj[parent.key];
break;
default:
throw new Error(`Type '${type}' not supported.`);
}
} else {
// Deleted the specified child at the key-path.
delete parent.obj[parent.key][key];
}
} else {
delete obj[key];
}
}
return obj;
};
const parts = prepareKeyPath(keyPath);
return parts.length === 1 && parts[0] === '*'
? {} // NB: A root wildcard was passed ("*"), this can only mean an empty object.
: process(parts, { ...root });
}
/**
* Prunes a path from the given object.
*/
export function prune(keyPath: string, root: { [key: string]: any }) {
return remove(keyPath, root, { type: 'PRUNE' });
}
/**
* INTERNAL
*/
function prepareKeyPath(keyPath: string) {
keyPath = keyPath.trim();
if (keyPath.startsWith('.') || keyPath.endsWith('.')) {
throw new Error(
`The keyPath cannot start or end with a period (.): "${keyPath}"`,
);
}
return compact(keyPath.replace(/\s/g, '').split('.'));
}