diff --git a/README.md b/README.md index 042dbe2..0082c52 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,58 @@ By default the string `_` is used as the variable in the map function. }] ``` +## Fn::Flatten + +This function flattens an array a single level. This is useful for flattening out nested `Fn::Map` calls. + +```json +{ + "Fn::Flatten": { + "Fn::Map": [ + [80, 443], "$", { + "Fn::Map": [ + ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], { + "CirdIp": "_", + "FromPort": "$", + "ToPort": "$" + } + ] + } + ] + } +}, +``` + +Results in: + +```json +[{ + "CirdIp": "10.0.0.0/8", + "FromPort": "80", + "ToPort": "80" +}, { + "CirdIp": "172.16.0.0/12", + "FromPort": "80", + "ToPort": "80" +}, { + "CirdIp": "192.168.0.0/16", + "FromPort": "80", + "ToPort": "80" +}, { + "CirdIp": "10.0.0.0/8", + "FromPort": "443", + "ToPort": "443" +}, { + "CirdIp": "172.16.0.0/12", + "FromPort": "443", + "ToPort": "443" +}, { + "CirdIp": "192.168.0.0/16", + "FromPort": "443", + "ToPort": "443" +}] +``` + ## Examples See [/examples](https://github.com/monken/cfn-include/tree/master/examples) for templates that call an API Gateway endpoint to collect AMI IDs for all regions. diff --git a/index.js b/index.js index 90f1cfa..b0820b8 100644 --- a/index.js +++ b/index.js @@ -22,31 +22,31 @@ 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)) { - return Promise.try(function() { - 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 Promise.resolve(list.map(function(replace) { - scope = _.clone(scope); - scope[placeholder] = replace; - var replaced = findAndReplace(scope, _.cloneDeep(body)); - return recurse(base, scope, replaced); - })); - } else if (object["Fn::Include"]) { - return include(base, object["Fn::Include"]).then(function(json) { - delete object["Fn::Include"]; - _.extend(object, json); - return object; - }).then(_.bind(findAndReplace, this, scope)).then(_.bind(recurse, this, base, scope)); - } else { + 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 Promise.all(list.map(function(replace) { + scope = _.clone(scope); + scope[placeholder] = replace; + var replaced = findAndReplace(scope, _.cloneDeep(body)); + return recurse(base, scope, replaced); + })); + } else if (object["Fn::Include"]) { + return include(base, object["Fn::Include"]).then(function(json) { + delete object["Fn::Include"]; + _.extend(object, json); return object; - } - }).then(function(object) { - return Promise.props(_.mapValues(object, _.bind(recurse, this, base, scope))); - }); + }).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); + }); + } else { + return Promise.props(_.mapValues(object, _.bind(recurse, this, base, scope))) + } } else { return object; } diff --git a/t/include.js b/t/include.js index e4dc7c4..415b797 100644 --- a/t/include.js +++ b/t/include.js @@ -2,10 +2,10 @@ var include = require('../index'), assert = require('assert'), fs = require('fs'); -var tests = ['map', 'literal', 'location']; +var tests = ['location', 'literal', 'map', 'flatten']; if(process.env['TEST_S3']) tests.push('s3'); -//var tests = ['map']; +//var tests = ['flatten']; tests.forEach(function(file) { var tests = require('./tests/' + file + '.json'); diff --git a/t/tests/flatten.json b/t/tests/flatten.json new file mode 100644 index 0000000..3ab5fe6 --- /dev/null +++ b/t/tests/flatten.json @@ -0,0 +1,178 @@ +{ + "basics": [{ + "name": "flatten flat list", + "template": { + "Fn::Flatten": [1, 2, 3] + }, + "output": [1, 2, 3] + }], + "flatten nested map": [{ + "name": "nesting with underscore", + "template": { + "Fn::Flatten": { + "Fn::Map": [ + [1, 2], { + "Fn::Map": [ + [3, 4], { + "foo": "_" + } + ] + } + ] + } + }, + "output": [{ + "foo": "3" + }, { + "foo": "4" + }, { + "foo": "3" + }, { + "foo": "4" + }] + + }, { + "name": "flatten custom placeholder", + "template": { + "Fn::Flatten": { + "Fn::Map": [ + [1, 2], "$", { + "Fn::Map": [ + [3, 4], { + "foo": "_", + "bar": "$" + } + ] + } + ] + } + }, + "output": [{ + "foo": "3", + "bar": "1" + }, { + "foo": "4", + "bar": "1" + }, { + "foo": "3", + "bar": "2" + }, { + "foo": "4", + "bar": "2" + }] + + }, { + "name": "nesting with underscore and includes", + "template": { + "Fn::Flatten": { + "Fn::Map": [ + ["includes/foobar.json", "includes/subfolder/include2.json"], { + "Fn::Map": [ + ["includes/foobar.json", "includes/subfolder/include2.json"], { + "foo": "bar" + } + ] + } + ] + } + }, + "output": [{ + "foo": "bar" + }, { + "foo": "bar" + }, { + "foo": "bar" + }, { + "foo": "bar" + }] + + }], + "include": [{ + "name": "include template with variables", + "template": { + "Fn::Flatten": { + "Fn::Map": [ + [1, 2], "$", { + "Fn::Map": [ + [3, 4], { + "Fn::Include": "includes/mapvariable.json" + } + ] + } + ] + } + }, + "output": [{ + "foo": "3", + "bar": "1", + "baz": [{ + "foo": "3", + "bar": "1" + }] + }, { + "foo": "4", + "bar": "1", + "baz": [{ + "foo": "4", + "bar": "1" + }] + }, { + "foo": "3", + "bar": "2", + "baz": [{ + "foo": "3", + "bar": "2" + }] + }, { + "foo": "4", + "bar": "2", + "baz": [{ + "foo": "4", + "bar": "2" + }] + }] + }], + "synopsis": [{ + "name": "flatten nested map", + "template": { + "Fn::Flatten": { + "Fn::Map": [ + [80, 443], "$", { + "Fn::Map": [ + ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], { + "CirdIp": "_", + "FromPort": "$", + "ToPort": "$" + } + ] + } + ] + } + }, + "output": [{ + "CirdIp": "10.0.0.0/8", + "FromPort": "80", + "ToPort": "80" + }, { + "CirdIp": "172.16.0.0/12", + "FromPort": "80", + "ToPort": "80" + }, { + "CirdIp": "192.168.0.0/16", + "FromPort": "80", + "ToPort": "80" + }, { + "CirdIp": "10.0.0.0/8", + "FromPort": "443", + "ToPort": "443" + }, { + "CirdIp": "172.16.0.0/12", + "FromPort": "443", + "ToPort": "443" + }, { + "CirdIp": "192.168.0.0/16", + "FromPort": "443", + "ToPort": "443" + }] + }] +}