Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lang/funcs: "one" function #27454

Merged
merged 1 commit into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions lang/funcs/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
"github.com/zclconf/go-cty/cty/gocty"
)

var LengthFunc = function.New(&function.Spec{
Expand Down Expand Up @@ -381,6 +382,83 @@ var MatchkeysFunc = function.New(&function.Spec{
},
})

// OneFunc returns either the first element of a one-element list, or null
// if given a zero-element list.
var OneFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
ty := args[0].Type()
switch {
case ty.IsListType() || ty.IsSetType():
return ty.ElementType(), nil
case ty.IsTupleType():
etys := ty.TupleElementTypes()
switch len(etys) {
case 0:
// No specific type information, so we'll ultimately return
// a null value of unknown type.
return cty.DynamicPseudoType, nil
case 1:
return etys[0], nil
}
}
return cty.NilType, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
val := args[0]
ty := val.Type()

// Our parameter spec above doesn't set AllowUnknown or AllowNull,
// so we can assume our top-level collection is both known and non-null
// in here.

switch {
case ty.IsListType() || ty.IsSetType():
lenVal := val.Length()
if !lenVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
var l int
err := gocty.FromCtyValue(lenVal, &l)
if err != nil {
// It would be very strange to get here, because that would
// suggest that the length is either not a number or isn't
// an integer, which would suggest a bug in cty.
return cty.NilVal, fmt.Errorf("invalid collection length: %s", err)
}
switch l {
case 0:
return cty.NullVal(retType), nil
case 1:
var ret cty.Value
// We'll use an iterator here because that works for both lists
// and sets, whereas indexing directly would only work for lists.
// Since we've just checked the length, we should only actually
// run this loop body once.
for it := val.ElementIterator(); it.Next(); {
_, ret = it.Element()
}
return ret, nil
}
case ty.IsTupleType():
etys := ty.TupleElementTypes()
switch len(etys) {
case 0:
return cty.NullVal(retType), nil
case 1:
ret := val.Index(cty.NumberIntVal(0))
return ret, nil
}
}
return cty.NilVal, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
},
})

// SumFunc constructs a function that returns the sum of all
// numbers provided in a list
var SumFunc = function.New(&function.Spec{
Expand Down Expand Up @@ -595,6 +673,12 @@ func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
return MatchkeysFunc.Call([]cty.Value{values, keys, searchset})
}

// One returns either the first element of a one-element list, or null
// if given a zero-element list..
func One(list cty.Value) (cty.Value, error) {
return OneFunc.Call([]cty.Value{list})
}

// Sum adds numbers in a list, set, or tuple
func Sum(list cty.Value) (cty.Value, error) {
return SumFunc.Call([]cty.Value{list})
Expand Down
281 changes: 281 additions & 0 deletions lang/funcs/collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,287 @@ func TestMatchkeys(t *testing.T) {
}
}

func TestOne(t *testing.T) {
tests := []struct {
List cty.Value
Want cty.Value
Err string
}{
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
}),
cty.NumberIntVal(1),
"",
},
{
cty.ListValEmpty(cty.Number),
cty.NullVal(cty.Number),
"",
},
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(3),
}),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.Number),
}),
cty.UnknownVal(cty.Number),
"",
},
{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.Number),
cty.UnknownVal(cty.Number),
}),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.UnknownVal(cty.List(cty.String)),
cty.UnknownVal(cty.String),
"",
},
{
cty.NullVal(cty.List(cty.String)),
cty.NilVal,
"argument must not be null",
},
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
}).Mark("boop"),
cty.NumberIntVal(1).Mark("boop"),
"",
},
{
cty.ListValEmpty(cty.Bool).Mark("boop"),
cty.NullVal(cty.Bool).Mark("boop"),
"",
},
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(1).Mark("boop"),
}),
cty.NumberIntVal(1).Mark("boop"),
"",
},

{
cty.SetVal([]cty.Value{
cty.NumberIntVal(1),
}),
cty.NumberIntVal(1),
"",
},
{
cty.SetValEmpty(cty.Number),
cty.NullVal(cty.Number),
"",
},
{
cty.SetVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(3),
}),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.SetVal([]cty.Value{
cty.UnknownVal(cty.Number),
}),
cty.UnknownVal(cty.Number),
"",
},
{
cty.SetVal([]cty.Value{
cty.UnknownVal(cty.Number),
cty.UnknownVal(cty.Number),
}),
// The above would be valid if those two unknown values were
// equal known values, so this returns unknown rather than failing.
cty.UnknownVal(cty.Number),
"",
},
{
cty.UnknownVal(cty.Set(cty.String)),
cty.UnknownVal(cty.String),
"",
},
{
cty.NullVal(cty.Set(cty.String)),
cty.NilVal,
"argument must not be null",
},
{
cty.SetVal([]cty.Value{
cty.NumberIntVal(1),
}).Mark("boop"),
cty.NumberIntVal(1).Mark("boop"),
"",
},
{
cty.SetValEmpty(cty.Bool).Mark("boop"),
cty.NullVal(cty.Bool).Mark("boop"),
"",
},
{
cty.SetVal([]cty.Value{
cty.NumberIntVal(1).Mark("boop"),
}),
cty.NumberIntVal(1).Mark("boop"),
"",
},

{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(1),
}),
cty.NumberIntVal(1),
"",
},
{
cty.EmptyTupleVal,
cty.NullVal(cty.DynamicPseudoType),
"",
},
{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(3),
}),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.TupleVal([]cty.Value{
cty.UnknownVal(cty.Number),
}),
cty.UnknownVal(cty.Number),
"",
},
{
cty.TupleVal([]cty.Value{
cty.UnknownVal(cty.Number),
cty.UnknownVal(cty.Number),
}),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.UnknownVal(cty.EmptyTuple),
// Could actually return null here, but don't for consistency with unknown lists
cty.UnknownVal(cty.DynamicPseudoType),
"",
},
{
cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool})),
cty.UnknownVal(cty.Bool),
"",
},
{
cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool, cty.Number})),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.NullVal(cty.EmptyTuple),
cty.NilVal,
"argument must not be null",
},
{
cty.NullVal(cty.Tuple([]cty.Type{cty.Bool})),
cty.NilVal,
"argument must not be null",
},
{
cty.NullVal(cty.Tuple([]cty.Type{cty.Bool, cty.Number})),
cty.NilVal,
"argument must not be null",
},
{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(1),
}).Mark("boop"),
cty.NumberIntVal(1).Mark("boop"),
"",
},
{
cty.EmptyTupleVal.Mark("boop"),
cty.NullVal(cty.DynamicPseudoType).Mark("boop"),
"",
},
{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(1).Mark("boop"),
}),
cty.NumberIntVal(1).Mark("boop"),
"",
},

{
cty.DynamicVal,
cty.DynamicVal,
"",
},
{
cty.NullVal(cty.DynamicPseudoType),
cty.NilVal,
"argument must not be null",
},
{
cty.MapValEmpty(cty.String),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.EmptyObjectVal,
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.True,
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
{
cty.UnknownVal(cty.Bool),
cty.NilVal,
"must be a list, set, or tuple value with either zero or one elements",
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("one(%#v)", test.List), func(t *testing.T) {
got, err := One(test.List)

if test.Err != "" {
if err == nil {
t.Fatal("succeeded; want error")
} else if got, want := err.Error(), test.Err; got != want {
t.Fatalf("wrong error\n got: %s\nwant: %s", got, want)
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !test.Want.RawEquals(got) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

func TestSum(t *testing.T) {
tests := []struct {
List cty.Value
Expand Down
Loading