diff --git a/cty/function/stdlib/collection.go b/cty/function/stdlib/collection.go index f05132e1..8c771104 100644 --- a/cty/function/stdlib/collection.go +++ b/cty/function/stdlib/collection.go @@ -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", @@ -185,11 +186,12 @@ 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() if l == 0 { return cty.DynamicVal, errors.New("cannot use element function with an empty list") } @@ -197,7 +199,7 @@ var ElementFunc = function.New(&function.Spec{ // 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 }, }) @@ -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) { @@ -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 } }, }) @@ -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", @@ -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 @@ -1073,18 +1079,18 @@ 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 }, }) @@ -1092,9 +1098,12 @@ 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 } diff --git a/cty/function/stdlib/collection_test.go b/cty/function/stdlib/collection_test.go index 2fe93f2c..d1018548 100644 --- a/cty/function/stdlib/collection_test.go +++ b/cty/function/stdlib/collection_test.go @@ -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 @@ -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), @@ -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) + } + }) + } +}