Skip to content

Commit

Permalink
fix types.Object handling
Browse files Browse the repository at this point in the history
  • Loading branch information
mgyucht committed Dec 9, 2024
1 parent ed7246e commit 0d49760
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 10 deletions.
45 changes: 39 additions & 6 deletions internal/providers/pluginfw/converters/converters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stretchr/testify/assert"
)

Expand All @@ -34,6 +35,8 @@ type DummyTfSdk struct {
Object types.Object `tfsdk:"object" tf:"optional"`
ObjectPtr types.Object `tfsdk:"object_ptr" tf:"optional"`
Type_ types.String `tfsdk:"type" tf:""` // Test Type_ renaming
EmptyStructList types.List `tfsdk:"empty_struct_list" tf:"optional"`
EmptyStructObject types.Object `tfsdk:"empty_struct_object" tf:"optional"`
}

func (DummyTfSdk) GetComplexFieldTypes(ctx context.Context) map[string]reflect.Type {
Expand All @@ -48,6 +51,8 @@ func (DummyTfSdk) GetComplexFieldTypes(ctx context.Context) map[string]reflect.T
"slice_struct_ptr": reflect.TypeOf(DummyNestedTfSdk{}),
"object": reflect.TypeOf(DummyNestedTfSdk{}),
"object_ptr": reflect.TypeOf(DummyNestedTfSdk{}),
"empty_struct_list": reflect.TypeOf(DummyNestedTfSdkEmpty{}),
"empty_struct_object": reflect.TypeOf(DummyNestedTfSdkEmpty{}),
}
}

Expand Down Expand Up @@ -83,6 +88,8 @@ type DummyNestedTfSdk struct {
Enabled types.Bool `tfsdk:"enabled" tf:"optional"`
}

type DummyNestedTfSdkEmpty struct{}

type DummyGoSdk struct {
Enabled bool `json:"enabled"`
Workers int64 `json:"workers"`
Expand All @@ -103,6 +110,8 @@ type DummyGoSdk struct {
Object DummyNestedGoSdk `json:"object"`
ObjectPtr *DummyNestedGoSdk `json:"object_ptr"`
Type string `json:"type"` // Test Type_ renaming
EmptyStructList []DummyNestedGoSdkEmpty `json:"empty_struct_list"`
EmptyStructObject *DummyNestedGoSdkEmpty `json:"empty_struct_object"`
ForceSendFields []string `json:"-"`
}

Expand All @@ -112,17 +121,14 @@ type DummyNestedGoSdk struct {
ForceSendFields []string `json:"-"`
}

type DummyNestedGoSdkEmpty struct{}

// This function is used to populate empty fields in the tfsdk struct with null values.
// This is required because the Go->TF conversion function instantiates list, map, and
// object fields with empty values, which are not equal to null values in the tfsdk struct.
func populateEmptyFields(c DummyTfSdk) DummyTfSdk {
if c.NoPointerNested.IsNull() {
c.NoPointerNested = types.ListValueMust(dummyType, []attr.Value{
types.ObjectValueMust(dummyType.AttrTypes, map[string]attr.Value{
"name": types.StringNull(),
"enabled": types.BoolNull(),
}),
})
c.NoPointerNested = types.ListNull(dummyType)
}
if c.NestedList.IsNull() {
c.NestedList = types.ListNull(dummyType)
Expand All @@ -146,14 +152,22 @@ func populateEmptyFields(c DummyTfSdk) DummyTfSdk {
c.SliceStructPtr = types.ListNull(dummyType)
}
if c.Object.IsNull() {
// type.Object fields that correspond to structs are considered never to be null.
c.Object = types.ObjectValueMust(dummyType.AttrTypes, map[string]attr.Value{
"name": types.StringNull(),
"enabled": types.BoolNull(),
})
}
if c.ObjectPtr.IsNull() {
// type.Object fields that correspond to pointers are considered null when the Go SDK value is nil.
c.ObjectPtr = types.ObjectNull(dummyType.AttrTypes)
}
if c.EmptyStructList.IsNull() {
c.EmptyStructList = types.ListNull(basetypes.ObjectType{AttrTypes: map[string]attr.Type{}})
}
if c.EmptyStructObject.IsNull() {
c.EmptyStructObject = types.ObjectNull(map[string]attr.Type{})
}
return c
}

Expand Down Expand Up @@ -194,6 +208,7 @@ func TestGoSdkToTfSdkStructConversionFailure(t *testing.T) {
}

var dummyType = tfcommon.NewObjectTyper(DummyNestedTfSdk{}).Type(context.Background()).(types.ObjectType)
var emptyType = basetypes.ObjectType{AttrTypes: map[string]attr.Type{}}

var tests = []struct {
name string
Expand Down Expand Up @@ -361,6 +376,24 @@ var tests = []struct {
DummyTfSdk{Type_: types.StringValue("abc")},
DummyGoSdk{Type: "abc", ForceSendFields: []string{"Type"}},
},
{
"empty list of empty struct to list conversion",
DummyTfSdk{EmptyStructList: types.ListValueMust(emptyType, []attr.Value{})},
DummyGoSdk{EmptyStructList: []DummyNestedGoSdkEmpty{}},
},
{
"non-empty list empty struct to list conversion",
DummyTfSdk{EmptyStructList: types.ListValueMust(emptyType, []attr.Value{
types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}),
types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}),
})},
DummyGoSdk{EmptyStructList: []DummyNestedGoSdkEmpty{{}, {}}},
},
{
"non-nil pointer of empty struct to object conversion",
DummyTfSdk{EmptyStructObject: types.ObjectValueMust(emptyType.AttrTypes, map[string]attr.Value{})},
DummyGoSdk{EmptyStructObject: &DummyNestedGoSdkEmpty{}, ForceSendFields: []string{"EmptyStructObject"}},
},
}

