From 68e8d2f58e28f87d7a745bf0ba71791e6e376860 Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Sat, 19 Jun 2021 09:40:35 +0100 Subject: [PATCH 1/2] Case: Add extraction of collection item by index Part of #130 Ability to extract items from collection based on their index position --- README.md | 7 +++ src/modules/case.js | 39 +++++++++++--- src/modules/case.test.js | 106 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9cd410e..a2d591e 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,9 @@ 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 + // 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..a3f6811 100644 --- a/src/modules/case.js +++ b/src/modules/case.js @@ -1,4 +1,5 @@ const RADIX_36 = 36; +const COLLECTION_ITEM_PATTERN = /^([^\[\]]+)(?:\[([^\[\]]+)\])?$/; /** * @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 [_, name, itemId] = COLLECTION_ITEM_PATTERN.exec(pathElement); + return {name, itemId}; }; const arrayFieldExtractor = (extractor) => (paths) => paths.map(extractor); @@ -87,13 +93,32 @@ 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, itemId) => { + if (Array.isArray(collection)) { + return collection[parseInt(itemId)]; + } +}; + +const extractNextElement = (from, {name, itemId}) => { + if (isObjectWithKey(from, name)) { + const nextValue = from[name]; + + if (itemId) { + return extractCollectionItem(nextValue, itemId); } + + return nextValue; } }; diff --git a/src/modules/case.test.js b/src/modules/case.test.js index efeb81f..6e4ee66 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,101 @@ 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 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 throw error if provided path is not of a supported type', () => { const aCase = {}; From 35e2bf4bdb3b91adc6ca29dc0feac44bc06441ed Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Mon, 21 Jun 2021 11:13:15 +0100 Subject: [PATCH 2/2] Case: Add extraction of collection item by id Resolves #130 Ability to extract items from collection based on their unique identifier --- README.md | 1 + src/modules/case.js | 24 ++++++++------ src/modules/case.test.js | 70 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a2d591e..aefe31b 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ 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']); diff --git a/src/modules/case.js b/src/modules/case.js index a3f6811..40860c8 100644 --- a/src/modules/case.js +++ b/src/modules/case.js @@ -1,5 +1,5 @@ const RADIX_36 = 36; -const COLLECTION_ITEM_PATTERN = /^([^\[\]]+)(?:\[([^\[\]]+)\])?$/; +const COLLECTION_ITEM_PATTERN = /^(?[^\[\]]+)(?:\[(?:(?\d+)|id:(?[^\[\]]+))\])?$/; /** * @typedef CreatePayload @@ -72,8 +72,8 @@ const singleFieldExtractor = (aCase) => (path) => { }; const parsePathElement = (pathElement) => { - const [_, name, itemId] = COLLECTION_ITEM_PATTERN.exec(pathElement); - return {name, itemId}; + const match = COLLECTION_ITEM_PATTERN.exec(pathElement); + return match ? match.groups : pathElement; }; const arrayFieldExtractor = (extractor) => (paths) => paths.map(extractor); @@ -104,18 +104,24 @@ const field = (from) => (pathElements) => { const isObjectWithKey = (obj, key) => obj && typeof obj === 'object' && Object.keys(obj).includes(key); -const extractCollectionItem = (collection, itemId) => { - if (Array.isArray(collection)) { - return collection[parseInt(itemId)]; +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, itemId}) => { +const extractNextElement = (from, {name, colIndex, colId}) => { if (isObjectWithKey(from, name)) { const nextValue = from[name]; - if (itemId) { - return extractCollectionItem(nextValue, itemId); + 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 6e4ee66..719c26d 100644 --- a/src/modules/case.test.js +++ b/src/modules/case.test.js @@ -181,6 +181,21 @@ describe('fieldExtractor', () => { 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: { @@ -220,6 +235,61 @@ describe('fieldExtractor', () => { 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 = {};