allow function in byProperty configurations

This commit is contained in:
Tobias Koppers 2021-01-05 11:29:54 +01:00
parent 67d2e227f4
commit b067bad545
2 changed files with 225 additions and 36 deletions

View File

@ -10,6 +10,7 @@ const mergeCache = new WeakMap();
/** @type {WeakMap<object, Map<string, Map<string|number|boolean, object>>>} */
const setPropertyCache = new WeakMap();
const DELETE = Symbol("DELETE");
const DYNAMIC_INFO = Symbol("cleverMerge dynamic info");
/**
* Merges two given objects and caches the result to avoid computation if same objects passed as arguments again.
@ -81,12 +82,18 @@ const cachedSetProperty = (obj, property, value) => {
* @property {Map<string, any>} byValues value depending on selector property, merged with base
*/
/** @type {WeakMap<object, Map<string, ObjectParsedPropertyEntry>>} */
/**
* @typedef {Object} ParsedObject
* @property {Map<string, ObjectParsedPropertyEntry>} static static properties (key is property name)
* @property {{ byProperty: string, fn: Function } | undefined} dynamic dynamic part
*/
/** @type {WeakMap<object, ParsedObject>} */
const parseCache = new WeakMap();
/**
* @param {object} obj the object
* @returns {Map<string, ObjectParsedPropertyEntry>} parsed properties
* @returns {ParsedObject} parsed object
*/
const cachedParseObject = obj => {
const entry = parseCache.get(obj);
@ -98,10 +105,11 @@ const cachedParseObject = obj => {
/**
* @param {object} obj the object
* @returns {Map<string, ObjectParsedPropertyEntry>} parsed properties
* @returns {ParsedObject} parsed object
*/
const parseObject = obj => {
const info = new Map();
let dynamicInfo;
const getInfo = p => {
const entry = info.get(p);
if (entry !== undefined) return entry;
@ -117,40 +125,60 @@ const parseObject = obj => {
if (key.startsWith("by")) {
const byProperty = key;
const byObj = obj[byProperty];
for (const byValue of Object.keys(byObj)) {
const obj = byObj[byValue];
for (const key of Object.keys(obj)) {
const entry = getInfo(key);
if (entry.byProperty === undefined) {
entry.byProperty = byProperty;
entry.byValues = new Map();
} else if (entry.byProperty !== byProperty) {
throw new Error(
`${byProperty} and ${entry.byProperty} for a single property is not supported`
);
}
entry.byValues.set(byValue, obj[key]);
if (byValue === "default") {
for (const otherByValue of Object.keys(byObj)) {
if (!entry.byValues.has(otherByValue))
entry.byValues.set(otherByValue, undefined);
if (typeof byObj === "object") {
for (const byValue of Object.keys(byObj)) {
const obj = byObj[byValue];
for (const key of Object.keys(obj)) {
const entry = getInfo(key);
if (entry.byProperty === undefined) {
entry.byProperty = byProperty;
entry.byValues = new Map();
} else if (entry.byProperty !== byProperty) {
throw new Error(
`${byProperty} and ${entry.byProperty} for a single property is not supported`
);
}
entry.byValues.set(byValue, obj[key]);
if (byValue === "default") {
for (const otherByValue of Object.keys(byObj)) {
if (!entry.byValues.has(otherByValue))
entry.byValues.set(otherByValue, undefined);
}
}
}
}
} else if (typeof byObj === "function") {
if (dynamicInfo === undefined) {
dynamicInfo = {
byProperty: key,
fn: byObj
};
} else {
throw new Error(
`${key} and ${dynamicInfo.byProperty} when both are functions is not supported`
);
}
} else {
const entry = getInfo(key);
entry.base = obj[key];
}
} else {
const entry = getInfo(key);
entry.base = obj[key];
}
}
return info;
return {
static: info,
dynamic: dynamicInfo
};
};
/**
* @param {Map<string, ObjectParsedPropertyEntry>} info property entries
* @param {Map<string, ObjectParsedPropertyEntry>} info static properties (key is property name)
* @param {{ byProperty: string, fn: Function } | undefined} dynamicInfo dynamic part
* @returns {object} the object
*/
const serializeObject = info => {
const serializeObject = (info, dynamicInfo) => {
const obj = {};
// Setup byProperty structure
for (const entry of info.values()) {
@ -174,6 +202,9 @@ const serializeObject = info => {
}
}
}
if (dynamicInfo !== undefined) {
obj[dynamicInfo.byProperty] = dynamicInfo.fn;
}
return obj;
};
@ -216,28 +247,53 @@ const getValueType = value => {
const cleverMerge = (first, second, internalCaching = false) => {
if (second === undefined) return first;
if (first === undefined) return second;
const firstInfo = internalCaching
const firstObject = internalCaching
? cachedParseObject(first)
: parseObject(first);
const secondInfo = internalCaching
const { static: firstInfo, dynamic: firstDynamicInfo } = firstObject;
// If the first argument has a dynamic part we modify the dynamic part to merge the second argument
if (firstDynamicInfo !== undefined) {
let { byProperty, fn } = firstDynamicInfo;
const fnInfo = fn[DYNAMIC_INFO];
if (fnInfo) {
second = internalCaching
? cachedCleverMerge(fnInfo[1], second)
: cleverMerge(fnInfo[1], second, false);
fn = fnInfo[0];
}
const newFn = (...args) => {
const fnResult = fn(...args);
if (typeof fnResult !== "object" || fnResult === null) return fnResult;
return internalCaching
? cachedCleverMerge(fnResult, second)
: cleverMerge(fnResult, second, false);
};
newFn[DYNAMIC_INFO] = [fn, second];
return serializeObject(firstObject.static, { byProperty, fn: newFn });
}
// If the first part is static only, we merge the static parts and keep the dynamic part of the second argument
const secondObject = internalCaching
? cachedParseObject(second)
: parseObject(second);
const { static: secondInfo, dynamic: secondDynamicInfo } = secondObject;
/** @type {Map<string, ObjectParsedPropertyEntry>} */
const result = new Map();
const resultInfo = new Map();
for (const [key, firstEntry] of firstInfo) {
const secondEntry = secondInfo.get(key);
const entry =
secondEntry !== undefined
? mergeEntries(firstEntry, secondEntry, internalCaching)
: firstEntry;
result.set(key, entry);
resultInfo.set(key, entry);
}
for (const [key, secondEntry] of secondInfo) {
if (!firstInfo.has(key)) {
result.set(key, secondEntry);
resultInfo.set(key, secondEntry);
}
}
return serializeObject(result);
return serializeObject(resultInfo, secondDynamicInfo);
};
/**
@ -469,12 +525,12 @@ const resolveByProperty = (obj, byProperty, ...values) => {
return remaining;
}
} else if (typeof byValue === "function") {
const result = resolveByProperty(
byValue.apply(null, values),
byProperty,
...values
const result = byValue.apply(null, values);
if (typeof result !== "object" || result === null) return result;
return cleverMerge(
remaining,
resolveByProperty(result, byProperty, ...values)
);
return cleverMerge(remaining, result);
}
};

View File

@ -3,7 +3,8 @@
const {
cleverMerge,
DELETE,
removeOperations
removeOperations,
resolveByProperty
} = require("../lib/util/cleverMerge");
describe("cleverMerge", () => {
@ -517,12 +518,144 @@ describe("cleverMerge", () => {
}
}
}
],
dynamicSecond: [
{
a: 4, // keep
b: 5, // static override
c: 6 // dynamic override
},
{
b: 50,
y: 20,
byArguments: (x, y, z) => ({
c: 60,
x,
y,
z
})
},
{
a: 4,
b: 50,
c: 60,
x: 1,
y: 2,
z: 3
}
],
dynamicBoth: [
{
a: 4, // keep
b: 5, // static override
c: 6, // dynamic override
byArguments: (x, y, z) => ({
x, // keep
y, // static override
z // dynamic override
})
},
{
b: 50,
y: 20,
byArguments: (x, y, z) => ({
c: 60,
z: z * 10
})
},
{
a: 4,
b: 50,
c: 60,
x: 1,
y: 20,
z: 30
}
],
dynamicChained: [
cleverMerge(
{
a: 6, // keep
b: 7, // static override
c: 8, // dynamic override
d: 9, // static override (3rd)
e: 10, // dynamic override (3rd)
byArguments: (x, y, z, v, w) => ({
x, // keep
y, // static override
z, // dynamic override
v, // static override (3rd)
w // dynamic override (3rd)
})
},
{
b: 70,
y: 20,
byArguments: (x, y, z) => ({
c: 80,
z: z * 10
})
}
),
{
d: 90,
v: 40,
byArguments: (x, y, z, v, w) => ({
e: 100,
w: w * 10
})
},
{
a: 6,
b: 70,
c: 80,
d: 90,
e: 100,
x: 1,
y: 20,
z: 30,
v: 40,
w: 50
}
],
dynamicFalse1: [
{
a: 1,
byArguments: () => false
},
{
b: 2
},
false
],
dynamicFalse2: [
{
a: 1
},
{
b: 2,
byArguments: () => false
},
false
],
dynamicFalse3: [
{
a: 1,
byArguments: () => false
},
{
b: 2,
byArguments: () => false
},
false
]
};
for (const key of Object.keys(cases)) {
const testCase = cases[key];
it(`should merge ${key} correctly`, () => {
expect(cleverMerge(testCase[0], testCase[1])).toEqual(testCase[2]);
let merged = cleverMerge(testCase[0], testCase[1]);
merged = resolveByProperty(merged, "byArguments", 1, 2, 3, 4, 5);
expect(merged).toEqual(testCase[2]);
});
}