2020-06-23 00:18:38 +08:00
|
|
|
/*
|
2021-04-23 06:24:41 +08:00
|
|
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
2020-06-23 00:18:38 +08:00
|
|
|
|
|
|
|
Licensed 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.
|
|
|
|
*/
|
|
|
|
|
2021-12-13 18:57:51 +08:00
|
|
|
import { arrayDiff, arrayUnion, arrayIntersection } from "./arrays";
|
2020-07-24 12:13:32 +08:00
|
|
|
|
2020-08-05 16:13:01 +08:00
|
|
|
type ObjectExcluding<O extends {}, P extends (keyof O)[]> = {[k in Exclude<keyof O, P[number]>]: O[k]};
|
|
|
|
|
2020-07-24 12:13:32 +08:00
|
|
|
/**
|
|
|
|
* Gets a new object which represents the provided object, excluding some properties.
|
|
|
|
* @param a The object to strip properties of. Must be defined.
|
|
|
|
* @param props The property names to remove.
|
|
|
|
* @returns The new object without the provided properties.
|
|
|
|
*/
|
2020-08-05 16:28:02 +08:00
|
|
|
export function objectExcluding<O extends {}, P extends Array<keyof O>>(a: O, props: P): ObjectExcluding<O, P> {
|
2020-07-24 12:13:32 +08:00
|
|
|
// We use a Map to avoid hammering the `delete` keyword, which is slow and painful.
|
2020-08-05 16:13:01 +08:00
|
|
|
const tempMap = new Map<keyof O, any>(Object.entries(a) as [keyof O, any][]);
|
2020-07-24 12:13:32 +08:00
|
|
|
for (const prop of props) {
|
|
|
|
tempMap.delete(prop);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert the map to an object again
|
|
|
|
return Array.from(tempMap.entries()).reduce((c, [k, v]) => {
|
|
|
|
c[k] = v;
|
|
|
|
return c;
|
2020-08-05 16:13:01 +08:00
|
|
|
}, {} as O);
|
2020-07-24 12:13:32 +08:00
|
|
|
}
|
|
|
|
|
2020-07-31 04:18:54 +08:00
|
|
|
/**
|
|
|
|
* Gets a new object which represents the provided object, with only some properties
|
|
|
|
* included.
|
|
|
|
* @param a The object to clone properties of. Must be defined.
|
|
|
|
* @param props The property names to keep.
|
|
|
|
* @returns The new object with only the provided properties.
|
|
|
|
*/
|
2020-08-05 16:28:02 +08:00
|
|
|
export function objectWithOnly<O extends {}, P extends Array<keyof O>>(a: O, props: P): {[k in P[number]]: O[k]} {
|
2020-08-05 16:13:01 +08:00
|
|
|
const existingProps = Object.keys(a) as (keyof O)[];
|
2020-07-31 04:18:54 +08:00
|
|
|
const diff = arrayDiff(existingProps, props);
|
|
|
|
if (diff.removed.length === 0) {
|
|
|
|
return objectShallowClone(a);
|
|
|
|
} else {
|
2020-08-05 16:13:01 +08:00
|
|
|
return objectExcluding(a, diff.removed) as {[k in P[number]]: O[k]};
|
2020-07-31 04:18:54 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-25 00:38:04 +08:00
|
|
|
/**
|
|
|
|
* Clones an object to a caller-controlled depth. When a propertyCloner is supplied, the
|
|
|
|
* object's properties will be passed through it with the return value used as the new
|
|
|
|
* object's type. This is intended to be used to deep clone a reference, but without
|
|
|
|
* having to deep clone the entire object. This function is safe to call recursively within
|
|
|
|
* the propertyCloner.
|
|
|
|
* @param a The object to clone. Must be defined.
|
|
|
|
* @param propertyCloner The function to clone the properties of the object with, optionally.
|
|
|
|
* First argument is the property key with the second being the current value.
|
|
|
|
* @returns A cloned object.
|
|
|
|
*/
|
2020-08-05 16:13:01 +08:00
|
|
|
export function objectShallowClone<O extends {}>(a: O, propertyCloner?: (k: keyof O, v: O[keyof O]) => any): O {
|
|
|
|
const newObj = {} as O;
|
|
|
|
for (const [k, v] of Object.entries(a) as [keyof O, O[keyof O]][]) {
|
2020-07-25 00:38:04 +08:00
|
|
|
newObj[k] = v;
|
|
|
|
if (propertyCloner) {
|
|
|
|
newObj[k] = propertyCloner(k, v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newObj;
|
|
|
|
}
|
|
|
|
|
2020-07-28 07:18:01 +08:00
|
|
|
/**
|
|
|
|
* Determines if any keys were added, removed, or changed between two objects.
|
|
|
|
* For changes, simple triple equal comparisons are done, not in-depth
|
|
|
|
* tree checking.
|
|
|
|
* @param a The first object. Must be defined.
|
|
|
|
* @param b The second object. Must be defined.
|
|
|
|
* @returns True if there's a difference between the objects, false otherwise
|
|
|
|
*/
|
2020-08-05 16:13:01 +08:00
|
|
|
export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
|
2021-02-19 08:00:10 +08:00
|
|
|
if (a === b) return false;
|
2020-07-28 07:18:01 +08:00
|
|
|
const aKeys = Object.keys(a);
|
|
|
|
const bKeys = Object.keys(b);
|
2021-02-24 21:40:22 +08:00
|
|
|
if (aKeys.length !== bKeys.length) return true;
|
2021-12-13 18:57:51 +08:00
|
|
|
const possibleChanges = arrayIntersection(aKeys, bKeys);
|
2021-02-19 08:15:00 +08:00
|
|
|
// if the amalgamation of both sets of keys has the a different length to the inputs then there must be a change
|
|
|
|
if (possibleChanges.length !== aKeys.length) return true;
|
|
|
|
|
2020-07-28 07:18:01 +08:00
|
|
|
return possibleChanges.some(k => a[k] !== b[k]);
|
|
|
|
}
|
|
|
|
|
2020-08-05 16:13:01 +08:00
|
|
|
type Diff<K> = { changed: K[], added: K[], removed: K[] };
|
|
|
|
|
2020-06-23 00:18:38 +08:00
|
|
|
/**
|
|
|
|
* Determines the keys added, changed, and removed between two objects.
|
|
|
|
* For changes, simple triple equal comparisons are done, not in-depth
|
|
|
|
* tree checking.
|
|
|
|
* @param a The first object. Must be defined.
|
|
|
|
* @param b The second object. Must be defined.
|
|
|
|
* @returns The difference between the keys of each object.
|
|
|
|
*/
|
2020-08-05 16:13:01 +08:00
|
|
|
export function objectDiff<O extends {}>(a: O, b: O): Diff<keyof O> {
|
|
|
|
const aKeys = Object.keys(a) as (keyof O)[];
|
|
|
|
const bKeys = Object.keys(b) as (keyof O)[];
|
2020-06-23 00:18:38 +08:00
|
|
|
const keyDiff = arrayDiff(aKeys, bKeys);
|
2021-12-13 18:57:51 +08:00
|
|
|
const possibleChanges = arrayIntersection(aKeys, bKeys);
|
2020-06-23 00:18:38 +08:00
|
|
|
const changes = possibleChanges.filter(k => a[k] !== b[k]);
|
|
|
|
|
2021-06-29 20:11:58 +08:00
|
|
|
return { changed: changes, added: keyDiff.added, removed: keyDiff.removed };
|
2020-06-23 00:18:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets all the key changes (added, removed, or value difference) between
|
|
|
|
* two objects. Triple equals is used to compare values, not in-depth tree
|
|
|
|
* checking.
|
|
|
|
* @param a The first object. Must be defined.
|
|
|
|
* @param b The second object. Must be defined.
|
|
|
|
* @returns The keys which have been added, removed, or changed between the
|
|
|
|
* two objects.
|
|
|
|
*/
|
2020-08-05 16:13:01 +08:00
|
|
|
export function objectKeyChanges<O extends {}>(a: O, b: O): (keyof O)[] {
|
2020-06-23 00:18:38 +08:00
|
|
|
const diff = objectDiff(a, b);
|
2021-12-13 18:57:51 +08:00
|
|
|
return arrayUnion(diff.removed, diff.added, diff.changed);
|
2020-06-23 00:18:38 +08:00
|
|
|
}
|
2020-06-23 04:14:43 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Clones an object by running it through JSON parsing. Note that this
|
|
|
|
* will destroy any complicated object types which do not translate to
|
|
|
|
* JSON.
|
|
|
|
* @param obj The object to clone.
|
|
|
|
* @returns The cloned object
|
|
|
|
*/
|
2020-08-05 16:13:01 +08:00
|
|
|
export function objectClone<O extends {}>(obj: O): O {
|
2020-06-23 04:14:43 +08:00
|
|
|
return JSON.parse(JSON.stringify(obj));
|
|
|
|
}
|