Changeset View
Changeset View
Standalone View
Standalone View
lib/utils/objects.js
// @flow | // @flow | ||||
import stableStringify from 'fast-json-stable-stringify'; | import stableStringify from 'fast-json-stable-stringify'; | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import _difference from 'lodash/fp/difference.js'; | import _difference from 'lodash/fp/difference.js'; | ||||
import _isEqual from 'lodash/fp/isEqual.js'; | import _isEqual from 'lodash/fp/isEqual.js'; | ||||
import _isPlainObject from 'lodash/fp/isPlainObject.js'; | |||||
import stringHash from 'string-hash'; | import stringHash from 'string-hash'; | ||||
type Map<K, T> = { +[key: K]: T }; | type ObjectMap<K, T> = { +[key: K]: T }; | ||||
type NestedObjectMap<K, T> = { +[key: K]: T | NestedObjectMap<K, T> }; | |||||
function findMaximumDepth(obj: Object): ?{ path: string, depth: number } { | function findMaximumDepth(obj: Object): ?{ path: string, depth: number } { | ||||
let longestPath = null; | let longestPath = null; | ||||
let longestDepth = null; | let longestDepth = null; | ||||
for (const key in obj) { | for (const key in obj) { | ||||
const value = obj[key]; | const value = obj[key]; | ||||
if (typeof value !== 'object' || !value) { | if (typeof value !== 'object' || !value) { | ||||
if (!longestDepth) { | if (!longestDepth) { | ||||
Show All 14 Lines | for (const key in obj) { | ||||
} | } | ||||
} | } | ||||
if (!longestPath || !longestDepth) { | if (!longestPath || !longestDepth) { | ||||
return null; | return null; | ||||
} | } | ||||
return { path: longestPath, depth: longestDepth }; | return { path: longestPath, depth: longestDepth }; | ||||
} | } | ||||
function values<K, T>(map: Map<K, T>): T[] { | function values<K, T>(map: ObjectMap<K, T>): T[] { | ||||
return Object.values | return Object.values | ||||
? // https://github.com/facebook/flow/issues/2221 | ? // https://github.com/facebook/flow/issues/2221 | ||||
// $FlowFixMe - Object.values currently does not have good flow support | // $FlowFixMe - Object.values currently does not have good flow support | ||||
Object.values(map) | Object.values(map) | ||||
: Object.keys(map).map((key: K): T => map[key]); | : Object.keys(map).map((key: K): T => map[key]); | ||||
} | } | ||||
function keys<K, T>(map: ObjectMap<K, T>): K[] { | |||||
return Object.keys(map); | |||||
} | |||||
function assignValueWithKey<K, T>( | |||||
obj: NestedObjectMap<K, T>, | |||||
key: K, | |||||
value: T | NestedObjectMap<K, T>, | |||||
): NestedObjectMap<K, T> { | |||||
return { | |||||
...obj, | |||||
...Object.fromEntries([[key, value]]), | |||||
}; | |||||
} | |||||
function hash(obj: ?Object): number { | function hash(obj: ?Object): number { | ||||
if (!obj) { | if (!obj) { | ||||
return -1; | return -1; | ||||
} | } | ||||
return stringHash(stableStringify(obj)); | return stringHash(stableStringify(obj)); | ||||
} | } | ||||
// returns an object with properties from obj1 not included in obj2 | |||||
function deepDiff<K, T>( | |||||
obj1: NestedObjectMap<K, T>, | |||||
obj2: NestedObjectMap<K, T>, | |||||
): NestedObjectMap<K, T> { | |||||
let diff: NestedObjectMap<K, T> = {}; | |||||
keys(obj1).forEach((key: K) => { | |||||
if (_isEqual(obj1[key], obj2[key])) { | |||||
return; | |||||
} | |||||
if (!_isPlainObject(obj1[key]) || !_isPlainObject(obj2[key])) { | |||||
diff = assignValueWithKey(diff, key, obj1[key]); | |||||
return; | |||||
} | |||||
const nestedObj1: ObjectMap<K, T> = (obj1[key]: any); | |||||
const nestedObj2: ObjectMap<K, T> = (obj2[key]: any); | |||||
const nestedDiff = deepDiff(nestedObj1, nestedObj2); | |||||
if (Object.keys(nestedDiff).length > 0) { | |||||
diff = assignValueWithKey(diff, key, nestedDiff); | |||||
} | |||||
}); | |||||
return diff; | |||||
} | |||||
function assertObjectsAreEqual<K, T>( | function assertObjectsAreEqual<K, T>( | ||||
processedObject: Map<K, T>, | processedObject: ObjectMap<K, T>, | ||||
expectedObject: Map<K, T>, | expectedObject: ObjectMap<K, T>, | ||||
message: string, | message: string, | ||||
) { | ) { | ||||
const processedObjectKeys = Object.keys(processedObject); | const processedObjectKeys = Object.keys(processedObject); | ||||
const expectedObjectKeys = Object.keys(expectedObject); | const expectedObjectKeys = Object.keys(expectedObject); | ||||
const inProcessedButNotExpected = | const inProcessedButNotExpected = | ||||
_difference(processedObjectKeys)(expectedObjectKeys); | _difference(processedObjectKeys)(expectedObjectKeys); | ||||
const inExpectedButNotProcessed = | const inExpectedButNotProcessed = | ||||
_difference(expectedObjectKeys)(processedObjectKeys); | _difference(expectedObjectKeys)(processedObjectKeys); | ||||
invariant( | invariant( | ||||
_isEqual(processedObject)(expectedObject), | _isEqual(processedObject)(expectedObject), | ||||
`${message}: Objects should be equal.` + | `${message}: Objects should be equal.` + | ||||
` Object keys processed but not expected:` + | ` Object keys processed but not expected:` + | ||||
` ${inExpectedButNotProcessed.toString()}` + | ` ${inExpectedButNotProcessed.toString()}` + | ||||
` Object keys expected but not processed:` + | ` Object keys expected but not processed:` + | ||||
` ${inProcessedButNotExpected.toString()}`, | ` ${inProcessedButNotExpected.toString()}`, | ||||
); | ); | ||||
} | } | ||||
export { findMaximumDepth, values, hash, assertObjectsAreEqual }; | export { findMaximumDepth, values, hash, assertObjectsAreEqual, deepDiff }; |