Skip to content

Commit

Permalink
fix(core/utils): Support traversing keys which contain dots in them u…
Browse files Browse the repository at this point in the history
…sing array notation (#7471)
  • Loading branch information
christopherthielen committed Oct 1, 2019
1 parent f2790a7 commit 30c7f2c
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 7 deletions.
19 changes: 19 additions & 0 deletions app/scripts/modules/core/src/utils/json/traverseObject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ describe('traverseObject', () => {
keyValuePairsWalked = [];
});

it('does not throw on null or undefined', () => {
const spyCallback = jasmine.createSpy();

expect(() => {
traverseObject(null, spyCallback);
traverseObject(undefined, spyCallback);
}).not.toThrow();

expect(spyCallback).not.toHaveBeenCalled();
});

it('walks simple properties of an object', () => {
const object = { foo: 1, bar: 2 };
traverseObject(object, defaultCallback);
Expand Down Expand Up @@ -79,4 +90,12 @@ describe('traverseObject', () => {
expect(keyValuePairsWalked[i++]).toEqual(['bar[0].name', 'abc']);
expect(keyValuePairsWalked[i++]).toEqual(['bar[1].name', 'def']);
});

it('uses array syntax when a key contains a dot', () => {
const object = { root: { 'foo.bar.baz': 1 } };
traverseObject(object, defaultCallback);
expect(keyValuePairsWalked.length).toEqual(2);
expect(keyValuePairsWalked[0][0]).toBe('root');
expect(keyValuePairsWalked[1][0]).toBe('root["foo.bar.baz"]');
});
});
28 changes: 21 additions & 7 deletions app/scripts/modules/core/src/utils/json/traverseObject.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forIn, isPlainObject } from 'lodash';
import { forIn, isPlainObject, isNumber, isNull, isUndefined } from 'lodash';

/**
* Deeply walks an object tree and invokes `callback` for each node
Expand All @@ -17,6 +17,21 @@ export const traverseObject = (object: object, callback: ITraverseCallback, trav

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

function constructPath(parent: string, child: string | number) {
// If child is a number, or a string that includes a dot, use array syntax
const childSegment = isNumber(child) ? `[${child}]` : child.includes('.') ? `["${child}"]` : child;
const useArraySyntax = childSegment.includes('[');
const parentSegment = !parent ? '' : useArraySyntax ? parent : `${parent}.`;

if (useArraySyntax && !parentSegment) {
throw new Error(
`Cannot construct path from '${childSegment}' using array syntax because the parent path is undefined.`,
);
}

return `${parentSegment}${childSegment}`;
}

const _traverseObject = (
contextPath: string,
obj: object,
Expand All @@ -29,15 +44,14 @@ const _traverseObject = (
}
}

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

0 comments on commit 30c7f2c

Please sign in to comment.