Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow iterator values to be objects #39

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 48 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 + '\\.(?<nestedKey>\\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;

Expand All @@ -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)) {
Expand Down
92 changes: 92 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,98 @@ 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',
});
});

describe('should throw an error when value is object and no valid key is used in template', function() {
[
{
scenario: 'no value key used in template',
config: {
iterator: {
bar: {
'name': 'bar',
},
baz: {
'name': 'baz',
}
},
template: {
'$forEach.key': '$forEach.value',
},
},
message: 'ForEach value is an object, but the template did not use a valid key from the object.\nValue: {"name":"bar"}\nTemplate: {"bar":"$forEach.value"}'
},
{
scenario: 'invalid key used in template',
config: {
iterator: {
bar: {
'name': 'bar',
},
baz: {
'name': 'baz',
}
},
template: {
'$forEach.key': '$forEach.value.invalidKey',
}
},
message: 'ForEach value is an object, but the template did not use a valid key from the object.\nValue: {"name":"bar"}\nTemplate: {"bar":"$forEach.value.invalidKey"}',
}
].forEach(({ scenario, config, message }) => {
it(scenario, function() {
const { plugin } = createTestInstance({
custom: {
$forEach: config
}
});

expect(
() => plugin.replace()
).to.throw(message);

});
});
});

it('should flatten one level when replacing array item and template is an array', function() {
const { plugin, serverless } = createTestInstance({
custom: {
Expand Down