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.
This commit is contained in:
Martin Atkins 2019-01-15 09:47:14 -08:00
parent 9d11d427a8
commit edb5f82de1
5 changed files with 487 additions and 0 deletions

View File

@ -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})

View File

@ -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"),

View File

@ -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,

View File

@ -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",
],
]
```

View File

@ -171,6 +171,10 @@
<a href="/docs/configuration/functions/merge.html">merge</a>
</li>
<li<%= sidebar_current("docs-funcs-collection-setproduct") %>>
<a href="/docs/configuration/functions/setproduct.html">setproduct</a>
</li>
<li<%= sidebar_current("docs-funcs-collection-slice") %>>
<a href="/docs/configuration/functions/slice.html">slice</a>
</li>