Skip to content

Commit

Permalink
cty: Mark-aware cty.Walk and cty.Transform
Browse files Browse the repository at this point in the history
These functions would previously panic if given marked values.

Walk will now walk correctly into marked collections, reporting the marked
collection to the callback and then reporting the elements with no
special additional marking under the assumption that the callback will
track its own parent mark state if it needs it.

Transform and TransformWithTransformer will also both walk correctly into
marked collections. By default they will preserve any marks on containers
as they rebuild corresponding new containers, but the transformer's Exit
method has the opportunity to modify the markings before returning if
needed, which allows using the transform functions to systematically apply
additional marks throughout a data structure.
  • Loading branch information
apparentlymart committed Jun 25, 2021
1 parent b9c7d6b commit 5f5dfca
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 61 deletions.
35 changes: 23 additions & 12 deletions cty/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
return nil
}

// The callback already got a chance to see the mark in our
// call above, so can safely strip it off here in order to
// visit the child elements, which might still have their own marks.
rawVal, _ := val.Unmark()

ty := val.Type()
switch {
case ty.IsObjectType():
for it := val.ElementIterator(); it.Next(); {
for it := rawVal.ElementIterator(); it.Next(); {
nameVal, av := it.Element()
path := append(path, GetAttrStep{
Name: nameVal.AsString(),
Expand All @@ -46,8 +51,8 @@ func walk(path Path, val Value, cb func(Path, Value) (bool, error)) error {
return err
}
}
case val.CanIterateElements():
for it := val.ElementIterator(); it.Next(); {
case rawVal.CanIterateElements():
for it := rawVal.ElementIterator(); it.Next(); {
kv, ev := it.Element()
path := append(path, IndexStep{
Key: kv,
Expand Down Expand Up @@ -134,21 +139,27 @@ func transform(path Path, val Value, t Transformer) (Value, error) {
ty := val.Type()
var newVal Value

// We need to peel off any marks here so that we can dig around
// inside any collection values. We'll reapply these to any
// new collections we construct, but the transformer's Exit
// method gets the final say on what to do with those.
rawVal, marks := val.Unmark()

switch {

case val.IsNull() || !val.IsKnown():
// Can't recurse into null or unknown values, regardless of type
newVal = val

case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
l := val.LengthInt()
l := rawVal.LengthInt()
switch l {
case 0:
// No deep transform for an empty sequence
newVal = val
default:
elems := make([]Value, 0, l)
for it := val.ElementIterator(); it.Next(); {
for it := rawVal.ElementIterator(); it.Next(); {
kv, ev := it.Element()
path := append(path, IndexStep{
Key: kv,
Expand All @@ -161,25 +172,25 @@ func transform(path Path, val Value, t Transformer) (Value, error) {
}
switch {
case ty.IsListType():
newVal = ListVal(elems)
newVal = ListVal(elems).WithMarks(marks)
case ty.IsSetType():
newVal = SetVal(elems)
newVal = SetVal(elems).WithMarks(marks)
case ty.IsTupleType():
newVal = TupleVal(elems)
newVal = TupleVal(elems).WithMarks(marks)
default:
panic("unknown sequence type") // should never happen because of the case we are in
}
}

case ty.IsMapType():
l := val.LengthInt()
l := rawVal.LengthInt()
switch l {
case 0:
// No deep transform for an empty map
newVal = val
default:
elems := make(map[string]Value)
for it := val.ElementIterator(); it.Next(); {
for it := rawVal.ElementIterator(); it.Next(); {
kv, ev := it.Element()
path := append(path, IndexStep{
Key: kv,
Expand All @@ -190,7 +201,7 @@ func transform(path Path, val Value, t Transformer) (Value, error) {
}
elems[kv.AsString()] = newEv
}
newVal = MapVal(elems)
newVal = MapVal(elems).WithMarks(marks)
}

case ty.IsObjectType():
Expand All @@ -212,7 +223,7 @@ func transform(path Path, val Value, t Transformer) (Value, error) {
}
newAVs[name] = newAV
}
newVal = ObjectVal(newAVs)
newVal = ObjectVal(newAVs).WithMarks(marks)
}

default:
Expand Down
128 changes: 79 additions & 49 deletions cty/walk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,27 @@ func TestWalk(t *testing.T) {
}

val := ObjectVal(map[string]Value{
"string": StringVal("hello"),
"number": NumberIntVal(10),
"bool": True,
"list": ListVal([]Value{True}),
"list_empty": ListValEmpty(Bool),
"set": SetVal([]Value{True}),
"set_empty": ListValEmpty(Bool),
"tuple": TupleVal([]Value{True}),
"tuple_empty": EmptyTupleVal,
"map": MapVal(map[string]Value{"true": True}),
"map_empty": MapValEmpty(Bool),
"object": ObjectVal(map[string]Value{"true": True}),
"object_empty": EmptyObjectVal,
"null": NullVal(List(String)),
"unknown": UnknownVal(Map(Bool)),
"string": StringVal("hello"),
"number": NumberIntVal(10),
"bool": True,
"list": ListVal([]Value{True}),
"list_empty": ListValEmpty(Bool),
"set": SetVal([]Value{True}),
"set_empty": ListValEmpty(Bool),
"tuple": TupleVal([]Value{True}),
"tuple_empty": EmptyTupleVal,
"map": MapVal(map[string]Value{"true": True}),
"map_empty": MapValEmpty(Bool),
"object": ObjectVal(map[string]Value{"true": True}),
"object_empty": EmptyObjectVal,
"null": NullVal(List(String)),
"unknown": UnknownVal(Map(Bool)),
"marked_string": StringVal("boop").Mark("blorp"),
"marked_list": ListVal([]Value{True}).Mark("blorp"),
"marked_tuple": TupleVal([]Value{True}).Mark("blorp"),
"marked_set": SetVal([]Value{True}).Mark("blorp"),
"marked_object": ObjectVal(map[string]Value{"true": True}).Mark("blorp"),
"marked_map": MapVal(map[string]Value{"true": True}),
})

gotCalls := map[Call]struct{}{}
Expand All @@ -52,6 +58,17 @@ func TestWalk(t *testing.T) {
{`cty.Path{cty.GetAttrStep{Name:"object_empty"}}`, "object"},
{`cty.Path{cty.GetAttrStep{Name:"null"}}`, "list of string"},
{`cty.Path{cty.GetAttrStep{Name:"unknown"}}`, "map of bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_string"}}`, "string"},
{`cty.Path{cty.GetAttrStep{Name:"marked_list"}}`, "list of bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_list"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`, "bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_set"}}`, "set of bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_set"}, cty.IndexStep{Key:cty.True}}`, "bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_object"}}`, "object"},
{`cty.Path{cty.GetAttrStep{Name:"marked_object"}, cty.GetAttrStep{Name:"true"}}`, "bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_tuple"}}`, "tuple"},
{`cty.Path{cty.GetAttrStep{Name:"marked_tuple"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`, "bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_map"}}`, "map of bool"},
{`cty.Path{cty.GetAttrStep{Name:"marked_map"}, cty.IndexStep{Key:cty.StringVal("true")}}`, "bool"},
}

err := Walk(val, func(path Path, val Value) (bool, error) {
Expand Down Expand Up @@ -95,23 +112,29 @@ func (pathTransformer) Exit(p Path, v Value) (Value, error) {

func TestTransformWithTransformer(t *testing.T) {
val := ObjectVal(map[string]Value{
"string": StringVal("hello"),
"number": NumberIntVal(10),
"bool": True,
"list": ListVal([]Value{True}),
"list_empty": ListValEmpty(Bool),
"set": SetVal([]Value{True}),
"set_empty": ListValEmpty(Bool),
"tuple": TupleVal([]Value{True}),
"tuple_empty": EmptyTupleVal,
"map": MapVal(map[string]Value{"true": True}),
"map_empty": MapValEmpty(Bool),
"object": ObjectVal(map[string]Value{"true": True}),
"object_empty": EmptyObjectVal,
"null": NullVal(String),
"unknown": UnknownVal(Bool),
"null_list": NullVal(List(String)),
"unknown_map": UnknownVal(Map(Bool)),
"string": StringVal("hello"),
"number": NumberIntVal(10),
"bool": True,
"list": ListVal([]Value{True}),
"list_empty": ListValEmpty(Bool),
"set": SetVal([]Value{True}),
"set_empty": ListValEmpty(Bool),
"tuple": TupleVal([]Value{True}),
"tuple_empty": EmptyTupleVal,
"map": MapVal(map[string]Value{"true": True}),
"map_empty": MapValEmpty(Bool),
"object": ObjectVal(map[string]Value{"true": True}),
"object_empty": EmptyObjectVal,
"null": NullVal(String),
"unknown": UnknownVal(Bool),
"null_list": NullVal(List(String)),
"unknown_map": UnknownVal(Map(Bool)),
"marked_string": StringVal("hello").Mark("blorp"),
"marked_list": ListVal([]Value{True}).Mark("blorp"),
"marked_set": SetVal([]Value{True}).Mark("blorp"),
"marked_tuple": TupleVal([]Value{True}).Mark("blorp"),
"marked_map": MapVal(map[string]Value{"true": True}).Mark("blorp"),
"marked_object": ObjectVal(map[string]Value{"true": True}).Mark("blorp"),
})

gotVal, err := TransformWithTransformer(val, pathTransformer{})
Expand All @@ -120,26 +143,33 @@ func TestTransformWithTransformer(t *testing.T) {
}

wantVal := ObjectVal(map[string]Value{
"string": StringVal(`cty.Path{cty.GetAttrStep{Name:"string"}}`),
"number": StringVal(`cty.Path{cty.GetAttrStep{Name:"number"}}`),
"bool": StringVal(`cty.Path{cty.GetAttrStep{Name:"bool"}}`),
"list": ListVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"list"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`)}),
"list_empty": ListValEmpty(Bool),
"set": SetVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"set"}, cty.IndexStep{Key:cty.True}}`)}),
"set_empty": ListValEmpty(Bool),
"tuple": TupleVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"tuple"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`)}),
"tuple_empty": EmptyTupleVal,
"map": MapVal(map[string]Value{"true": StringVal(`cty.Path{cty.GetAttrStep{Name:"map"}, cty.IndexStep{Key:cty.StringVal("true")}}`)}),
"map_empty": MapValEmpty(Bool),
"object": ObjectVal(map[string]Value{"true": StringVal(`cty.Path{cty.GetAttrStep{Name:"object"}, cty.GetAttrStep{Name:"true"}}`)}),
"object_empty": EmptyObjectVal,
"null": StringVal(`cty.Path{cty.GetAttrStep{Name:"null"}}`),
"unknown": StringVal(`cty.Path{cty.GetAttrStep{Name:"unknown"}}`),
"null_list": NullVal(List(String)),
"unknown_map": UnknownVal(Map(Bool)),
"string": StringVal(`cty.Path{cty.GetAttrStep{Name:"string"}}`),
"number": StringVal(`cty.Path{cty.GetAttrStep{Name:"number"}}`),
"bool": StringVal(`cty.Path{cty.GetAttrStep{Name:"bool"}}`),
"list": ListVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"list"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`)}),
"list_empty": ListValEmpty(Bool),
"set": SetVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"set"}, cty.IndexStep{Key:cty.True}}`)}),
"set_empty": ListValEmpty(Bool),
"tuple": TupleVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"tuple"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`)}),
"tuple_empty": EmptyTupleVal,
"map": MapVal(map[string]Value{"true": StringVal(`cty.Path{cty.GetAttrStep{Name:"map"}, cty.IndexStep{Key:cty.StringVal("true")}}`)}),
"map_empty": MapValEmpty(Bool),
"object": ObjectVal(map[string]Value{"true": StringVal(`cty.Path{cty.GetAttrStep{Name:"object"}, cty.GetAttrStep{Name:"true"}}`)}),
"object_empty": EmptyObjectVal,
"null": StringVal(`cty.Path{cty.GetAttrStep{Name:"null"}}`),
"unknown": StringVal(`cty.Path{cty.GetAttrStep{Name:"unknown"}}`),
"null_list": NullVal(List(String)),
"unknown_map": UnknownVal(Map(Bool)),
"marked_string": StringVal(`cty.Path{cty.GetAttrStep{Name:"marked_string"}}`),
"marked_list": ListVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"marked_list"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`)}).Mark("blorp"),
"marked_set": SetVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"marked_set"}, cty.IndexStep{Key:cty.True}}`)}).Mark("blorp"),
"marked_tuple": TupleVal([]Value{StringVal(`cty.Path{cty.GetAttrStep{Name:"marked_tuple"}, cty.IndexStep{Key:cty.NumberIntVal(0)}}`)}).Mark("blorp"),
"marked_map": MapVal(map[string]Value{"true": StringVal(`cty.Path{cty.GetAttrStep{Name:"marked_map"}, cty.IndexStep{Key:cty.StringVal("true")}}`)}).Mark("blorp"),
"marked_object": ObjectVal(map[string]Value{"true": StringVal(`cty.Path{cty.GetAttrStep{Name:"marked_object"}, cty.GetAttrStep{Name:"true"}}`)}).Mark("blorp"),
})

if !gotVal.RawEquals(wantVal) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal)
if got, want := len(gotVal.Type().AttributeTypes()), len(gotVal.Type().AttributeTypes()); got != want {
t.Errorf("wrong length %d; want %d", got, want)
}
Expand Down

0 comments on commit 5f5dfca

Please sign in to comment.