diff --git a/app/scripts/modules/core/src/utils/json/traverseObject.spec.ts b/app/scripts/modules/core/src/utils/json/traverseObject.spec.ts index 18c9346e5e9..3cd7dd598f7 100644 --- a/app/scripts/modules/core/src/utils/json/traverseObject.spec.ts +++ b/app/scripts/modules/core/src/utils/json/traverseObject.spec.ts @@ -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); @@ -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"]'); + }); }); diff --git a/app/scripts/modules/core/src/utils/json/traverseObject.ts b/app/scripts/modules/core/src/utils/json/traverseObject.ts index 9aa40ebdf39..a7970f309ae 100644 --- a/app/scripts/modules/core/src/utils/json/traverseObject.ts +++ b/app/scripts/modules/core/src/utils/json/traverseObject.ts @@ -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 @@ -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, @@ -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); }