func TestConverter(t *testing.T) {
Expand Down
23 changes: 20 additions & 3 deletions internal/providers/pluginfw/converters/go_to_tf.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,19 @@ func goSdkToTfSdkSingleField(
return
}

// Otherwise, dereference the pointer and continue.
srcField = srcField.Elem()
d.Append(goSdkToTfSdkSingleField(ctx, srcField, destField, forceSendField, tfType, innerType)...)
// Otherwise, the source field is a non-nil pointer to a struct.
// If the target is a list, we treat the source field as a slice with length 1
// containing only the dereferenced pointer.
if destField.Type() == reflect.TypeOf(types.List{}) {
listSrc := reflect.MakeSlice(reflect.SliceOf(srcField.Type().Elem()), 1, 1)
listSrc.Index(0).Set(srcField.Elem())
d.Append(goSdkToTfSdkSingleField(ctx, listSrc, destField, forceSendField, tfType, innerType)...)
return
}

// Otherwise, the target is an object. Dereference the pointer and convert the underlying struct.
d.Append(goSdkToTfSdkSingleField(ctx, srcField.Elem(), destField, forceSendField, tfType, innerType)...)
return
case reflect.Bool:
boolVal := srcField.Interface().(bool)
// check if the value is non-zero or if the field is in the forceSendFields list
Expand Down Expand Up @@ -190,6 +200,13 @@ func goSdkToTfSdkSingleField(
// If the destination field is a types.List, treat the source field as a slice with length 1
// containing only this struct.
if destField.Type() == reflect.TypeOf(types.List{}) {
// For compatibility, a field consisting of a zero-valued struct that is mapped to lists is treated as an
// empty list.
if srcField.IsZero() {
setFieldToNull(destField, tfType)
return
}

listSrc := reflect.MakeSlice(reflect.SliceOf(srcField.Type()), 1, 1)
listSrc.Index(0).Set(srcField)
d.Append(goSdkToTfSdkSingleField(ctx, listSrc, destField, forceSendField, tfType, innerType)...)
Expand Down
15 changes: 14 additions & 1 deletion internal/providers/pluginfw/converters/tf_to_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,20 @@ func tfsdkToGoSdkStructField(
return
}

d.Append(TfSdkToGoSdkStruct(ctx, innerValue.Interface(), destField.Addr().Interface())...)
destType := destField.Type()
if destType.Kind() == reflect.Ptr {
destType = destType.Elem()
}
destValue := reflect.New(destType)
d.Append(TfSdkToGoSdkStruct(ctx, innerValue.Interface(), destValue.Interface())...)
if d.HasError() {
return
}
if destField.Type().Kind() == reflect.Ptr {
destField.Set(destValue)
} else {
destField.Set(destValue.Elem())
}
default:
d.AddError(tfSdkToGoSdkFieldConversionFailureMessage, fmt.Sprintf("%T is not currently supported as a source field. %s", v, common.TerraformBugErrorMessage))
return
Expand Down

0 comments on commit 0d49760

Please sign in to comment.