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

ElementFunc, ReverseListFun and SliceFunc mark handling #101

Merged
merged 3 commits into from
Apr 30, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 32 additions & 23 deletions cty/function/stdlib/collection.go
Expand Up @@ -129,8 +129,9 @@ var LengthFunc = function.New(&function.Spec{
var ElementFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
Name: "list",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
{
Name: "index",
Expand Down Expand Up @@ -185,19 +186,20 @@ var ElementFunc = function.New(&function.Spec{
return cty.DynamicVal, fmt.Errorf("cannot use element function with a negative index")
}

if !args[0].IsKnown() {
input, marks := args[0].Unmark()
if !input.IsKnown() {
return cty.UnknownVal(retType), nil
}

l := args[0].LengthInt()
l := input.LengthInt()
Comment on lines -192 to +194
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's funny that in this particular function we end up needing to unmark only because of this LengthInt call, because otherwise everything we do with args[0] is an operation method that will handle marks automatically! Having the length is important for this function though (to implement the wrapping behavior), so this does seem reasonable.

if l == 0 {
return cty.DynamicVal, errors.New("cannot use element function with an empty list")
}
index = index % l

// We did all the necessary type checks in the type function above,
// so this is guaranteed not to fail.
return args[0].Index(cty.NumberIntVal(int64(index))), nil
return input.Index(cty.NumberIntVal(int64(index))).WithMarks(marks), nil
},
})

Expand Down Expand Up @@ -841,8 +843,9 @@ var MergeFunc = function.New(&function.Spec{
var ReverseListFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
Name: "list",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
Expand All @@ -862,19 +865,21 @@ var ReverseListFunc = function.New(&function.Spec{
}
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
in := args[0].AsValueSlice()
outVals := make([]cty.Value, len(in))
for i, v := range in {
in, marks := args[0].Unmark()
inVals := in.AsValueSlice()
outVals := make([]cty.Value, len(inVals))

for i, v := range inVals {
outVals[len(outVals)-i-1] = v
}
switch {
case retType.IsTupleType():
return cty.TupleVal(outVals), nil
return cty.TupleVal(outVals).WithMarks(marks), nil
default:
if len(outVals) == 0 {
return cty.ListValEmpty(retType.ElementType()), nil
return cty.ListValEmpty(retType.ElementType()).WithMarks(marks), nil
}
return cty.ListVal(outVals), nil
return cty.ListVal(outVals).WithMarks(marks), nil
}
},
})
Expand Down Expand Up @@ -1018,8 +1023,9 @@ var SetProductFunc = function.New(&function.Spec{
var SliceFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
Name: "list",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
{
Name: "start_index",
Expand Down Expand Up @@ -1058,10 +1064,10 @@ var SliceFunc = function.New(&function.Spec{
return cty.Tuple(argTy.TupleElementTypes()[startIndex:endIndex]), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputList := args[0]
inputList, marks := args[0].Unmark()

if retType == cty.DynamicPseudoType {
return cty.DynamicVal, nil
return cty.DynamicVal.WithMarks(marks), nil
}

// we ignore idxsKnown return value here because the indices are always
Expand All @@ -1073,28 +1079,31 @@ var SliceFunc = function.New(&function.Spec{

if endIndex-startIndex == 0 {
if retType.IsTupleType() {
return cty.EmptyTupleVal, nil
return cty.EmptyTupleVal.WithMarks(marks), nil
}
return cty.ListValEmpty(retType.ElementType()), nil
return cty.ListValEmpty(retType.ElementType()).WithMarks(marks), nil
}

outputList := inputList.AsValueSlice()[startIndex:endIndex]

if retType.IsTupleType() {
return cty.TupleVal(outputList), nil
return cty.TupleVal(outputList).WithMarks(marks), nil
}

return cty.ListVal(outputList), nil
return cty.ListVal(outputList).WithMarks(marks), nil
},
})

func sliceIndexes(args []cty.Value) (int, int, bool, error) {
var startIndex, endIndex, length int
var startKnown, endKnown, lengthKnown bool

// remove marks from args[0]
list, _ := args[0].Unmark()

// If it's a tuple then we always know the length by the type, but collections might be unknown or have unknown length
if args[0].Type().IsTupleType() || args[0].Length().IsKnown() {
length = args[0].LengthInt()
if list.Type().IsTupleType() || list.Length().IsKnown() {
length = list.LengthInt()
lengthKnown = true
}

Expand Down
201 changes: 201 additions & 0 deletions cty/function/stdlib/collection_test.go
Expand Up @@ -1125,6 +1125,12 @@ func TestElement(t *testing.T) {
cty.StringVal("brown"),
cty.UnknownVal(cty.String),
})
listWithMarks := cty.ListVal([]cty.Value{
cty.StringVal("the"),
cty.StringVal("quick"),
cty.StringVal("brown").Mark("fox"),
cty.UnknownVal(cty.String),
})

tests := []struct {
List cty.Value
Expand Down Expand Up @@ -1174,6 +1180,24 @@ func TestElement(t *testing.T) {
cty.UnknownVal(cty.String),
false,
},
{ // preserve marks
listWithMarks,
cty.NumberIntVal(2),
cty.StringVal("brown").Mark("fox"),
false,
},
{ // marked items
listWithMarks,
cty.NumberIntVal(1),
cty.StringVal("quick"),
false,
},
{ // The entire list is marked
listWithMarks.Mark("thewholeshebang"),
cty.NumberIntVal(2),
cty.StringVal("brown").WithMarks(cty.NewValueMarks("thewholeshebang", "fox")),
false,
},
{
listOfStrings,
cty.NumberIntVal(-1),
Expand Down Expand Up @@ -2182,3 +2206,180 @@ func TestSetproduct(t *testing.T) {
})
}
}

func TestReverseList(t *testing.T) {
tests := []struct {
Input cty.Value
Want cty.Value
Err string
}{
{
cty.NilVal,
cty.NilVal,
`argument must not be null`,
},
{
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String),
``,
},
{
cty.ListValEmpty(cty.String).Mark("foo"),
cty.ListValEmpty(cty.String).Mark("foo"),
``,
},
{
cty.UnknownVal(cty.List(cty.String)),
cty.UnknownVal(cty.List(cty.String)),
``,
},
{ // marks on list elements
cty.ListVal([]cty.Value{
cty.StringVal("beep").Mark("boop"),
cty.StringVal("bop"),
cty.StringVal("bloop"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("bloop"),
cty.StringVal("bop"),
cty.StringVal("beep").Mark("boop"),
}),
``,
},
{ // marks on the entire input are preserved
cty.ListVal([]cty.Value{
cty.StringVal("beep").Mark("boop"),
cty.StringVal("bop"),
cty.StringVal("bloop"),
}).Mark("outer"),
cty.ListVal([]cty.Value{
cty.StringVal("bloop"),
cty.StringVal("bop"),
cty.StringVal("beep").Mark("boop"),
}).Mark("outer"),
``,
},
{ // marks on tuple elements
cty.TupleVal([]cty.Value{
cty.StringVal("beep").Mark("boop"),
cty.StringVal("bop"),
cty.StringVal("bloop"),
}),
cty.TupleVal([]cty.Value{
cty.StringVal("bloop"),
cty.StringVal("bop"),
cty.StringVal("beep").Mark("boop"),
}),
``,
},
{ // Set elements don't support individual marks; any marks on elements get propegated to the entire set.
cty.SetVal([]cty.Value{
cty.StringVal("beep").Mark("boop"),
cty.StringVal("bop"),
cty.StringVal("bloop"),
}),
// sets end up sorted alphabetically when converted to lists
cty.ListVal([]cty.Value{
cty.StringVal("bop"),
cty.StringVal("bloop"),
cty.StringVal("beep"),
}).Mark("boop"),
``,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("ReverseList(%#v)", test.Input), func(t *testing.T) {
got, err := ReverseList(test.Input)
if test.Err != "" {
if err == nil {
t.Fatal("succeeded; want error")
}
if got, want := err.Error(), test.Err; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
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 TestSlice(t *testing.T) {
tests := []struct {
Input cty.Value
Start cty.Value
End cty.Value
Want cty.Value
Err string
}{
{
Input: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
Start: cty.NumberIntVal(0),
End: cty.NumberIntVal(2),
Want: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
}),
Err: ``,
},
{ // The entire input list is marked, so the return should be marked
Input: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}).Mark("bloop"),
Start: cty.NumberIntVal(0),
End: cty.NumberIntVal(2),
Want: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
}).Mark("bloop"),
Err: ``,
},
{ // individual element marks should be preserved
Input: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b").Mark("bloop"),
cty.StringVal("c"),
}),
Start: cty.NumberIntVal(0),
End: cty.NumberIntVal(2),
Want: cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b").Mark("bloop"),
}),
Err: ``,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("Slice(%#v)", test.Input), func(t *testing.T) {
got, err := Slice(test.Input, test.Start, test.End)
if test.Err != "" {
if err == nil {
t.Fatal("succeeded; want error")
}
if got, want := err.Error(), test.Err; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
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)
}
})
}
}