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 // CoalesceListFunc contructs a function that takes any number of list arguments
// and returns the first one that isn't empty. // and returns the first one that isn't empty.
var CoalesceListFunc = function.New(&function.Spec{ var CoalesceListFunc = function.New(&function.Spec{
@ -1258,6 +1302,11 @@ func Length(collection cty.Value) (cty.Value, error) {
return LengthFunc.Call([]cty.Value{collection}) 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. // CoalesceList takes any number of list arguments and returns the first one that isn't empty.
func CoalesceList(args ...cty.Value) (cty.Value, error) { func CoalesceList(args ...cty.Value) (cty.Value, error) {
return CoalesceListFunc.Call(args) 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) { func TestCoalesceList(t *testing.T) {
tests := []struct { tests := []struct {
Values []cty.Value Values []cty.Value

View File

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

View File

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