diff --git a/README.md b/README.md index 9cd410e..aefe31b 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,10 @@ const aCase = { data: { complex1: { field1: 'value1' }, field2: 'value2', + collection3: [ + {id: '123', value: {field2: 'value3'}}, + {id: '456', value: {field2: 'value4'}}, + ] } }; @@ -175,6 +179,10 @@ const fields = fieldExtractor(aCase); const value1 = fields('complex1.field1'); const value2 = fields('field2'); +// Extract from collections +const value3 = fields('collection3[0].value.field2'); // By index +const value4 = fields('collection3[id:456].value.field2'); // By id + // Bulk extract as array const values = fields(['complex1.field1', 'field2', 'field3']); // ['value1', 'value2', undefined] diff --git a/src/modules/case.js b/src/modules/case.js index 11cc291..40860c8 100644 --- a/src/modules/case.js +++ b/src/modules/case.js @@ -1,4 +1,5 @@ const RADIX_36 = 36; +const COLLECTION_ITEM_PATTERN = /^(?[^\[\]]+)(?:\[(?:(?\d+)|id:(?[^\[\]]+))\])?$/; /** * @typedef CreatePayload @@ -67,7 +68,12 @@ export const fieldExtractor = (aCase) => (path) => { const singleFieldExtractor = (aCase) => (path) => { const caseData = dataExtractor(aCase); - return caseData ? field(caseData)(path.split('.')) : undefined; + return caseData ? field(caseData)(path.split('.').map(parsePathElement)) : undefined; +}; + +const parsePathElement = (pathElement) => { + const match = COLLECTION_ITEM_PATTERN.exec(pathElement); + return match ? match.groups : pathElement; }; const arrayFieldExtractor = (extractor) => (paths) => paths.map(extractor); @@ -87,13 +93,38 @@ const dataExtractor = (aCase) => aCase && (aCase.data || aCase.case_data); const field = (from) => (pathElements) => { const [nextElement, ...remainingElements] = pathElements; - if (typeof from === 'object' && Object.keys(from).includes(nextElement)) { - const nextValue = from[nextElement]; - if (remainingElements && remainingElements.length > 0) { - return field(nextValue)(remainingElements); - } else { - return nextValue; + const nextValue = extractNextElement(from, nextElement); + + if (remainingElements && remainingElements.length > 0) { + return field(nextValue)(remainingElements); + } else { + return nextValue; + } +}; + +const isObjectWithKey = (obj, key) => obj && typeof obj === 'object' && Object.keys(obj).includes(key); + +const extractCollectionItem = (collection, {colIndex, colId}) => { + if (!Array.isArray(collection)) { + return undefined; + } + + if (colId) { + return collection.find((item) => item.id === colId); + } + + return collection[parseInt(colIndex)]; +}; + +const extractNextElement = (from, {name, colIndex, colId}) => { + if (isObjectWithKey(from, name)) { + const nextValue = from[name]; + + if (colIndex || colId) { + return extractCollectionItem(nextValue, {colIndex, colId}); } + + return nextValue; } }; diff --git a/src/modules/case.test.js b/src/modules/case.test.js index efeb81f..719c26d 100644 --- a/src/modules/case.test.js +++ b/src/modules/case.test.js @@ -68,6 +68,17 @@ describe('fieldExtractor', () => { expect(fieldValue).toBeUndefined(); }); + test('should extract field as undefined when parent element is null', () => { + const aCase = { + data: { + level1: null + } + }; + + const fieldValue = fieldExtractor(aCase)('level1.level2'); + expect(fieldValue).toBeUndefined(); + }); + test('should extract field as undefined when case has no data', () => { const aCase = {}; @@ -114,6 +125,171 @@ describe('fieldExtractor', () => { }); }); + test('should extract simple collection item from case `data` using item index', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: 'value1'}, + {id: '456', value: 'value2'}, + {id: '789', value: 'value3'}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)([ + 'level1.level2[2].value', + 'level1.level2[1].value', + 'level1.level2[0].value', + ]); + expect(fieldValues).toEqual([ + 'value3', + 'value2', + 'value1', + ]); + }); + + test('should extract complex collection item from case `data` using item index', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: {key: 'value1'}}, + {id: '456', value: {key: 'value2'}}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[1].value.key'); + expect(fieldValues).toEqual('value2'); + }); + + test('should extract collection item as undefined when out of range', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: 'value1'}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[1].value'); + expect(fieldValues).toBeUndefined(); + }); + + test('should extract collection item as undefined when invalid index', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: 'value1'}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[a].value'); + expect(fieldValues).toBeUndefined(); + }); + + test('should extract collection item as undefined when not collection', () => { + const aCase = { + data: { + level1: { + level2: 'hello', + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[0].value'); + expect(fieldValues).toBeUndefined(); + }); + + test('should extract collection item as undefined when item malformed', () => { + const aCase = { + data: { + level1: { + level2: [true], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[0].value'); + expect(fieldValues).toBeUndefined(); + }); + + test('should extract collection item as undefined when null', () => { + const aCase = { + data: { + level1: { + level2: [null], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[0].value'); + expect(fieldValues).toBeUndefined(); + }); + + test('should extract simple collection item from case `data` using item ID', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: 'value1'}, + {id: '456', value: 'value2'}, + {id: '789', value: 'value3'}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)([ + 'level1.level2[id:456].value', + 'level1.level2[id:789].value', + 'level1.level2[id:123].value', + ]); + expect(fieldValues).toEqual([ + 'value2', + 'value3', + 'value1', + ]); + }); + + test('should extract complex collection item from case `data` using item ID', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: {key: 'value1'}}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[id:123].value.key'); + expect(fieldValues).toEqual('value1'); + }); + + test('should extract collection item as undefined when invalid ID', () => { + const aCase = { + data: { + level1: { + level2: [ + {id: '123', value: 'value1'}, + ], + } + } + }; + + const fieldValues = fieldExtractor(aCase)('level1.level2[id:456].value'); + expect(fieldValues).toBeUndefined(); + }); + test('should throw error if provided path is not of a supported type', () => { const aCase = {};