Skip to content

Commit

Permalink
Add index and size to Fn::Map and add Fn::Length.
Browse files Browse the repository at this point in the history
  • Loading branch information
Arjan van der Velde authored Dec 7, 2020
1 parent b21f9d0 commit 7519e00
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 50 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ For example, [`Fn::Include`](#fninclude) provides a convenient way to include fi
* [`Fn::Include`](#fninclude)
* [`Fn::Flatten`](#fnflatten)
* [`Fn::GetEnv`](#fngetenv)
* [`Fn::Length`](#fnlength)
* [`Fn::Map`](#fnmap)
* [`Fn::Merge`](#fnmerge)
* [`Fn::Outputs`](#fnoutputs)
Expand Down Expand Up @@ -238,6 +239,44 @@ Fn::Map:
}]
```

Custom variables can be specified as a single value, of as a list of up to three values. If a list is specified, the second variable is used as index and the third (if present) as size.

```yaml
Fn::Map:
- !Sequence [A, C]
- [NET, INDEX, N]
- Subnet${NET}:
Type: 'AWS::EC2::Subnet'
Properties:
CidrBlock: !Select [INDEX, !Cidr [MyCIDR, N, 8]]
```

```json
[{
"SubnetA": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"CidrBlock": { "Fn::Select": [ 0, { "Fn::Cidr": [ "MyCIDR", 3, 8 ] } ] }
}
}
}, {
"SubnetB": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"CidrBlock": { "Fn::Select": [ 1, { "Fn::Cidr": [ "MyCIDR", 3, 8 ] } ] }
}
}
}, {
"SubnetC": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"CidrBlock": { "Fn::Select": [ 2, { "Fn::Cidr": [ "MyCIDR", 3, 8 ] } ] }
}
}
}]
```


## Fn::Flatten

This function flattens an array a single level. This is useful for flattening out nested [`Fn::Map`](#fnmap) calls.
Expand Down Expand Up @@ -304,6 +343,11 @@ Resources:

The second argument is optional and provides the default value and will be used of the environmental variable is not defined. If the second argument is omitted `!GetEnv BUCKET_NAME` and the environmental variable is not defined then the compilation will fail.

## Fn::Length

`Fn::Length` returns the length of a list or expanded section.


## Fn::Merge

`Fn::Merge` will merge an array of objects into a single object. See [lodash / merge](https://devdocs.io/lodash~4/index#merge) for details on its behavior. This function is useful if you want to add functionality to an existing template if you want to merge objects of your template that have been created with [`Fn::Map`](#fnmap).
Expand Down
145 changes: 95 additions & 50 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ var _ = require('lodash'),

const { lowerCamelCase, upperCamelCase } = require('./lib/utils');


module.exports = function (options) {
var template = options.template,
base = parseLocation(options.url),
Expand All @@ -31,33 +30,69 @@ module.exports = function (options) {

async function recurse(base, scope, object) {
scope = _.clone(scope);
if (_.isArray(object)) return Promise.all(object.map(_.bind(recurse, this, base, scope)));
else if (_.isPlainObject(object)) {
if (_.isArray(object)) {
return Promise.all(object.map(_.bind(recurse, this, base, scope)))
} else if (_.isPlainObject(object)) {
if (object["Fn::Map"]) {
var args = object["Fn::Map"],
list = args[0],
placeholder = args[1],
body = args[args.length - 1];
if (args.length === 2) placeholder = '_';
return p.map(recurse(base, scope, list), function (replace) {
list = args[0],
placeholder = args[1],
body = args[args.length - 1],
idx, sz, i = 0,
hasindex = false,
hassize = false;
if (Array.isArray(placeholder)) { // multiple placeholders
idx = placeholder[1];
hasindex = true;
if (placeholder.length > 2) {
sz = placeholder[2];
hassize = true;
}
placeholder = placeholder[0];
}
if (args.length === 2) {
placeholder = '_';
}
return p.map(recurse(base, scope, list), function(replace) {
scope = _.clone(scope);
scope[placeholder] = replace;
if (hasindex) {
scope[idx] = i++;
}
var replaced = findAndReplace(scope, _.cloneDeep(body));
return recurse(base, scope, replaced);
}).then(function(obj) {
if (hassize) {
obj = findAndReplace({[sz] :obj.length}, obj);
}
return recurse(base, scope, obj);
});
} else if (object["Fn::Length"]) {
if (Array.isArray(object["Fn::Length"])) {
return object["Fn::Length"].length;
}
return recurse(base, scope, object["Fn::Length"]).then((x) => {
if (Array.isArray(x)) {
return x.length;
}
return 0;
});
} else if (object["Fn::Include"]) {
return include(base, scope, object["Fn::Include"]).then(function (json) {
if (!_.isPlainObject(json)) return json;
delete object["Fn::Include"];
_.defaults(object, json);
return object;
}).then(_.bind(findAndReplace, this, scope)).then(_.bind(recurse, this, base, scope));
return include(base, scope, object["Fn::Include"])
.then(function(json) {
if (!_.isPlainObject(json))
return json;
delete object["Fn::Include"];
_.defaults(object, json);
return object;
})
.then(_.bind(findAndReplace, this, scope))
.then(_.bind(recurse, this, base, scope));
} else if (object["Fn::Flatten"]) {
return recurse(base, scope, object["Fn::Flatten"]).then(function (json) {
return _.flatten(json);
});
return recurse(base, scope, object["Fn::Flatten"])
.then(function(json) { return _.flatten(json); });
} else if (object["Fn::Merge"]) {
return recurse(base, scope, object["Fn::Merge"]).then(function (json) {
return recurse(base, scope, object["Fn::Merge"]).then(function(json) {
delete object["Fn::Merge"];
_.defaults(object, _.merge.apply(_, json));
return object;
Expand All @@ -75,9 +110,8 @@ async function recurse(base, scope, object) {
return object;
});
} else if (object["Fn::Stringify"]) {
return recurse(base, scope, object["Fn::Stringify"]).then(function (json) {
return JSON.stringify(json);
});
return recurse(base, scope, object["Fn::Stringify"])
.then(function(json) { return JSON.stringify(json); });
} else if (object["Fn::UpperCamelCase"]) {
return upperCamelCase(object["Fn::UpperCamelCase"]);
} else if (object["Fn::LowerCamelCase"]) {
Expand All @@ -89,23 +123,27 @@ async function recurse(base, scope, object) {
return val === undefined ? args[1] : val;
}
const val = process.env[args];
if (val === undefined) throw new Error(`environmental variable ${args} is undefined`);
if (val === undefined) {
throw new Error(`environmental variable ${args} is undefined`);
}
return val;
} else if (object["Fn::Outputs"]) {
const outputs = await recurse(base, scope, object["Fn::Outputs"]);
const result = {};
for(const output in outputs) {
for (const output in outputs) {
const val = outputs[output];
const exp = { Export: { Name: { 'Fn::Sub': '${AWS::StackName}:' + output }} };
const exp = {
Export : {Name : {'Fn::Sub' : '${AWS::StackName}:' + output}}
};
if (!Array.isArray(val) && typeof val === 'object') {
result[output] = {
Value: { 'Fn::Sub': val.Value },
Condition: val.Condition,
Value : {'Fn::Sub' : val.Value},
Condition : val.Condition,
...exp,
}
} else {
result[output] = {
Value: { 'Fn::Sub': val },
Value : {'Fn::Sub' : val},
...exp,
}
}
Expand All @@ -119,7 +157,8 @@ async function recurse(base, scope, object) {
start = start.charCodeAt(0);
stop = stop.charCodeAt(0);
}
const seq = Array.from({ length: Math.floor((stop - start) / step) + 1 }, (_, i) => start + i * step);
const seq = Array.from({length : Math.floor((stop - start) / step) + 1},
(_, i) => start + i * step);
return isString ? seq.map((i) => String.fromCharCode(i)) : seq;
} else {
return p.props(_.mapValues(object, _.bind(recurse, this, base, scope)))
Expand All @@ -131,29 +170,35 @@ async function recurse(base, scope, object) {
}
}


function findAndReplace(scope, object) {
_.forEach(scope, function (replace, find) {
var regex = new RegExp('\\${' + find + '}', 'g');
if (_.isString(object) && object === find) {
object = replace;
} else if (_.isString(object) && find !== '_' && object.match(regex)) {
object = object.replace(regex, replace);
} else if (_.isArray(object)) {
object = object.map(_.bind(findAndReplace, this, scope));
} else if (_.isPlainObject(object)) {
object = _.mapKeys(object, function (value, key) {
return findAndReplace(scope, key);
});
_.keys(object).forEach(function (key) {
if (key === 'Fn::Map') return;
object[key] = findAndReplace(scope, object[key]);
});
return object;
return _.mapValues(object, _.bind(findAndReplace, this, scope));
} else {
return object;
}
});
if (_.isString(object)) {
_.forEach(scope, function(replace, find) {
if (object === find) {
object = replace;
}
});
}
if (_.isString(object)) {
_.forEach(scope, function(replace, find) {
let regex = new RegExp('\\${' + find + '}', 'g');
let found = false;
if (find !== '_' && object.match(regex)) {
object = object.replace(regex, replace);
}
});
}
if (_.isArray(object)) {
object = object.map(_.bind(findAndReplace, this, scope));
} else if (_.isPlainObject(object)) {
object = _.mapKeys(
object, function(value, key) { return findAndReplace(scope, key); });
_.keys(object).forEach(function(key) {
if (key === 'Fn::Map')
return;
object[key] = findAndReplace(scope, object[key]);
});
}
return object;
}

Expand Down
4 changes: 4 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var tags = [
{ short: 'Stringify', full: 'Fn::Stringify', type: 'sequence' },
{ short: 'Stringify', full: 'Fn::Stringify', type: 'mapping' },
{ short: 'Map', full: 'Fn::Map', type: 'sequence' },
{ short: 'Length', full: 'Fn::Length', type: 'sequence' },
{ short: 'Flatten', full: 'Fn::Flatten', type: 'sequence' },
{ short: 'GetEnv', full: 'Fn::GetEnv', type: 'sequence' },
{ short: 'GetEnv', full: 'Fn::GetEnv', type: 'scalar' },
Expand All @@ -23,6 +24,7 @@ var tags = [
{ short: 'GetAtt', full: 'Fn::GetAtt', type: 'sequence' },
{ short: 'GetAtt', full: 'Fn::GetAtt', type: 'scalar', dotSyntax: true },
{ short: 'GetAZs', full: 'Fn::GetAZs', type: 'sequence' },
{ short: 'GetAZs', full: 'Fn::GetAZs', type: 'scalar' },
{ short: 'ImportValue', full: 'Fn::ImportValue', type: 'scalar' },
{ short: 'ImportValue', full: 'Fn::ImportValue', type: 'mapping' },
{ short: 'Join', full: 'Fn::Join', type: 'sequence' },
Expand All @@ -42,6 +44,8 @@ var tags = [
{ short: 'If', full: 'Fn::If', type: 'sequence' },
{ short: 'Not', full: 'Fn::Not', type: 'sequence' },
{ short: 'Or', full: 'Fn::Or', type: 'sequence' },
{ short: 'Condition', full: 'Condition', type: 'scalar' },


].map(function(fn) {
return new yaml.Type('!' + fn.short, {
Expand Down
1 change: 1 addition & 0 deletions t/include.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var tests = [
'jmespath.yml',
'sequence.yml',
'deepmerge.yml',
'extendedmaps.json',
];
if(process.env['TEST_AWS']) tests.push('s3.json', 'api.js');

Expand Down
Loading

0 comments on commit 7519e00

Please sign in to comment.