Skip to content

Commit

Permalink
Refactor reflect to use attr.Types.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
paddycarver committed Jun 8, 2021
1 parent fefe01c commit 0fe3b76
Show file tree
Hide file tree
Showing 25 changed files with 817 additions and 1,315 deletions.
6 changes: 0 additions & 6 deletions attr/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package attr

import (
"context"

"github.com/hashicorp/terraform-plugin-go/tftypes"
)

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

// SetTerraformValue updates the data in Value to match the
// passed tftypes.Value.
SetTerraformValue(context.Context, tftypes.Value) error

// Equal must return true if the Value is considered semantically equal
// to the Value passed as an argument.
Equal(Value) bool
Expand Down
56 changes: 34 additions & 22 deletions internal/reflect/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import (
"context"
"reflect"

"github.com/hashicorp/terraform-plugin-framework/attr"

"github.com/hashicorp/terraform-plugin-go/tftypes"
)

type setUnknownable interface {
// SetUnknownable is an interface for types that can be explicitly set to known
// or unknown.
type SetUnknownable interface {
SetUnknown(context.Context, bool) error
}

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

type setNullable interface {
// SetNullable is an interface for types that can be explicitly set to null.
type SetNullable interface {
SetNull(context.Context, bool) error
}

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

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

// call the SetTerraformValue method on attr.Values.
func reflectAttributeValue(ctx context.Context, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
receiver := pointerSafeZeroValue(ctx, target)
method := receiver.MethodByName("SetTerraformValue")
if !method.IsValid() {
return target, path.NewErrorf("unexpectedly couldn't find SetTeraformValue method on type %s", receiver.Type().String())
}
results := method.Call([]reflect.Value{
reflect.ValueOf(ctx),
reflect.ValueOf(val),
})
err := results[0].Interface()
// AttributeValue creates a new reflect.Value by calling the ValueFromTerraform
// method on `typ`. It will return an error if the returned `attr.Value` is not
// the same type as `target`.
//
// It is meant to be called through Into, not directly.
func AttributeValue(ctx context.Context, typ attr.Type, val tftypes.Value, target reflect.Value, opts Options, path *tftypes.AttributePath) (reflect.Value, error) {
res, err := typ.ValueFromTerraform(ctx, val)
if err != nil {
return target, path.NewError(err.(error))
return target, err
}
return receiver, nil
if reflect.TypeOf(res) != target.Type() {
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)
}
return reflect.ValueOf(res), nil
}
Loading

0 comments on commit 0fe3b76

Please sign in to comment.