Skip to content

Commit f431af6

Browse files
authored
Refactor reflect to use attr.Types. (#41)
Reflection has A Problem: We want to support using `attr.Value`s inside of structs, slices, etc. and just having the reflection package Do The Right Thing. This means the reflection package needs to be able to create them. We originally solved that in #30 by just instantiating empty values of their type, and then calling a `SetTerraformValue` method on that value. This was super annoying, because then we needed two methods for getting an `attr.Value` set to the right values: `attr.Type.ValueFromTerraform` and `attr.Value.SetTerraformValue` were basically the same thing. But whatever, it worked. Except, no it didn't. Complex types like lists and maps store their element/attribute types as properties on their structs. It's important that these be set. Only the `attr.Type` has this information, it's not passed in as part of the `tftypes.Value`. So reflect couldn't set those values, and produced broken `attr.Value`s as a result. (Their `ToTerraformValue` methods would run into trouble, because they wouldn't know what `tftypes.Type` to use for elements or attributes). To solve this problem, we decided to supply the `attr.Type` from the schema to the reflect package, wiring it through so that we could instantiate new `attr.Value`s when the opportunity presented itself. This solves our problem, because we got rid of the `attr.Value.SetTerraformValue` method and used the `attr.Type.ValueFromTerraform` directly to just instantiate a new value, which made sure it was set up correctly. But now we have a new problem: what if the `attr.Type` in the schema doesn't produce the `attr.Value` they're trying to assign to? We decided to just throw an error on that one, because there's no reasonable way around it. Depends on #44.
1 parent fefe01c commit f431af6

25 files changed

+817
-1315
lines changed

Diff for: attr/value.go

-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package attr
22

33
import (
44
"context"
5-
6-
"github.com/hashicorp/terraform-plugin-go/tftypes"
75
)
86

97
// Value defines an interface for describing data associated with an attribute.
@@ -14,10 +12,6 @@ type Value interface {
1412
// a Go type that tftypes.NewValue will accept.
1513
ToTerraformValue(context.Context) (interface{}, error)
1614

17-
// SetTerraformValue updates the data in Value to match the
18-
// passed tftypes.Value.
19-
SetTerraformValue(context.Context, tftypes.Value) error
20-
2115
// Equal must return true if the Value is considered semantically equal
2216
// to the Value passed as an argument.
2317
Equal(Value) bool

Diff for: internal/reflect/interfaces.go

+34-22
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ import (
44
"context"
55
"reflect"
66

7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
79
"github.com/hashicorp/terraform-plugin-go/tftypes"
810
)
911

10-
type setUnknownable interface {
12+
// SetUnknownable is an interface for types that can be explicitly set to known
13+
// or unknown.
14+
type SetUnknownable interface {
1115
SetUnknown(context.Context, bool) error
1216
}
1317

14-
// call the SetUnknown method on types that support it.
15-
func reflectUnknownable(ctx context.Context, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
18+
// Unknownable creates a zero value of `target` (or the concrete type it's
19+
// referencing, if it's a pointer) and calls its SetUnknown method.
20+
//
21+
// It is meant to be called through Into, not directly.
22+
func Unknownable(ctx context.Context, typ attr.Type, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
1623
receiver := pointerSafeZeroValue(ctx, target)
1724
method := receiver.MethodByName("SetUnknown")
1825
if !method.IsValid() {
@@ -29,12 +36,16 @@ func reflectUnknownable(ctx context.Context, val tftypes.Value, target reflect.V
2936
return receiver, nil
3037
}
3138

32-
type setNullable interface {
39+
// SetNullable is an interface for types that can be explicitly set to null.
40+
type SetNullable interface {
3341
SetNull(context.Context, bool) error
3442
}
3543

36-
// call the SetNull method on types that support it.
37-
func reflectNullable(ctx context.Context, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
44+
// Nullable creates a zero value of `target` (or the concrete type it's
45+
// referencing, if it's a pointer) and calls its SetNull method.
46+
//
47+
// It is meant to be called through Into, not directly.
48+
func Nullable(ctx context.Context, typ attr.Type, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
3849
receiver := pointerSafeZeroValue(ctx, target)
3950
method := receiver.MethodByName("SetNull")
4051
if !method.IsValid() {
@@ -51,8 +62,11 @@ func reflectNullable(ctx context.Context, val tftypes.Value, target reflect.Valu
5162
return receiver, nil
5263
}
5364

54-
// call the FromTerraform5Value method on types that support it.
55-
func reflectValueConverter(ctx context.Context, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
65+
// ValueConverter creates a zero value of `target` (or the concrete type it's
66+
// referencing, if it's a pointer) and calls its FromTerraform5Value method.
67+
//
68+
// It is meant to be called through Into, not directly.
69+
func ValueConverter(ctx context.Context, typ attr.Type, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
5670
receiver := pointerSafeZeroValue(ctx, target)
5771
method := receiver.MethodByName("FromTerraform5Value")
5872
if !method.IsValid() {
@@ -66,20 +80,18 @@ func reflectValueConverter(ctx context.Context, val tftypes.Value, target reflec
6680
return receiver, nil
6781
}
6882

69-
// call the SetTerraformValue method on attr.Values.
70-
func reflectAttributeValue(ctx context.Context, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
71-
receiver := pointerSafeZeroValue(ctx, target)
72-
method := receiver.MethodByName("SetTerraformValue")
73-
if !method.IsValid() {
74-
return target, path.NewErrorf("unexpectedly couldn't find SetTeraformValue method on type %s", receiver.Type().String())
75-
}
76-
results := method.Call([]reflect.Value{
77-
reflect.ValueOf(ctx),
78-
reflect.ValueOf(val),
79-
})
80-
err := results[0].Interface()
83+
// AttributeValue creates a new reflect.Value by calling the ValueFromTerraform
84+
// method on `typ`. It will return an error if the returned `attr.Value` is not
85+
// the same type as `target`.
86+
//
87+
// It is meant to be called through Into, not directly.
88+
func AttributeValue(ctx context.Context, typ attr.Type, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
89+
res, err := typ.ValueFromTerraform(ctx, val)
8190
if err != nil {
82-
return target, path.NewError(err.(error))
91+
return target, err
8392
}
84-
return receiver, nil
93+
if reflect.TypeOf(res) != target.Type() {
94+
return target, path.NewErrorf("can't use attr.Value %s, only %s is supported because %T is the type in the schema", target.Type(), reflect.TypeOf(res), typ)
95+
}
96+
return reflect.ValueOf(res), nil
8597
}

0 commit comments

Comments
 (0)