funcs/coalesce: return the first non-null, non-empty-string element from a sequence (#21002)

* funcs/coalesce: return the first non-null, non-empty element from a
sequence.

The go-cty coalesce function, which was originally used here, returns the
first non-null element from a sequence. Terraform 0.11's coalesce,
however, returns the first non-empty string from a list of strings.

This new coalesce function aims to preserve terraform's documented
functionality while adding support for additional argument types. The
tests include those in go-cty and adapted tests from the 0.11 version of
coalesce.

* website/docs: update coalesce function document
This commit is contained in:
Kristin Laemmert 2019-04-12 13:57:52 -04:00 committed by GitHub
parent e35e3d367e
commit d4669246c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 6 deletions

View File

@ -119,6 +119,50 @@ var LengthFunc = function.New(&function.Spec{
},
})
// CoalesceFunc contructs 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
// from a sequence, instead of merely the first non-null.
var CoalesceFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
argTypes := make([]cty.Type, len(args))
for i, val := range args {
argTypes[i] = val.Type()
}
retType, _ := convert.UnifyUnsafe(argTypes)
if retType == cty.NilType {
return cty.NilType, fmt.Errorf("all arguments must have the same type")
}
return retType, nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, argVal := range args {
// We already know this will succeed because of the checks in our Type func above
argVal, _ = convert.Convert(argVal, retType)
if !argVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
if argVal.IsNull() {
continue
}
if retType == cty.String && argVal.RawEquals(cty.StringVal("")) {
continue
}
return argVal, nil
}
return cty.NilVal, fmt.Errorf("no non-null, non-empty-string arguments")
},
})
// CoalesceListFunc contructs a function that takes any number of list arguments
// and returns the first one that isn't empty.
var CoalesceListFunc = function.New(&function.Spec{
@ -1258,6 +1302,11 @@ func Length(collection cty.Value) (cty.Value, error) {
return LengthFunc.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)
}
// CoalesceList takes any number of list arguments and returns the first one that isn't empty.
func CoalesceList(args ...cty.Value) (cty.Value, error) {
return CoalesceListFunc.Call(args)

View File

@ -257,6 +257,94 @@ func TestLength(t *testing.T) {
}
}
func TestCoalesce(t *testing.T) {
tests := []struct {
Values []cty.Value
Want cty.Value
Err bool
}{
{
[]cty.Value{cty.StringVal("first"), cty.StringVal("second"), cty.StringVal("third")},
cty.StringVal("first"),
false,
},
{
[]cty.Value{cty.StringVal(""), cty.StringVal("second"), cty.StringVal("third")},
cty.StringVal("second"),
false,
},
{
[]cty.Value{cty.StringVal(""), cty.StringVal("")},
cty.NilVal,
true,
},
{
[]cty.Value{cty.True},
cty.True,
false,
},
{
[]cty.Value{cty.NullVal(cty.Bool), cty.True},
cty.True,
false,
},
{
[]cty.Value{cty.NullVal(cty.Bool), cty.False},
cty.False,
false,
},
{
[]cty.Value{cty.NullVal(cty.Bool), cty.False, cty.StringVal("hello")},
cty.StringVal("false"),
false,
},
{
[]cty.Value{cty.True, cty.UnknownVal(cty.Bool)},
cty.True,
false,
},
{
[]cty.Value{cty.UnknownVal(cty.Bool), cty.True},
cty.UnknownVal(cty.Bool),
false,
},
{
[]cty.Value{cty.UnknownVal(cty.Bool), cty.StringVal("hello")},
cty.UnknownVal(cty.String),
false,
},
{
[]cty.Value{cty.DynamicVal, cty.True},
cty.UnknownVal(cty.Bool),
false,
},
{
[]cty.Value{cty.DynamicVal},
cty.DynamicVal,
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Coalesce(%#v...)", test.Values), func(t *testing.T) {
got, err := Coalesce(test.Values...)
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 TestCoalesceList(t *testing.T) {
tests := []struct {
Values []cty.Value

View File

@ -42,7 +42,7 @@ func (s *Scope) Functions() map[string]function.Function {
"cidrhost": funcs.CidrHostFunc,
"cidrnetmask": funcs.CidrNetmaskFunc,
"cidrsubnet": funcs.CidrSubnetFunc,
"coalesce": stdlib.CoalesceFunc,
"coalesce": funcs.CoalesceFunc,
"coalescelist": funcs.CoalesceListFunc,
"compact": funcs.CompactFunc,
"concat": stdlib.ConcatFunc,

View File

@ -3,8 +3,8 @@ layout: "functions"
page_title: "coalesce - Functions - Configuration Language"
sidebar_current: "docs-funcs-collection-coalesce-x"
description: |-
The coalesce function takes any number of string arguments and returns the
first one that isn't empty.
The coalesce function takes any number of arguments and returns the
first one that isn't null nor empty.
---
# `coalesce` Function
@ -13,8 +13,8 @@ description: |-
earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
`coalesce` takes any number of string arguments and returns the first one
that isn't empty.
`coalesce` takes any number of arguments and returns the first one
that isn't null or an empty string.
## Examples
@ -23,6 +23,8 @@ that isn't empty.
a
> coalesce("", "b")
b
> coalesce(1,2)
1
```
To perform the `coalesce` operation with a list of strings, use the `...`
@ -36,4 +38,4 @@ b
## Related Functions
* [`coalescelist`](./coalescelist.html) performs a similar operation with
list arguments rather than string arguments.
list arguments rather than individual arguments.