Skip to content

Commit

Permalink
feat(core/utils): Add traverseObject which deeply walks object prop…
Browse files Browse the repository at this point in the history
…erties (#7452)
  • Loading branch information
christopherthielen authored Sep 27, 2019
1 parent e667bdf commit 1873094
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export * from './clipboard/CopyToClipboard';
export * from './debug';
export * from './json/JsonUtils';
export * from './json/traverseObject';
export * from './noop';
export * from './q';
export * from './renderIfFeature.component';
Expand Down
82 changes: 82 additions & 0 deletions app/scripts/modules/core/src/utils/json/traverseObject.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { traverseObject } from './traverseObject';

describe('traverseObject', () => {
let keyValuePairsWalked: Array<[string, any]>;
let i: number;

function defaultCallback(key: string, val: any) {
keyValuePairsWalked.push([key, val]);
}

beforeEach(() => {
i = 0;
keyValuePairsWalked = [];
});

it('walks simple properties of an object', () => {
const object = { foo: 1, bar: 2 };
traverseObject(object, defaultCallback);
expect(keyValuePairsWalked.length).toEqual(2);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar', 2]);
});

it('walks nested properties of an object', () => {
const object = { foo: 1, bar: { prop1: 1, prop2: 2 } };
traverseObject(object, defaultCallback);
expect(keyValuePairsWalked.length).toEqual(4);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar', { prop1: 1, prop2: 2 }]);
expect(keyValuePairsWalked[i++]).toEqual(['bar.prop1', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar.prop2', 2]);
});

it('only walks simple leaf nodes an object when traverseLeafNodesOnly is true', () => {
const object = { foo: 1, bar: { prop1: 1, prop2: 2 } };
traverseObject(object, defaultCallback, true);
expect(keyValuePairsWalked.length).toEqual(3);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar.prop1', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar.prop2', 2]);
});

it('walks array properties of an object', () => {
const object = { foo: 1, bar: [1, 2] };
traverseObject(object, defaultCallback);
expect(keyValuePairsWalked.length).toEqual(4);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar', [1, 2]]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[0]', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[1]', 2]);
});

it('walks only leaf array elements when traverseLeafNodesOnly is true', () => {
const object = { foo: 1, bar: [1, 2] };
traverseObject(object, defaultCallback, true);
expect(keyValuePairsWalked.length).toEqual(3);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[0]', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[1]', 2]);
});

it('walks nested objects inside array properties of an object', () => {
const object = { foo: 1, bar: [{ name: 'abc' }, { name: 'def' }] };
traverseObject(object, defaultCallback);
expect(keyValuePairsWalked.length).toEqual(6);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar', [{ name: 'abc' }, { name: 'def' }]]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[0]', { name: 'abc' }]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[0].name', 'abc']);
expect(keyValuePairsWalked[i++]).toEqual(['bar[1]', { name: 'def' }]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[1].name', 'def']);
});

it('walks only leaf nodes nested objects inside array properties of an object when traverseLeafNodesOnly is true', () => {
const object = { foo: 1, bar: [{ name: 'abc' }, { name: 'def' }] };
traverseObject(object, defaultCallback, true);
expect(keyValuePairsWalked.length).toEqual(3);
expect(keyValuePairsWalked[i++]).toEqual(['foo', 1]);
expect(keyValuePairsWalked[i++]).toEqual(['bar[0].name', 'abc']);
expect(keyValuePairsWalked[i++]).toEqual(['bar[1].name', 'def']);
});
});
44 changes: 44 additions & 0 deletions app/scripts/modules/core/src/utils/json/traverseObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { forIn, isPlainObject } from 'lodash';

/**
* Deeply walks an object tree and invokes `callback` for each node
*
* @param object the object to walk
* @param callback the callback to invoke on each simple leaf nodes.
* This callback receives the `path` and `value`.
* The `path` is a string representing the path into the object, which is compatible with lodash _.get(key).
* The `value` is the value of the simple leaf node.
* @param traverseLeafNodesOnly When true, only walks simple leaf nodes,
* i.e., properties that are neither a nested object, nor an array.
*/
export const traverseObject = (object: object, callback: ITraverseCallback, traverseLeafNodesOnly = false) => {
return _traverseObject(null, object, callback, traverseLeafNodesOnly);
};

type ITraverseCallback = (path: string, obj: object) => void;

const _traverseObject = (
contextPath: string,
obj: object,
callback: ITraverseCallback,
traverseLeafNodesOnly: boolean,
) => {
function maybeInvokeCallback(isLeafNode: boolean) {
if (contextPath !== null && (isLeafNode || !traverseLeafNodesOnly)) {
callback(contextPath, obj);
}
}

if (isPlainObject(obj)) {
maybeInvokeCallback(false);
forIn(obj, (val, key) => {
const path = contextPath ? contextPath + '.' : '';
return _traverseObject(`${path}${key}`, val, callback, traverseLeafNodesOnly);
});
} else if (Array.isArray(obj)) {
maybeInvokeCallback(false);
obj.forEach((val, idx) => _traverseObject(`${contextPath}[${idx}]`, val, callback, traverseLeafNodesOnly));
} else {
maybeInvokeCallback(true);
}
};

0 comments on commit 1873094

Please sign in to comment.