From edb5f82de17d7dafcf80786708942feb08253b5d Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 15 Jan 2019 09:47:14 -0800 Subject: [PATCH] lang/funcs: Convert the "setproduct" function to the new approach In our new world it produces either a set of a tuple type or a list of a tuple type, depending on the given argument types. The resulting collection's element tuple type is decided by the element types of the given collections, allowing type information to propagate even if unknown values are present. --- lang/funcs/collection.go | 128 ++++++++++ lang/funcs/collection_test.go | 239 ++++++++++++++++++ lang/functions.go | 1 + .../functions/setproduct.html.md | 115 +++++++++ website/layouts/functions.erb | 4 + 5 files changed, 487 insertions(+) create mode 100644 website/docs/configuration/functions/setproduct.html.md 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