Skip to content

Commit

Permalink
lang/funcs: "anytrue" function
Browse files Browse the repository at this point in the history
This is an analog to the "alltrue" function, using OR as the reduce
operator rather than AND.

This also includes some simplification of the "alltrue" implementation
to implement it similarly as a sort of reduce operation with AND
as the reduce operator, but with the same effective behavior.
  • Loading branch information
artburkart authored Oct 23, 2020
1 parent 5522a79 commit d4716a6
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 49 deletions.
62 changes: 41 additions & 21 deletions lang/funcs/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,40 +57,54 @@ 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.
// list are true. If the list is empty, return true.
var AllTrueFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "collection",
Type: cty.DynamicPseudoType,
Name: "list",
Type: cty.List(cty.Bool),
},
},
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)
result := cty.True
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
return cty.UnknownVal(cty.Bool), nil
if v.IsNull() {
return cty.False, nil
}
got, err := tobool.Call([]cty.Value{v})
if err != nil {
result = result.And(v)
if result.False() {
return cty.False, nil
}
eq, err := stdlib.Equal(got, cty.True)
if err != nil {
return cty.NilVal, err
}
return result, nil
},
})

// AnyTrueFunc constructs a function that returns true if any element of the
// list is true. If the list is empty, return false.
var AnyTrueFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.False
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if v.IsNull() {
continue
}
if eq.False() {
return cty.False, nil
result = result.Or(v)
if result.True() {
return cty.True, nil
}
}
return cty.True, nil
return result, nil
},
})

Expand Down Expand Up @@ -620,12 +634,18 @@ 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.
// AllTrue returns true if all elements of the list are true. If the list is empty,
// return true.
func AllTrue(collection cty.Value) (cty.Value, error) {
return AllTrueFunc.Call([]cty.Value{collection})
}

// AnyTrue returns true if any element of the list is true. If the list is empty,
// return false.
func AnyTrue(collection cty.Value) (cty.Value, error) {
return AnyTrueFunc.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)
Expand Down
87 changes: 60 additions & 27 deletions lang/funcs/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,75 +146,108 @@ func TestAllTrue(t *testing.T) {
Err bool
}{
{
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.Bool),
cty.True,
false,
},
{
cty.TupleVal([]cty.Value{}),
cty.ListVal([]cty.Value{cty.True}),
cty.True,
false,
},
{
cty.SetValEmpty(cty.Bool),
cty.True,
cty.ListVal([]cty.Value{cty.False}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.True}),
cty.True,
cty.ListVal([]cty.Value{cty.True, cty.False}),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.StringVal("true")}),
cty.True,
cty.ListVal([]cty.Value{cty.False, cty.True}),
cty.False,
false,
},
{
cty.TupleVal([]cty.Value{cty.True, cty.StringVal("true")}),
cty.True,
false,
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
cty.UnknownVal(cty.Bool),
true,
},
{
cty.ListVal([]cty.Value{cty.False}),
cty.False,
false,
cty.NullVal(cty.List(cty.Bool)),
cty.NilVal,
true,
},
}

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 TestAnyTrue(t *testing.T) {
tests := []struct {
Collection cty.Value
Want cty.Value
Err bool
}{
{
cty.ListVal([]cty.Value{cty.True, cty.False}),
cty.ListValEmpty(cty.Bool),
cty.False,
false,
},
{
cty.ListVal([]cty.Value{cty.False, cty.True}),
cty.False,
cty.ListVal([]cty.Value{cty.True}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.NumberIntVal(1)}),
cty.ListVal([]cty.Value{cty.False}),
cty.False,
false,
},
{
cty.StringVal("true"),
cty.False,
true,
cty.ListVal([]cty.Value{cty.True, cty.False}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.ListValEmpty(cty.String)}),
cty.False,
cty.ListVal([]cty.Value{cty.False, cty.True}),
cty.True,
false,
},
{
cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
cty.UnknownVal(cty.Bool),
false,
true,
},
{
cty.NullVal(cty.List(cty.Bool)),
cty.NilVal,
true,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("alltrue(%#v)", test.Collection), func(t *testing.T) {
got, err := AllTrue(test.Collection)
t.Run(fmt.Sprintf("anytrue(%#v)", test.Collection), func(t *testing.T) {
got, err := AnyTrue(test.Collection)

if test.Err {
if err == nil {
Expand Down
1 change: 1 addition & 0 deletions lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func (s *Scope) Functions() map[string]function.Function {
"abs": stdlib.AbsoluteFunc,
"abspath": funcs.AbsPathFunc,
"alltrue": funcs.AllTrueFunc,
"anytrue": funcs.AnyTrueFunc,
"basename": funcs.BasenameFunc,
"base64decode": funcs.Base64DecodeFunc,
"base64encode": funcs.Base64EncodeFunc,
Expand Down
9 changes: 8 additions & 1 deletion lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,18 @@ func TestFunctions(t *testing.T) {

"alltrue": {
{
`alltrue([true])`,
`alltrue(["true", true])`,
cty.True,
},
},

"anytrue": {
{
`anytrue([])`,
cty.False,
},
},

"base64decode": {
{
`base64decode("YWJjMTIzIT8kKiYoKSctPUB+")`,
Expand Down
34 changes: 34 additions & 0 deletions website/docs/configuration/functions/anytrue.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
layout: functions
page_title: anytrue - Functions - Configuration Language
sidebar_current: docs-funcs-collection-anytrue
description: |-
The anytrue function determines whether any element of a collection
is true or "true". If the collection is empty, it returns false.
---

# `anytrue` 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).

`anytrue` returns `true` if any element in a given collection is `true`
or `"true"`. It also returns `false` if the collection is empty.

```hcl
anytrue(list)
```

## Examples

```command
> anytrue(["true"])
true
> anytrue([true])
true
> anytrue([true, false])
true
> anytrue([])
false
```
4 changes: 4 additions & 0 deletions website/layouts/functions.erb
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
<a href="/docs/configuration/functions/alltrue.html">alltrue</a>
</li>

<li>
<a href="/docs/configuration/functions/anytrue.html">anytrue</a>
</li>

<li>
<a href="/docs/configuration/functions/chunklist.html">chunklist</a>
</li>
Expand Down

0 comments on commit d4716a6

Please sign in to comment.