Skip to content

Commit 83596bb

Browse files
Fix transform property value to preserve nil arrays and objects (#2655)
The `propertyvalue.Transform` functions had a bug where it would always mutate nil arrays and maps into empty ones: ``` value=resource.PropertyValue{} ``` would become: ``` value=resource.PropertyValue{ V: []resource.PropertyValue{ }, } ``` This had to do with typed nil interfaces in go: https://dave.cheney.net/2017/08/09/typed-nils-in-go-2 The `V` in `resource.PropertyValue` is an interface, so it might be non-nil `!= nil` but the underlying value is nil. This change adds a fix and some tests for the problem.
1 parent 37482e6 commit 83596bb

File tree

2 files changed

+71
-14
lines changed

2 files changed

+71
-14
lines changed

unstable/propertyvalue/propertyvalue.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,32 +39,50 @@ func TransformErr(
3939
}, value)
4040
}
4141

42+
// isNilArray returns true if a property value is not nil but its underlying array is nil.
43+
// See https://dave.cheney.net/2017/08/09/typed-nils-in-go-2 for more details.
44+
func isNilArray(value resource.PropertyValue) bool {
45+
return value.IsArray() && !value.IsNull() && value.ArrayValue() == nil
46+
}
47+
48+
// isNilObject returns true if a property value is not nil but its underlying object is nil.
49+
// See https://dave.cheney.net/2017/08/09/typed-nils-in-go-2 for more details.
50+
func isNilObject(value resource.PropertyValue) bool {
51+
return value.IsObject() && !value.IsNull() && value.ObjectValue() == nil
52+
}
53+
4254
func TransformPropertyValue(
4355
path resource.PropertyPath,
4456
transformer func(resource.PropertyPath, resource.PropertyValue) (resource.PropertyValue, error),
4557
value resource.PropertyValue,
4658
) (resource.PropertyValue, error) {
4759
switch {
4860
case value.IsArray():
49-
tvs := []resource.PropertyValue{}
50-
for i, v := range value.ArrayValue() {
51-
tv, err := TransformPropertyValue(extendPath(path, i), transformer, v)
52-
if err != nil {
53-
return resource.NewNullProperty(), err
61+
// preserve nil arrays
62+
if !isNilArray(value) {
63+
tvs := []resource.PropertyValue{}
64+
for i, v := range value.ArrayValue() {
65+
tv, err := TransformPropertyValue(extendPath(path, i), transformer, v)
66+
if err != nil {
67+
return resource.NewNullProperty(), err
68+
}
69+
tvs = append(tvs, tv)
5470
}
55-
tvs = append(tvs, tv)
71+
value = resource.NewArrayProperty(tvs)
5672
}
57-
value = resource.NewArrayProperty(tvs)
5873
case value.IsObject():
59-
pm := make(resource.PropertyMap)
60-
for k, v := range value.ObjectValue() {
61-
tv, err := TransformPropertyValue(extendPath(path, string(k)), transformer, v)
62-
if err != nil {
63-
return resource.NewNullProperty(), err
74+
// preserve nil objects
75+
if !isNilObject(value) {
76+
pm := make(resource.PropertyMap)
77+
for k, v := range value.ObjectValue() {
78+
tv, err := TransformPropertyValue(extendPath(path, string(k)), transformer, v)
79+
if err != nil {
80+
return resource.NewNullProperty(), err
81+
}
82+
pm[k] = tv
6483
}
65-
pm[k] = tv
84+
value = resource.NewObjectProperty(pm)
6685
}
67-
value = resource.NewObjectProperty(pm)
6886
case value.IsOutput():
6987
o := value.OutputValue()
7088
te, err := TransformPropertyValue(path, transformer, o.Element)

unstable/propertyvalue/propertyvalue_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ package propertyvalue
1717
import (
1818
"testing"
1919

20+
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
2021
rtesting "github.com/pulumi/pulumi/sdk/v3/go/common/resource/testing"
22+
"github.com/stretchr/testify/require"
2123
"pgregory.net/rapid"
2224
)
2325

@@ -30,3 +32,40 @@ func TestRemoveSecrets(t *testing.T) {
3032
}
3133
})
3234
}
35+
36+
func TestIsNilArray(t *testing.T) {
37+
t.Parallel()
38+
39+
require.True(t, isNilArray(resource.PropertyValue{V: []resource.PropertyValue(nil)}))
40+
require.False(t, isNilArray(resource.PropertyValue{V: []resource.PropertyValue{}}))
41+
}
42+
43+
func TestIsNilObject(t *testing.T) {
44+
t.Parallel()
45+
46+
require.True(t, isNilObject(resource.PropertyValue{V: resource.PropertyMap(nil)}))
47+
require.False(t, isNilObject(resource.PropertyValue{V: resource.PropertyMap{}}))
48+
}
49+
50+
func TestTransformPreservesNilArrays(t *testing.T) {
51+
t.Parallel()
52+
53+
nilArrayPV := resource.PropertyValue{V: []resource.PropertyValue(nil)}
54+
result := Transform(func(value resource.PropertyValue) resource.PropertyValue {
55+
return value
56+
}, nilArrayPV)
57+
58+
require.True(t, result.IsArray())
59+
require.Nil(t, result.ArrayValue())
60+
}
61+
62+
func TestTransformPreservesNilObjects(t *testing.T) {
63+
t.Parallel()
64+
65+
nilObjectPV := resource.PropertyValue{V: resource.PropertyMap(nil)}
66+
result := Transform(func(value resource.PropertyValue) resource.PropertyValue {
67+
return value
68+
}, nilObjectPV)
69+
require.True(t, result.IsObject())
70+
require.Nil(t, result.ObjectValue())
71+
}

0 commit comments

Comments
 (0)