blob: c246313f17d1ca8c65f2b7e0def951bb1b50894f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function shallowCopy(v: any): any {
return Array.isArray(v) ? v.slice() : { ...v };
}
export function isEmpty(v: any): boolean {
return !(Array.isArray(v) ? v.length : Object.keys(v).length);
}
function isObjectOrArray(v: any): boolean {
return Boolean(v && typeof v === 'object');
}
export function parsePath(path: string): string[] {
const parts: string[] = [];
let rest = path;
while (rest) {
const escapedMatch = /^\{([^{}]*)\}(?:\.(.*))?$/.exec(rest);
if (escapedMatch) {
parts.push(escapedMatch[1]);
rest = escapedMatch[2];
continue;
}
const normalMatch = /^([^.]*)(?:\.(.*))?$/.exec(rest);
if (normalMatch) {
parts.push(normalMatch[1]);
rest = normalMatch[2];
continue;
}
throw new Error(`Could not parse path ${path}`);
}
return parts;
}
export function makePath(parts: string[]): string {
return parts.map(p => (p.includes('.') ? `{${p}}` : p)).join('.');
}
function isAppend(key: string): boolean {
return key === '[append]' || key === '-1';
}
export function deepGet<T extends Record<string, any>>(value: T, path: string): any {
const parts = parsePath(path);
for (const part of parts) {
value = (value || {})[part];
}
return value;
}
export function deepSet<T extends Record<string, any>>(value: T, path: string, x: any): T {
const parts = parsePath(path);
let myKey = parts.shift()!; // Must be defined
const valueCopy = shallowCopy(value);
if (Array.isArray(valueCopy) && isAppend(myKey)) myKey = String(valueCopy.length);
if (parts.length) {
const nextKey = parts[0];
const rest = makePath(parts);
valueCopy[myKey] = deepSet(value[myKey] || (isAppend(nextKey) ? [] : {}), rest, x);
} else {
valueCopy[myKey] = x;
}
return valueCopy;
}
export function deepSetIfUnset<T extends Record<string, any>>(value: T, path: string, x: any): T {
if (typeof deepGet(value, path) !== 'undefined') return value;
return deepSet(value, path, x);
}
export function deepSetMulti<T extends Record<string, any>>(
value: T,
changes: Record<string, any>,
): T {
let newValue = value;
for (const k in changes) {
newValue = deepSet(newValue, k, changes[k]);
}
return newValue;
}
export function deepDelete<T extends Record<string, any>>(value: T, path: string): T {
const valueCopy = shallowCopy(value);
const parts = parsePath(path);
const firstKey = parts.shift()!; // Must be defined
if (parts.length) {
const firstKeyValue = value[firstKey];
if (firstKeyValue) {
const restPath = makePath(parts);
const prunedFirstKeyValue = deepDelete(value[firstKey], restPath);
if (isEmpty(prunedFirstKeyValue)) {
delete valueCopy[firstKey];
} else {
valueCopy[firstKey] = prunedFirstKeyValue;
}
} else {
delete valueCopy[firstKey];
}
} else {
if (Array.isArray(valueCopy) && !isNaN(Number(firstKey))) {
valueCopy.splice(Number(firstKey), 1);
} else {
delete valueCopy[firstKey];
}
}
return valueCopy;
}
export function deepMove<T extends Record<string, any>>(
value: T,
fromPath: string,
toPath: string,
): T {
value = deepSet(value, toPath, deepGet(value, fromPath));
value = deepDelete(value, fromPath);
return value;
}
export function deepExtend<T extends Record<string, any>>(target: T, diff: Record<string, any>): T {
if (typeof target !== 'object') throw new TypeError(`Invalid target`);
if (typeof diff !== 'object') throw new TypeError(`Invalid diff`);
const newValue = shallowCopy(target);
for (const key in diff) {
const targetValue = target[key];
const diffValue = diff[key];
if (typeof diffValue === 'undefined') {
delete newValue[key];
} else {
if (isObjectOrArray(targetValue) && isObjectOrArray(diffValue)) {
newValue[key] = deepExtend(targetValue, diffValue);
} else {
newValue[key] = diffValue;
}
}
}
return newValue;
}
export function whitelistKeys(obj: Record<string, any>, whitelist: string[]): Record<string, any> {
const newObj: Record<string, any> = {};
for (const w of whitelist) {
if (Object.prototype.hasOwnProperty.call(obj, w)) {
newObj[w] = obj[w];
}
}
return newObj;
}