Last active
April 11, 2022 13:56
-
-
Save plukevdh/dec4b41d5b7d67f83be630afecee499e to your computer and use it in GitHub Desktop.
Ramda - Compact diff output for complex objects.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import R from 'ramda' | |
const isObject = R.compose(R.equals('Object'), R.type); | |
const allAreObjects = R.compose(R.all(isObject), R.values); | |
const hasLeft = R.has('left'); | |
const hasRight = R.has('right'); | |
const hasBoth = R.both(hasLeft, hasRight); | |
const isEqual = R.both(hasBoth, R.compose(R.apply(R.equals), R.values)); | |
const markAdded = R.compose(R.append(undefined), R.values); | |
const markRemoved = R.compose(R.prepend(undefined), R.values); | |
const isAddition = R.both(hasLeft, R.complement(hasRight)); | |
const isRemoval = R.both(R.complement(hasLeft), hasRight); | |
const objectDiff = R.curry(_diff); | |
function _diff(l, r) { | |
return R.compose( | |
R.map(R.cond([ | |
[isAddition, markAdded], | |
[isRemoval, markRemoved], | |
[hasBoth, R.ifElse( | |
allAreObjects, | |
R.compose(R.apply(objectDiff), R.values), | |
R.values) | |
] | |
])), | |
R.reject(isEqual), | |
R.useWith(R.mergeWith(R.merge), [R.map(R.objOf('left')), R.map(R.objOf('right'))]) | |
)(l, r); | |
} | |
export default objectDiff; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import objectDiff from 'objectDiff' | |
const lhs = { | |
one: 1, | |
two: [1,2], | |
three: { more: 'items', over: 'here' }, | |
four: 'four', | |
five: 5, | |
}; | |
const rhs = { | |
one: 1, | |
two: [1,3], | |
three: { more: 'robots', over: 'here' }, | |
four: 4, | |
six: 6, | |
}; | |
describe("deep diffing", () => { | |
const diff = objectDiff(lhs, rhs); | |
it('only detects changed', () => { | |
expect(diff).to.have.all.keys('two', 'three', 'four', 'five', 'six'); | |
}); | |
it('diffs arrays by returning the full array of items', () => { | |
expect(diff.two).to.eql([[1,2], [1,3]]) | |
}); | |
it('diffs objects by returning only changed items', () => { | |
expect(diff.three).to.eql({more: ['items', 'robots']}); | |
}); | |
it('detects plain value differences', () => { | |
expect(diff.four).to.eql(['four', 4]) | |
}); | |
it('detects removed values', () => { | |
expect(diff.five).to.eql([5, undefined]) | |
}); | |
it('detects added values', () => { | |
expect(diff.six).to.eql([undefined, 6]) | |
}) | |
}); |
@joeyfigaro I have something that does what you are describing which uses this gist. Mine actually takes a whitelist of fields that you want to ignore but if you don't pass a whitelist(spec object) it will just return True or False. I have attached below:
const isNotNil = R.complement(R.isNil);
const isNotObject = R.complement(isObject);
const isArray = R.compose(
R.equals('Array'),
R.type,
);
const isObject = R.compose(
R.equals('Object'),
R.type,
);
// Prop :: String
// StructPath :: { [String]: Array<Prop> | StructPath }
// ExpandedStructPath :: { [String]: true | ExpandedStructPath }
// Spec :: StructPath | Array<Prop> | Prop | Null
// ExpandStructPath :: Spec -> ExpandedStructPath
const expandStructPath = spec =>
R.cond([
[
R.is(String),
R.compose(
expandStructPath,
R.of,
),
],
[isObject, R.map(expandStructPath)],
[isArray, R.converge(R.zipObj, [R.identity, R.identity])],
[R.T, R.identity],
])(spec);
const _objDiffWhitelistFilter = R.curry((whiteList, diffRes) =>
R.compose(
R.reduce((acc, [key, diffResValue]) => {
const whiteListValue = R.path([key], whiteList);
// `null` `whiteList` indicates any diff is valid
// Skip any indexes that don't exist in the whiteList
if (isNotNil(whiteList) && R.isNil(whiteListValue)) {
return acc;
}
// if: `diffResValue` is an array, indicates a valid difference
// or: `whiteListValue` as a non object indicates it isn't spec'd as a substructure
// and there's no need to recurse any deeper.
if (isArray(diffResValue) || isNotObject(whiteListValue)) {
return R.reduced(true);
}
// Dig furtther down into the structure
return _objDiffWhitelistFilter(whiteListValue, diffResValue);
}, false),
R.toPairs,
)(diffRes),
);
// ObjDiffWhitelistFilter :: Spec -> ObjDiffResult -> Boolean
const objDiffWhitelistFilter = R.curry((whiteListStruct, objDiffRes) =>
R.compose(
_objDiffWhitelistFilter(R.__, objDiffRes),
expandStructPath,
)(whiteListStruct),
);
// NOTES:
// - When `WhiteListStruct` is `Nil`, assume all fields should be checked
// isObjDiff :: Spec -> Object -> Object -> Boolean
const isObjDiff = R.curry((whiteListStruct, left, right) =>
R.compose(
objDiffWhitelistFilter(whiteListStruct),
objDiff,
)(left, right),
);
Also some test cases to see what the spec/whitelist object looks like:
import isObjectDiff from './isObjectDiff';
const objectOne = {
foo: {
variantInfo: {
product: 'ONEAPP_SAMTV',
somethingExtra: 'hi mom',
featureOverrides: [
{ name: 'bug', type: 'STRING', value: 'feature' },
{ name: 'extra', type: 'STRING', value: 'feature' },
],
jsonForOverrides: 'A',
useJsonForOverrides: false,
},
},
};
const objectTwo = {
foo: {
variantInfo: {
product: 'ONEAPP_SAMTV',
featureOverrides: [{ name: 'bug', type: 'STRING', value: 'feature' }],
jsonForOverrides: '',
useJsonForOverrides: false,
},
},
};
describe('is Object diff', () => {
it('ignores extraneous parameters', () => {
const objectDiffStruct = {
foo: { variantInfo: ['useJsonForOverrides', 'product'] },
};
expect(isObjectDiff(objectDiffStruct)(objectOne, objectTwo)).toBe(false);
});
it('takes an array at the top of the structure', () => {
const objectDiffStruct = ['foo'];
expect(isObjectDiff(objectDiffStruct)(objectOne, objectTwo)).toBe(true);
});
it('defaults to all keys', () => {
const objectDiffStruct = null;
expect(isObjectDiff(objectDiffStruct)(objectOne, objectTwo)).toBe(true);
});
});
It also supports more complex whitelist/spec objects.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks!