diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index d3343415a..f01ab66cc 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -807,6 +807,129 @@ var MergeFunc = function.New(&function.Spec{ }, }) +// SetProductFunc calculates the cartesian product of two or more sets or +// sequences. If the arguments are all lists then the result is a list of tuples, +// preserving the ordering of all of the input lists. Otherwise the result is a +// set of tuples. +var SetProductFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "sets", + Type: cty.DynamicPseudoType, + }, + Type: func(args []cty.Value) (retType cty.Type, err error) { + if len(args) < 2 { + return cty.NilType, fmt.Errorf("at least two arguments are required") + } + + listCount := 0 + elemTys := make([]cty.Type, len(args)) + for i, arg := range args { + aty := arg.Type() + switch { + case aty.IsSetType(): + elemTys[i] = aty.ElementType() + case aty.IsListType(): + elemTys[i] = aty.ElementType() + listCount++ + case aty.IsTupleType(): + // We can accept a tuple type only if there's some common type + // that all of its elements can be converted to. + allEtys := aty.TupleElementTypes() + if len(allEtys) == 0 { + elemTys[i] = cty.DynamicPseudoType + listCount++ + break + } + ety, _ := convert.UnifyUnsafe(allEtys) + if ety == cty.NilType { + return cty.NilType, function.NewArgErrorf(i, "all elements must be of the same type") + } + elemTys[i] = ety + listCount++ + default: + return cty.NilType, function.NewArgErrorf(i, "a set or a list is required") + } + } + + if listCount == len(args) { + return cty.List(cty.Tuple(elemTys)), nil + } + return cty.Set(cty.Tuple(elemTys)), nil + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + ety := retType.ElementType() + + total := 1 + for _, arg := range args { + // Because of our type checking function, we are guaranteed that + // all of the arguments are known, non-null values of types that + // support LengthInt. + total *= arg.LengthInt() + } + + if total == 0 { + // If any of the arguments was an empty collection then our result + // is also an empty collection, which we'll short-circuit here. + if retType.IsListType() { + return cty.ListValEmpty(ety), nil + } + return cty.SetValEmpty(ety), nil + } + + subEtys := ety.TupleElementTypes() + product := make([][]cty.Value, total) + + b := make([]cty.Value, total*len(args)) + n := make([]int, len(args)) + s := 0 + argVals := make([][]cty.Value, len(args)) + for i, arg := range args { + argVals[i] = arg.AsValueSlice() + } + + for i := range product { + e := s + len(args) + pi := b[s:e] + product[i] = pi + s = e + + for j, n := range n { + val := argVals[j][n] + ty := subEtys[j] + if !val.Type().Equals(ty) { + var err error + val, err = convert.Convert(val, ty) + if err != nil { + // Should never happen since we checked this in our + // type-checking function. + return cty.NilVal, fmt.Errorf("failed to convert argVals[%d][%d] to %s; this is a bug in Terraform", j, n, ty.FriendlyName()) + } + } + pi[j] = val + } + + for j := len(n) - 1; j >= 0; j-- { + n[j]++ + if n[j] < len(argVals[j]) { + break + } + n[j] = 0 + } + } + + productVals := make([]cty.Value, total) + for i, vals := range product { + productVals[i] = cty.TupleVal(vals) + } + + if retType.IsListType() { + return cty.ListVal(productVals), nil + } + return cty.SetVal(productVals), nil + }, +}) + // SliceFunc contructs a function that extracts some consecutive elements // from within a list. var SliceFunc = function.New(&function.Spec{ @@ -1169,6 +1292,11 @@ func Merge(maps ...cty.Value) (cty.Value, error) { return MergeFunc.Call(maps) } +// SetProduct computes the cartesian product of sets or sequences. +func SetProduct(sets ...cty.Value) (cty.Value, error) { + return SetProductFunc.Call(sets) +} + // Slice extracts some consecutive elements from within a list. func Slice(list, start, end cty.Value) (cty.Value, error) { return SliceFunc.Call([]cty.Value{list, start, end}) diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 7e1ed6714..0f56878a7 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -1854,6 +1854,245 @@ func TestMerge(t *testing.T) { } } +func TestSetProduct(t *testing.T) { + tests := []struct { + Sets []cty.Value + Want cty.Value + Err string + }{ + { + nil, + cty.DynamicVal, + "at least two arguments are required", + }, + { + []cty.Value{ + cty.SetValEmpty(cty.String), + }, + cty.DynamicVal, + "at least two arguments are required", + }, + { + []cty.Value{ + cty.SetValEmpty(cty.String), + cty.StringVal("hello"), + }, + cty.DynamicVal, + "a set or a list is required", // this is an ArgError, so is presented against the second argument in particular + }, + { + []cty.Value{ + cty.SetValEmpty(cty.String), + cty.SetValEmpty(cty.String), + }, + cty.SetValEmpty(cty.Tuple([]cty.Type{cty.String, cty.String})), + "", + }, + { + []cty.Value{ + cty.SetVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.ListVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.ListVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }, + cty.ListVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.ListVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }, + cty.ListVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("bar")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.ListVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.True}), + }, + cty.ListVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("true")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("true")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("true")}), + }), + "", + }, + { + []cty.Value{ + cty.ListVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.EmptyTupleVal, + }, + cty.ListValEmpty(cty.Tuple([]cty.Type{cty.String, cty.DynamicPseudoType})), + "", + }, + { + []cty.Value{ + cty.ListVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.EmptyObjectVal}), + }, + cty.DynamicVal, + "all elements must be of the same type", // this is an ArgError for the second argument + }, + { + []cty.Value{ + cty.SetVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + cty.SetVal([]cty.Value{cty.StringVal("baz")}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("foo"), cty.StringVal("baz")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("foo"), cty.StringVal("baz")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("foo"), cty.StringVal("baz")}), + cty.TupleVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("bar"), cty.StringVal("baz")}), + cty.TupleVal([]cty.Value{cty.StringVal("stg"), cty.StringVal("bar"), cty.StringVal("baz")}), + cty.TupleVal([]cty.Value{cty.StringVal("prd"), cty.StringVal("bar"), cty.StringVal("baz")}), + }), + "", + }, + { + []cty.Value{ + cty.SetVal([]cty.Value{cty.StringVal("dev"), cty.StringVal("stg"), cty.StringVal("prd")}), + cty.SetValEmpty(cty.String), + }, + cty.SetValEmpty(cty.Tuple([]cty.Type{cty.String, cty.String})), + "", + }, + { + []cty.Value{ + cty.SetVal([]cty.Value{cty.StringVal("foo")}), + cty.SetVal([]cty.Value{cty.StringVal("bar")}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("foo")}), + cty.TupleVal([]cty.Value{cty.StringVal("bar")}), + }, + cty.ListVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), + }), + "", + }, + { + []cty.Value{ + cty.SetVal([]cty.Value{cty.StringVal("foo")}), + cty.SetVal([]cty.Value{cty.DynamicVal}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.DynamicVal}), + }), + "", + }, + { + []cty.Value{ + cty.SetVal([]cty.Value{cty.StringVal("foo")}), + cty.SetVal([]cty.Value{cty.True, cty.DynamicVal}), + }, + cty.SetVal([]cty.Value{ + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.True}), + cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.UnknownVal(cty.Bool)}), + }), + "", + }, + { + []cty.Value{ + cty.UnknownVal(cty.Set(cty.String)), + cty.SetVal([]cty.Value{cty.True, cty.False}), + }, + cty.UnknownVal(cty.Set(cty.Tuple([]cty.Type{cty.String, cty.Bool}))), + "", + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("setproduct(%#v)", test.Sets), func(t *testing.T) { + got, err := SetProduct(test.Sets...) + + if test.Err != "" { + if err == nil { + t.Fatal("succeeded; want error") + } + if got, want := err.Error(), test.Err; got != want { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } + +} + func TestSlice(t *testing.T) { listOfStrings := cty.ListVal([]cty.Value{ cty.StringVal("a"), diff --git a/lang/functions.go b/lang/functions.go index b116708dd..6b03a1271 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -81,6 +81,7 @@ func (s *Scope) Functions() map[string]function.Function { "pow": funcs.PowFunc, "replace": funcs.ReplaceFunc, "rsadecrypt": funcs.RsaDecryptFunc, + "setproduct": funcs.SetProductFunc, "sha1": funcs.Sha1Func, "sha256": funcs.Sha256Func, "sha512": funcs.Sha512Func, diff --git a/website/docs/configuration/functions/setproduct.html.md b/website/docs/configuration/functions/setproduct.html.md new file mode 100644 index 000000000..2ccb74e6d --- /dev/null +++ b/website/docs/configuration/functions/setproduct.html.md @@ -0,0 +1,115 @@ +--- +layout: "functions" +page_title: "setproduct - Functions - Configuration Language" +sidebar_current: "docs-funcs-collection-setproduct" +description: |- + The setproduct function finds all of the possible combinations of elements + from all of the given sets by computing the cartesian product. +--- + +# `setproduct` Function + +The `setproduct` function finds all of the possible combinations of elements +from all of the given sets by computing the +[cartesian product](https://en.wikipedia.org/wiki/Cartesian_product). + +```hcl +setproduct(sets...) +``` + +This function is particularly useful for finding the exhaustive set of all +combinations of members of multiple sets, such as per-application-per-environment +resources. + +``` +> setproduct(["development", "staging", "production"], ["app1", "app2"]) +[ + [ + "development", + "app1", + ], + [ + "development", + "app2", + ], + [ + "staging", + "app1", + ], + [ + "staging", + "app2", + ], + [ + "production", + "app1", + ], + [ + "production", + "app2", + ], +] +``` + +You must past at least two arguments to this function. + +Although defined primarily for sets, this function can also work with lists. +If all of the given arguments are lists then the result is a list, preserving +the ordering of the given lists. Otherwise the result is a set. In either case, +the result's element type is a list of values corresponding to each given +argument in turn. + +## Examples + +There is an example of the common usage of this function above. There are some +other situations that are less common when hand-writing but may arise in +reusable module situations. + +If any of the arguments is empty then the result is always empty itself, +similar to how multiplying any number by zero gives zero: + +``` +> setproduct(["development", "staging", "production"], []) +[] +``` + +Similarly, if all of the arguments have only one element then the result has +only one element, which is the first element of each argument: + +``` +> setproduct(["a"], ["b"]) +[ + [ + "a", + "b", + ], +] +``` + +Each argument must have a consistent type for all of its elements. If not, +Terraform will attempt to convert to the most general type, or produce an +error if such a conversion is impossible. For example, mixing both strings and +numbers results in the numbers being converted to strings so that the result +elements all have a consistent type: + +``` +> setproduct(["staging", "production"], ["a", 2]) +[ + [ + "staging", + "a", + ], + [ + "staging", + "2", + ], + [ + "production", + "a", + ], + [ + "production", + "2", + ], +] +``` diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index f6d7bf496..2546c67b1 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -171,6 +171,10 @@ merge + > + setproduct + + > slice