diff --git a/README.md b/README.md index 912c901..79d08a6 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ An array or object to replace `$forEach` block with. Template can contain `$forE - `$forEach.key` - environment variable name - `$forEach.value` - environment variable value +When using an object type iterator, nested values can be accessed with dot notation, (e.g. `$forEach.value.nestedKey`). + ## Examples -### Populate environment variables based on the list +### Populate environment variables based on the object #### Config ```yaml diff --git a/src/index.js b/src/index.js index d25b2c1..1b0568e 100644 --- a/src/index.js +++ b/src/index.js @@ -71,9 +71,12 @@ class ForEachPlugin { interpolate(template, key, value) { const stringified = JSON.stringify(template); - const interpolated = stringified - .replace(/\$forEach.key/g, key) - .replace(/\$forEach.value/g, value); + const keyRegex = /\$forEach.key/g; + const valueRegex = /\$forEach\.value/g; + + const template_with_keys_updated = stringified.replace(keyRegex, key); + + const interpolated = this.replaceValues(template_with_keys_updated, valueRegex, value); try { return JSON.parse(interpolated); @@ -82,6 +85,47 @@ class ForEachPlugin { } } + /** + * + * This function handles values that may be objects instead of strings. + * If the value variable is an object, check to see if the template indicates + * nested keys should be used. + */ + replaceValues(stringified, valueRegex, value) { + if (typeof value === 'string') { + return stringified.replace(valueRegex, value); + } else if (typeof value === 'object') { + const nestedKeyCaptureRegex = new RegExp(valueRegex.source + '\\.(?\\w*)', 'g'); + + const nestedKeyMatches = [...stringified.matchAll(nestedKeyCaptureRegex)]; + + const ValueObjectErrorMsg = ( + 'ForEach value is an object, but the template did not use a valid key from the object.\n' + + `Value: ${JSON.stringify(value)}\nTemplate: ${stringified}` + ) + + if (!nestedKeyMatches.length) { + throw new Error(ValueObjectErrorMsg); + } + + for (const nestedKeyMatch of nestedKeyMatches) { + const nestedKey = nestedKeyMatch.groups.nestedKey; + + if (!(nestedKey in value)) { + throw new Error(ValueObjectErrorMsg); + } + + const nestedValue = value[nestedKey] + + const nestedValueRegex = new RegExp(valueRegex.source + '\\.' + nestedKey); + + stringified = this.replaceValues(stringified, nestedValueRegex, nestedValue); + } + + return stringified + } + } + findAndReplace(obj, path) { let count = 0; @@ -103,6 +147,7 @@ class ForEachPlugin { const { iterator: rawIterator, template } = obj[key]; const iterator = {}; + if (rawIterator.$env) { Object.entries(process.env).forEach(([name, value]) => { if (name.match(rawIterator.$env)) { diff --git a/test/index.spec.js b/test/index.spec.js index 2380bd7..9610a8d 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -290,6 +290,46 @@ describe('ForEachPlugin', function() { ]); }); + it('should support using iterators with objects as values', function() { + const { plugin, serverless } = createTestInstance({ + custom: { + $forEach: { + iterator: { + bar: { + 'name': 'bar', + 'nicknames': { + 'main': 'barberator', + 'secondary': 'barbarella', + }, + }, + baz: { + 'name': 'baz', + 'nicknames': { + 'main': 'bazerator', + 'secondary': 'big baz', + }, + } + }, + template: { + '$forEach.key': '$forEach.value.name', + '$forEach.key_nickname': '$forEach.value.nicknames.main' + } + } + } + }); + + expect( + () => plugin.replace() + ).to.not.throw(); + + expect(serverless.service.custom).to.deep.equal({ + 'bar': 'bar', + 'bar_nickname': 'barberator', + 'baz': 'baz', + 'baz_nickname': 'bazerator', + }); + }); + it('should flatten one level when replacing array item and template is an array', function() { const { plugin, serverless } = createTestInstance({ custom: {