Skip to content

Commit 251726e

Browse files
authored
fix: serialise value.List fields through proto round-trip (#61)
1 parent d2cf3e5 commit 251726e

4 files changed

Lines changed: 96 additions & 0 deletions

File tree

pkg/tree/convert.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ func structToValueObject(v reflect.Value) *prototree.ValueObject {
3333
if v.Kind() == reflect.Pointer {
3434
v = v.Elem()
3535
}
36+
// Make the struct addressable so its fields can satisfy pointer-receiver Valuer interfaces.
37+
if !v.CanAddr() {
38+
addr := reflect.New(v.Type()).Elem()
39+
addr.Set(v)
40+
v = addr
41+
}
3642

3743
obj := &prototree.ValueObject{
3844
Entries: make(map[string]*prototree.Value),
@@ -66,6 +72,9 @@ func fieldToProtoValue(field reflect.Value) *prototree.Value {
6672
if field.Type().Implements(valuerType) {
6773
return field.Interface().(value.Valuer).ToProto()
6874
}
75+
if field.CanAddr() && reflect.PointerTo(field.Type()).Implements(valuerType) {
76+
return field.Addr().Interface().(value.Valuer).ToProto()
77+
}
6978

7079
switch field.Kind() {
7180
case reflect.Pointer:

pkg/tree/proto_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/infracost/go-proto/pkg/tree/aws"
77
"github.com/infracost/go-proto/pkg/tree/aws/ec2"
8+
"github.com/infracost/go-proto/pkg/tree/aws/eks"
89
"github.com/infracost/go-proto/pkg/tree/resource"
910
"github.com/infracost/go-proto/pkg/tree/value"
1011
prototree "github.com/infracost/proto/gen/go/infracost/tree"
@@ -87,6 +88,33 @@ func TestRoundTrip(t *testing.T) {
8788
assert.Equal(t, "c5.xlarge", result.AWS.EC2.Instances[2].Type.Value())
8889
}
8990

91+
func TestRoundTrip_ListField(t *testing.T) {
92+
original := &Tree{
93+
AWS: aws.AWS{
94+
EKS: eks.EKS{
95+
NodeGroups: []eks.NodeGroup{{
96+
InstanceTypes: *value.NewList([]value.Value[string]{
97+
value.New("t3.micro", 0, "instance_types", nil),
98+
value.New("t3.small", 0, "instance_types", nil),
99+
}, 0, "instance_types", nil),
100+
}},
101+
},
102+
},
103+
}
104+
105+
proto, err := original.ToProto()
106+
require.NoError(t, err)
107+
108+
result, err := FromProto(proto)
109+
require.NoError(t, err)
110+
111+
require.Len(t, result.AWS.EKS.NodeGroups, 1)
112+
items := result.AWS.EKS.NodeGroups[0].InstanceTypes.Items()
113+
require.Len(t, items, 2, "list items should survive proto round-trip")
114+
assert.Equal(t, "t3.micro", items[0].Value())
115+
assert.Equal(t, "t3.small", items[1].Value())
116+
}
117+
90118
func TestFromProtoUnmappedProvider(t *testing.T) {
91119
proto := &prototree.Tree{
92120
Providers: map[string]*prototree.Provider{

pkg/tree/value/value.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type Value[T Primitive] struct {
2121
var (
2222
_ Valuer = (*Value[bool])(nil)
2323
_ Settable = (*Value[bool])(nil)
24+
_ Valuer = (*List[bool])(nil)
25+
_ Settable = (*List[bool])(nil)
2426
)
2527

2628
// Settable extends Valuer to allow setting the proto value via reflection.
@@ -325,6 +327,43 @@ func (l *List[T]) Items() []Value[T] {
325327
return l.items
326328
}
327329

330+
func (l *List[T]) ToProto() *prototree.Value {
331+
if l == nil {
332+
return nil
333+
}
334+
pl := &prototree.ValueList{
335+
Values: make([]*prototree.Value, len(l.items)),
336+
}
337+
for i, item := range l.items {
338+
pl.Values[i] = item.ToProto()
339+
}
340+
return &prototree.Value{
341+
Flags: l.Flags,
342+
SourceFieldName: l.SourceFieldName,
343+
Source: l.Source,
344+
Value: &prototree.Value_ListValue{ListValue: pl},
345+
}
346+
}
347+
348+
func (l *List[T]) SetProto(p *prototree.Value) {
349+
if l == nil || p == nil {
350+
return
351+
}
352+
l.Flags = p.Flags
353+
l.SourceFieldName = p.SourceFieldName
354+
l.Source = p.Source
355+
lv := p.GetListValue()
356+
if lv == nil {
357+
l.items = nil
358+
return
359+
}
360+
items := make([]Value[T], 0, len(lv.Values))
361+
for _, item := range lv.Values {
362+
items = append(items, FromProto[T](item))
363+
}
364+
l.items = items
365+
}
366+
328367
func (l *List[T]) Contains(v T) bool {
329368
for _, item := range l.items {
330369
if item.Value() == v {

pkg/tree/value/value_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/infracost/go-proto/pkg/flag"
77
prototree "github.com/infracost/proto/gen/go/infracost/tree"
88
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
910
)
1011

1112
func TestStringValue(t *testing.T) {
@@ -217,6 +218,25 @@ func TestNewList_Empty(t *testing.T) {
217218
assert.Empty(t, l.Items())
218219
}
219220

221+
func TestList_ToProtoRoundTrip(t *testing.T) {
222+
in := NewList([]Value[string]{
223+
New("a", flag.TerraformCode, "items", nil),
224+
New("b", flag.TerraformCode, "items", nil),
225+
}, flag.TerraformCode, "items", nil)
226+
227+
p := in.ToProto()
228+
require.NotNil(t, p)
229+
require.NotNil(t, p.GetListValue())
230+
require.Len(t, p.GetListValue().Values, 2)
231+
232+
var out List[string]
233+
out.SetProto(p)
234+
assert.Equal(t, "items", *out.SourceFieldName)
235+
require.Len(t, out.Items(), 2)
236+
assert.Equal(t, "a", out.Items()[0].Value())
237+
assert.Equal(t, "b", out.Items()[1].Value())
238+
}
239+
220240
func TestContains(t *testing.T) {
221241
v := New("hello world", 0, "", nil)
222242
assert.True(t, v.Contains("world"))

0 commit comments

Comments
 (0)