diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go index bc93f8a2c..5fff2b903 100644 --- a/lang/funcs/collection.go +++ b/lang/funcs/collection.go @@ -56,6 +56,44 @@ var LengthFunc = function.New(&function.Spec{ }, }) +// AllTrueFunc constructs a function that returns true if all elements of the +// collection are true or "true". If the collection is empty, return true. +var AllTrueFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "collection", + Type: cty.DynamicPseudoType, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + ty := args[0].Type() + if !ty.IsListType() && !ty.IsTupleType() && !ty.IsSetType() { + return cty.NilVal, errors.New("argument must be list, tuple, or set") + } + + tobool := MakeToFunc(cty.Bool) + for it := args[0].ElementIterator(); it.Next(); { + _, v := it.Element() + if !v.IsKnown() { + return cty.UnknownVal(cty.Bool), nil + } + got, err := tobool.Call([]cty.Value{v}) + if err != nil { + return cty.False, nil + } + eq, err := stdlib.Equal(got, cty.True) + if err != nil { + return cty.NilVal, err + } + if eq.False() { + return cty.False, nil + } + } + return cty.True, nil + }, +}) + // CoalesceFunc constructs a function that takes any number of arguments and // returns the first one that isn't empty. This function was copied from go-cty // stdlib and modified so that it returns the first *non-empty* non-null element @@ -582,6 +620,12 @@ func Length(collection cty.Value) (cty.Value, error) { return LengthFunc.Call([]cty.Value{collection}) } +// AllTrue returns true if all elements of the collection are true or "true". +// If the collection is empty, return true. +func AllTrue(collection cty.Value) (cty.Value, error) { + return AllTrueFunc.Call([]cty.Value{collection}) +} + // Coalesce takes any number of arguments and returns the first one that isn't empty. func Coalesce(args ...cty.Value) (cty.Value, error) { return CoalesceFunc.Call(args) diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go index 8e15f81ee..db42a997a 100644 --- a/lang/funcs/collection_test.go +++ b/lang/funcs/collection_test.go @@ -139,6 +139,99 @@ func TestLength(t *testing.T) { } } +func TestAllTrue(t *testing.T) { + tests := []struct { + Collection cty.Value + Want cty.Value + Err bool + }{ + { + cty.ListValEmpty(cty.String), + cty.True, + false, + }, + { + cty.TupleVal([]cty.Value{}), + cty.True, + false, + }, + { + cty.SetValEmpty(cty.Bool), + cty.True, + false, + }, + { + cty.ListVal([]cty.Value{cty.True}), + cty.True, + false, + }, + { + cty.ListVal([]cty.Value{cty.StringVal("true")}), + cty.True, + false, + }, + { + cty.TupleVal([]cty.Value{cty.True, cty.StringVal("true")}), + cty.True, + false, + }, + { + cty.ListVal([]cty.Value{cty.False}), + cty.False, + false, + }, + { + cty.ListVal([]cty.Value{cty.True, cty.False}), + cty.False, + false, + }, + { + cty.ListVal([]cty.Value{cty.False, cty.True}), + cty.False, + false, + }, + { + cty.ListVal([]cty.Value{cty.NumberIntVal(1)}), + cty.False, + false, + }, + { + cty.StringVal("true"), + cty.False, + true, + }, + { + cty.ListVal([]cty.Value{cty.ListValEmpty(cty.String)}), + cty.False, + false, + }, + { + cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), + cty.UnknownVal(cty.Bool), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("alltrue(%#v)", test.Collection), func(t *testing.T) { + got, err := AllTrue(test.Collection) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + 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 TestCoalesce(t *testing.T) { tests := []struct { Values []cty.Value diff --git a/lang/functions.go b/lang/functions.go index b4cc2d72e..24698cda5 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -33,6 +33,7 @@ func (s *Scope) Functions() map[string]function.Function { s.funcs = map[string]function.Function{ "abs": stdlib.AbsoluteFunc, "abspath": funcs.AbsPathFunc, + "alltrue": funcs.AllTrueFunc, "basename": funcs.BasenameFunc, "base64decode": funcs.Base64DecodeFunc, "base64encode": funcs.Base64EncodeFunc, diff --git a/lang/functions_test.go b/lang/functions_test.go index d226a32aa..24667d61f 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -67,6 +67,13 @@ func TestFunctions(t *testing.T) { }, }, + "alltrue": { + { + `alltrue([true])`, + cty.True, + }, + }, + "base64decode": { { `base64decode("YWJjMTIzIT8kKiYoKSctPUB+")`, diff --git a/website/docs/configuration/functions/alltrue.html.md b/website/docs/configuration/functions/alltrue.html.md new file mode 100644 index 000000000..173685c1f --- /dev/null +++ b/website/docs/configuration/functions/alltrue.html.md @@ -0,0 +1,30 @@ +--- +layout: functions +page_title: alltrue - Functions - Configuration Language +sidebar_current: docs-funcs-collection-alltrue +description: |- + The alltrue function determines whether all elements of a collection + are true or "true". If the collection is empty, it returns true. +--- + +# `alltrue` Function + +-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`alltrue` returns `true` if all elements in a given collection are `true` +or `"true"`. It also returns `true` if the collection is empty. + +```hcl +alltrue(list) +``` + +## Examples + +```command +> alltrue(["true", true]) +true +> alltrue([true, false]) +false +``` diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index d0ecd70a5..49ed7e5c9 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -138,6 +138,10 @@ Collection Functions