/*! * 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. */ /** * various helpers to deal with associative * arrays. If someone feels uncomfortable using * The config system, this is similar! */ import {IValueHolder} from "./Monad"; import {Es2019Array, pushChunked} from "./Es2019Array"; /** * A nop as assign functionality (aka ignore assign) */ class IgnoreAssign implements IValueHolder { constructor(private parent: any) {} set value(value: any | Array) { } get value(): any | Array { return this.parent; } }; /** * uses the known pattern from config * assign(target, key1, key2, key3).value = value; * @param target * @param keys */ export function assign(target: {[key: string]: any}, ...accessPath: string[]): IValueHolder { if (accessPath.length < 1) { return new IgnoreAssign(target); } const lastPathItem = buildPath(target, ...accessPath); let assigner: IValueHolder = new (class { set value(value: T | Array) { lastPathItem.target[lastPathItem.key] = value; } get value(): T | Array { return lastPathItem.target[lastPathItem.key]; } })(); return assigner; } export function append(target: {[key: string]: any}, ...accessPath: string[]): IValueHolder { if (accessPath.length < 1) { return new IgnoreAssign(target); } const lastPathItem = buildPath(target, ...accessPath); let appender: IValueHolder = new (class { set value(value: T | Array) { if(!Array.isArray(value)) { value = [value]; } if(!lastPathItem.target[lastPathItem.key]) { lastPathItem.target[lastPathItem.key] = value } else { if(!Array.isArray(lastPathItem.target[lastPathItem.key])) { lastPathItem.target[lastPathItem.key] = [lastPathItem.target[lastPathItem.key]]; } pushChunked(lastPathItem.target[lastPathItem.key], value); } } })(); return appender; } /** * uses the known pattern from config * assign(target, key1, key2, key3).value = value; * @param target * @param keys */ export function assignIf(condition: boolean, target: {[key: string]: any}, ...accessPath: string[]): IValueHolder { if ((!condition) || accessPath.length < 1) { return new IgnoreAssign(target); } return assign(target, ...accessPath); } /** * uses the known pattern from config * assign(target, key1, key2, key3).value = value; * @param target * @param keys */ export function appendIf(condition: boolean, target: {[key: string]: any}, ...accessPath: string[]): IValueHolder { if ((!condition) || accessPath.length < 1) { return new IgnoreAssign(target); } return append(target, ...accessPath); } export function resolve(target: {[key: string]: any}, ...accessPath: string[]): T | null { let ret = null; accessPath = flattenAccessPath(accessPath); let currPtr = target; for(let cnt = 0; cnt < accessPath.length; cnt++) { let accessKeyIndex: number | string = accessPath[cnt]; accessKeyIndex = arrayIndex(accessKeyIndex) != -1 ? arrayIndex(accessKeyIndex) : accessKeyIndex; currPtr = currPtr?.[accessKeyIndex]; if('undefined' == typeof currPtr) { return null; } ret = currPtr; } return currPtr as T; } function keyVal(key: string): string { let start = key.indexOf("["); if (start >= 0) { return key.substring(0, start); } else { return key; } } function arrayIndex(key: string): number { let start = key.indexOf("["); let end = key.indexOf("]"); if (start >= 0 && end > 0 && start < end) { return parseInt(key.substring(start + 1, end)); } else { return -1; } } function isArrayPos(currKey: string, arrPos: number): boolean { return currKey === "" && arrPos >= 0; } function isNoArray(arrPos: number): boolean { return arrPos == -1; } function alloc(arr: Array, length: number, defaultVal = {}) { let toAdd = []; toAdd.length = length; toAdd[length - 1] = defaultVal; pushChunked(arr, toAdd); } function flattenAccessPath(accessPath: string[]) { return new Es2019Array(...accessPath).flatMap((path: string) => path.split("[")) .map((path: string) => path.indexOf("]") != -1 ? "[" + path : path) .filter((path: string) => path != ""); } /** * builds up a path, only done if no data is present! * @param target * @param accessPath * @returns the last assignable entry */ export function buildPath(target: {[key: string]: any}, ...accessPath: string[]): {target: any, key: string | number} { accessPath = flattenAccessPath(accessPath); //we now have a pattern of having the array accessors always in separate items let parentPtr: any = target; let parKeyArrPos: string | number | null = null; let currKey: string | null = null; let arrPos = -1; for (let cnt = 0; cnt < accessPath.length; cnt++) { currKey = keyVal(accessPath[cnt]); arrPos = arrayIndex(accessPath[cnt]); //it now is either key or arrPos if (arrPos != -1) { //case root(array)[5] -> root must be array and allocate 5 elements //case root.item[5] root.item must be array and of 5 elements if(!Array.isArray(parentPtr)) { throw Error("Associative array referenced as index array in path reference"); } //we need to look ahead for proper allocation //not end reached let nextArrPos = -1; if(cnt < accessPath.length - 1) { nextArrPos = arrayIndex(accessPath[cnt + 1]) } let dataPresent = 'undefined' != typeof parentPtr?.[arrPos]; //no data present check here is needed, because alloc only reserves if not present alloc(parentPtr, arrPos + 1, nextArrPos != -1 ?[]: {}); parKeyArrPos = arrPos; //we now go to the reserved element if(cnt == accessPath.length - 1) { parentPtr[arrPos] = (dataPresent) ? parentPtr[arrPos] : null; } else { parentPtr = parentPtr[arrPos]; } } else { if(Array.isArray(parentPtr)) { throw Error("Index array referenced as associative array in path reference"); } //again look ahead whether the next value is an array or assoc array let nextArrPos = -1; if(cnt < accessPath.length - 1) { nextArrPos = arrayIndex(accessPath[cnt + 1]) } parKeyArrPos = currKey; let dataPresent = 'undefined' != typeof parentPtr?.[currKey]; if(cnt == accessPath.length - 1) { if(!dataPresent) { parentPtr[currKey] = null; } } else { if(!dataPresent) { parentPtr[currKey] = nextArrPos == -1 ? {} : []; } parentPtr = parentPtr[currKey]; } } } return {target: parentPtr, key: parKeyArrPos as string | number}; } export function deepCopy(fromAssoc: {[key: string]: any}): {[key: string]: any} { return JSON.parse(JSON.stringify(fromAssoc)); } /** * simple left to right merge * * @param assocArrays */ export function simpleShallowMerge(...assocArrays: {[key: string]: any}[]) { return shallowMerge(true, false, ...assocArrays); } function _appendWithOverwrite(withAppend: boolean, target: { [p: string]: any }, key: string, arr: {[key: string]: any}, toAssign: any) { if (!withAppend) { target[key] = arr[key]; } else { //overwrite means in this case, no double entries! //we do not a deep compare for now a single value compare suffices if ('undefined' == typeof target?.[key]) { target[key] = toAssign } else if (!Array.isArray(target[key])) { let oldVal = target[key]; let newVals: any[] = []; //TODO maybe deep deep compare here, but on the other hand it is //shallow toAssign.forEach((item: any) => { if (oldVal != item) { newVals.push(item); } }); target[key] = new Es2019Array(...[]); target[key].push(oldVal); pushChunked(target[key], newVals); } else { let oldVal = target[key]; let newVals: any[] = []; //TODO deep compare here toAssign.forEach((item: any) => { if (oldVal.indexOf(item) == -1) { newVals.push(item); } }); pushChunked(target[key], newVals); } } } function _appendWithoutOverwrite(withAppend: boolean, target: { [p: string]: any }, key: string, arr: {[key: string]: any}, toAssign: any) { if (!withAppend) { return; } else { //overwrite means in this case, no double entries! //we do not a deep compare for now a single value compare suffices if ('undefined' == typeof target?.[key]) { target[key] = toAssign } else if (!Array.isArray(target[key])) { let oldVal = target[key]; target[key] = new Es2019Array(...[]); target[key].push(oldVal); pushChunked(target[key], toAssign); } else { pushChunked(target[key], toAssign); } } } /** * Shallow merge as in config, but on raw associative arrays * * @param overwrite overwrite existing keys, if they exist with their subtrees * @param withAppend if a key exist append the values or drop them * Combination overwrite withappend filters doubles out of merged arrays * @param assocArrays array of assoc arres reduced right to left */ export function shallowMerge(overwrite = true, withAppend = false, ...assocArrays: {[key: string]: any}[]) { let target: {[key: string]: any} = {}; new Es2019Array(...assocArrays).map((arr: {[key: string]: any}) => { return {arr, keys: Object.keys(arr)}; }).forEach(({arr, keys}: {arr: {[key: string]: any}, keys: string[]}) => { keys.forEach((key: string) => { let toAssign = arr[key]; if(!Array.isArray(toAssign) && withAppend) { toAssign = new Es2019Array(...[toAssign]); } if(overwrite || !target?.[key]) { _appendWithOverwrite(withAppend, target, key, arr, toAssign); } else if(!overwrite && target?.[key]) { _appendWithoutOverwrite(withAppend, target, key, arr, toAssign); } }) }); return target; } //TODO test this, slightly altered from https://medium.com/@pancemarko/deep-equality-in-javascript-determining-if-two-objects-are-equal-bf98cf47e934 //he overlooked some optimizations and a shortcut at typeof! export function deepEqual(obj1: any, obj2: any): boolean | void { if(obj1 == obj2) { return false; } if(typeof obj1 != typeof obj2) { return false; } if(Array.isArray(obj1) && Array.isArray(obj2)) { if(obj1.length != obj2.length) { return; } //arrays must be equal, order as well, there is no way around it //this is the major limitation we have return obj1.every((item, cnt) => deepEqual(item, obj2[cnt])); } //string number and other primitives are filtered out here if("object" == typeof obj1 && "object" == typeof obj2) { let keys1 = Object.keys(obj1); let keys2 = Object.keys(obj2); if(keys1.length != keys2.length) { return false; } return keys1.every(key => keys2.indexOf(key) != -1) && keys1.every(key => deepEqual(obj1[key], obj2[key])); } return false; //done here no match found }