Skip to content

New function ValueFrom that takes a Go value and populates a compatible attr.Value, given a descriptive attr.Type #350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/350.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
tfsdk: New function `ValueFrom` that takes a Go value and populates a compatible `attr.Value`, given a descriptive `attr.Type`.
```
6 changes: 3 additions & 3 deletions tfsdk/value_as.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
)

// ValueAs populates the Go value passed as `target` with
// the contents of `val`, using the reflection rules
// defined for `Get` and `GetAttribute`.
// ValueAs takes the attr.Value `val` and populates the Go value `target` with its content.
//
// This is achieved using reflection rules provided by the internal/reflect package.
func ValueAs(ctx context.Context, val attr.Value, target interface{}) diag.Diagnostics {
if reflect.IsGenericAttrValue(ctx, target) {
*(target.(*attr.Value)) = val
Expand Down
23 changes: 23 additions & 0 deletions tfsdk/value_from.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package tfsdk

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// ValueFrom takes the Go value `val` and populates `target` with an attr.Value,
// based on the type definition provided in `targetType`.
//
// This is achieved using reflection rules provided by the internal/reflect package.
func ValueFrom(ctx context.Context, val interface{}, targetType attr.Type, target interface{}) diag.Diagnostics {
v, diags := reflect.FromValue(ctx, targetType, val, tftypes.NewAttributePath())
if diags.HasError() {
return diags
}

return ValueAs(ctx, v, target)
}
220 changes: 220 additions & 0 deletions tfsdk/value_from_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package tfsdk

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"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-go/tftypes"
)

type person struct {
Name types.String `tfsdk:"name"`
Age types.Int64 `tfsdk:"age"`
OptedIn types.Bool `tfsdk:"opted_in"`
Address types.List `tfsdk:"address"`
FullName types.Map `tfsdk:"full_name"`
}

func TestValueFrom(t *testing.T) {
t.Parallel()

personAttrTypes := map[string]attr.Type{
"name": types.StringType,
"age": types.Int64Type,
"opted_in": types.BoolType,
"address": types.ListType{
ElemType: types.StringType,
},
"full_name": types.MapType{
ElemType: types.StringType,
},
}

mrX := person{
Name: types.String{Value: "x"},
Age: types.Int64{Value: 30},
OptedIn: types.Bool{Value: true},
Address: types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "1"},
types.String{Value: "Beckford Close"},
types.String{Value: "Gotham"},
},
},
FullName: types.Map{
ElemType: types.StringType,
Elems: map[string]attr.Value{
"first": types.String{Value: "x"},
"middle": types.String{Value: "b"},
"last": types.String{Value: "c"},
},
},
}

mrsY := person{
Name: types.String{Value: "y"},
Age: types.Int64{Value: 23},
OptedIn: types.Bool{Value: false},
Address: types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "2"},
types.String{Value: "Windmill Close"},
types.String{Value: "Smallville"},
},
},
FullName: types.Map{
ElemType: types.StringType,
Elems: map[string]attr.Value{
"first": types.String{Value: "y"},
"middle": types.String{Value: "e"},
"last": types.String{Value: "f"},
},
},
}

expectedMrXObj := types.Object{
AttrTypes: personAttrTypes,
Attrs: map[string]attr.Value{
"name": types.String{Value: "x", Unknown: false, Null: false},
"age": types.Int64{Value: 30, Unknown: false, Null: false},
"opted_in": types.Bool{Value: true, Unknown: false, Null: false},
"address": types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "1"},
types.String{Value: "Beckford Close"},
types.String{Value: "Gotham"},
},
},
"full_name": types.Map{
ElemType: types.StringType,
Elems: map[string]attr.Value{
"first": types.String{Value: "x"},
"middle": types.String{Value: "b"},
"last": types.String{Value: "c"},
},
},
},
}

expectedMrsYObj := types.Object{
AttrTypes: personAttrTypes,
Attrs: map[string]attr.Value{
"name": types.String{Value: "y", Unknown: false, Null: false},
"age": types.Int64{Value: 23, Unknown: false, Null: false},
"opted_in": types.Bool{Value: false, Unknown: false, Null: false},
"address": types.List{
ElemType: types.StringType,
Elems: []attr.Value{
types.String{Value: "2"},
types.String{Value: "Windmill Close"},
types.String{Value: "Smallville"},
},
},
"full_name": types.Map{
ElemType: types.StringType,
Elems: map[string]attr.Value{
"first": types.String{Value: "y"},
"middle": types.String{Value: "e"},
"last": types.String{Value: "f"},
},
},
},
}

type testCase struct {
val interface{}
target attr.Value
expected attr.Value
expectedDiags diag.Diagnostics
}

tests := map[string]testCase{
"primitive": {
val: "hello",
target: types.String{},
expected: types.String{Value: "hello", Unknown: false, Null: false},
},
"struct": {
val: mrX,
target: types.Object{
AttrTypes: personAttrTypes,
},
expected: expectedMrXObj,
},
"list": {
val: []person{mrX, mrsY},
target: types.List{
ElemType: types.ObjectType{
AttrTypes: personAttrTypes,
},
},
expected: types.List{
ElemType: types.ObjectType{
AttrTypes: personAttrTypes,
},
Elems: []attr.Value{expectedMrXObj, expectedMrsYObj},
},
},
"map": {
val: map[string]person{
"x": mrX,
"y": mrsY,
},
target: types.Map{
ElemType: types.ObjectType{
AttrTypes: personAttrTypes,
},
},
expected: types.Map{
ElemType: types.ObjectType{
AttrTypes: personAttrTypes,
},
Elems: map[string]attr.Value{
"x": expectedMrXObj,
"y": expectedMrsYObj,
},
},
},
"incompatible-type": {
val: 0,
target: types.String{},
expectedDiags: diag.Diagnostics{
diag.WithPath(
tftypes.NewAttributePath(),
diag.NewErrorDiagnostic(
"Value Conversion Error",
"An unexpected error was encountered trying to convert the Terraform value. This is always an error in the provider. Please report the following to the provider developer:\n\ncan't unmarshal tftypes.Number into *string, expected string",
),
),
},
},
}

for name, tc := range tests {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()

diags := ValueFrom(context.Background(), tc.val, tc.target.Type(context.Background()), &tc.target)

if diff := cmp.Diff(tc.expectedDiags, diags); diff != "" {
t.Fatalf("Unexpected diff in diagnostics (-wanted, +got): %s", diff)
}

if diags.HasError() {
return
}

if diff := cmp.Diff(tc.expected, tc.target); diff != "" {
t.Fatalf("Unexpected diff in results (-wanted, +got): %s", diff)
}
})
}
}