Skip to content

Commit b097d2f

Browse files
megan07Ivan De Marino
and
Ivan De Marino
authored
New function ValueFrom that takes a Go value and populates a compatible attr.Value, given a descriptive attr.Type (#350)
* add value_from * Allowing `ValueFrom` to set the resulting converted value on any compatible pointer provided as `target` * Added CHANGELOG entry * Adding test case to unmarshal 'ValueFrom' a Map Co-authored-by: Ivan De Marino <[email protected]>
1 parent a482b23 commit b097d2f

File tree

4 files changed

+249
-3
lines changed

4 files changed

+249
-3
lines changed

.changelog/350.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
tfsdk: New function `ValueFrom` that takes a Go value and populates a compatible `attr.Value`, given a descriptive `attr.Type`.
3+
```

tfsdk/value_as.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import (
99
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
1010
)
1111

12-
// ValueAs populates the Go value passed as `target` with
13-
// the contents of `val`, using the reflection rules
14-
// defined for `Get` and `GetAttribute`.
12+
// ValueAs takes the attr.Value `val` and populates the Go value `target` with its content.
13+
//
14+
// This is achieved using reflection rules provided by the internal/reflect package.
1515
func ValueAs(ctx context.Context, val attr.Value, target interface{}) diag.Diagnostics {
1616
if reflect.IsGenericAttrValue(ctx, target) {
1717
*(target.(*attr.Value)) = val

tfsdk/value_from.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package tfsdk
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/attr"
7+
"github.com/hashicorp/terraform-plugin-framework/diag"
8+
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
9+
"github.com/hashicorp/terraform-plugin-go/tftypes"
10+
)
11+
12+
// ValueFrom takes the Go value `val` and populates `target` with an attr.Value,
13+
// based on the type definition provided in `targetType`.
14+
//
15+
// This is achieved using reflection rules provided by the internal/reflect package.
16+
func ValueFrom(ctx context.Context, val interface{}, targetType attr.Type, target interface{}) diag.Diagnostics {
17+
v, diags := reflect.FromValue(ctx, targetType, val, tftypes.NewAttributePath())
18+
if diags.HasError() {
19+
return diags
20+
}
21+
22+
return ValueAs(ctx, v, target)
23+
}

tfsdk/value_from_test.go

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package tfsdk
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
)
13+
14+
type person struct {
15+
Name types.String `tfsdk:"name"`
16+
Age types.Int64 `tfsdk:"age"`
17+
OptedIn types.Bool `tfsdk:"opted_in"`
18+
Address types.List `tfsdk:"address"`
19+
FullName types.Map `tfsdk:"full_name"`
20+
}
21+
22+
func TestValueFrom(t *testing.T) {
23+
t.Parallel()
24+
25+
personAttrTypes := map[string]attr.Type{
26+
"name": types.StringType,
27+
"age": types.Int64Type,
28+
"opted_in": types.BoolType,
29+
"address": types.ListType{
30+
ElemType: types.StringType,
31+
},
32+
"full_name": types.MapType{
33+
ElemType: types.StringType,
34+
},
35+
}
36+
37+
mrX := person{
38+
Name: types.String{Value: "x"},
39+
Age: types.Int64{Value: 30},
40+
OptedIn: types.Bool{Value: true},
41+
Address: types.List{
42+
ElemType: types.StringType,
43+
Elems: []attr.Value{
44+
types.String{Value: "1"},
45+
types.String{Value: "Beckford Close"},
46+
types.String{Value: "Gotham"},
47+
},
48+
},
49+
FullName: types.Map{
50+
ElemType: types.StringType,
51+
Elems: map[string]attr.Value{
52+
"first": types.String{Value: "x"},
53+
"middle": types.String{Value: "b"},
54+
"last": types.String{Value: "c"},
55+
},
56+
},
57+
}
58+
59+
mrsY := person{
60+
Name: types.String{Value: "y"},
61+
Age: types.Int64{Value: 23},
62+
OptedIn: types.Bool{Value: false},
63+
Address: types.List{
64+
ElemType: types.StringType,
65+
Elems: []attr.Value{
66+
types.String{Value: "2"},
67+
types.String{Value: "Windmill Close"},
68+
types.String{Value: "Smallville"},
69+
},
70+
},
71+
FullName: types.Map{
72+
ElemType: types.StringType,
73+
Elems: map[string]attr.Value{
74+
"first": types.String{Value: "y"},
75+
"middle": types.String{Value: "e"},
76+
"last": types.String{Value: "f"},
77+
},
78+
},
79+
}
80+
81+
expectedMrXObj := types.Object{
82+
AttrTypes: personAttrTypes,
83+
Attrs: map[string]attr.Value{
84+
"name": types.String{Value: "x", Unknown: false, Null: false},
85+
"age": types.Int64{Value: 30, Unknown: false, Null: false},
86+
"opted_in": types.Bool{Value: true, Unknown: false, Null: false},
87+
"address": types.List{
88+
ElemType: types.StringType,
89+
Elems: []attr.Value{
90+
types.String{Value: "1"},
91+
types.String{Value: "Beckford Close"},
92+
types.String{Value: "Gotham"},
93+
},
94+
},
95+
"full_name": types.Map{
96+
ElemType: types.StringType,
97+
Elems: map[string]attr.Value{
98+
"first": types.String{Value: "x"},
99+
"middle": types.String{Value: "b"},
100+
"last": types.String{Value: "c"},
101+
},
102+
},
103+
},
104+
}
105+
106+
expectedMrsYObj := types.Object{
107+
AttrTypes: personAttrTypes,
108+
Attrs: map[string]attr.Value{
109+
"name": types.String{Value: "y", Unknown: false, Null: false},
110+
"age": types.Int64{Value: 23, Unknown: false, Null: false},
111+
"opted_in": types.Bool{Value: false, Unknown: false, Null: false},
112+
"address": types.List{
113+
ElemType: types.StringType,
114+
Elems: []attr.Value{
115+
types.String{Value: "2"},
116+
types.String{Value: "Windmill Close"},
117+
types.String{Value: "Smallville"},
118+
},
119+
},
120+
"full_name": types.Map{
121+
ElemType: types.StringType,
122+
Elems: map[string]attr.Value{
123+
"first": types.String{Value: "y"},
124+
"middle": types.String{Value: "e"},
125+
"last": types.String{Value: "f"},
126+
},
127+
},
128+
},
129+
}
130+
131+
type testCase struct {
132+
val interface{}
133+
target attr.Value
134+
expected attr.Value
135+
expectedDiags diag.Diagnostics
136+
}
137+
138+
tests := map[string]testCase{
139+
"primitive": {
140+
val: "hello",
141+
target: types.String{},
142+
expected: types.String{Value: "hello", Unknown: false, Null: false},
143+
},
144+
"struct": {
145+
val: mrX,
146+
target: types.Object{
147+
AttrTypes: personAttrTypes,
148+
},
149+
expected: expectedMrXObj,
150+
},
151+
"list": {
152+
val: []person{mrX, mrsY},
153+
target: types.List{
154+
ElemType: types.ObjectType{
155+
AttrTypes: personAttrTypes,
156+
},
157+
},
158+
expected: types.List{
159+
ElemType: types.ObjectType{
160+
AttrTypes: personAttrTypes,
161+
},
162+
Elems: []attr.Value{expectedMrXObj, expectedMrsYObj},
163+
},
164+
},
165+
"map": {
166+
val: map[string]person{
167+
"x": mrX,
168+
"y": mrsY,
169+
},
170+
target: types.Map{
171+
ElemType: types.ObjectType{
172+
AttrTypes: personAttrTypes,
173+
},
174+
},
175+
expected: types.Map{
176+
ElemType: types.ObjectType{
177+
AttrTypes: personAttrTypes,
178+
},
179+
Elems: map[string]attr.Value{
180+
"x": expectedMrXObj,
181+
"y": expectedMrsYObj,
182+
},
183+
},
184+
},
185+
"incompatible-type": {
186+
val: 0,
187+
target: types.String{},
188+
expectedDiags: diag.Diagnostics{
189+
diag.WithPath(
190+
tftypes.NewAttributePath(),
191+
diag.NewErrorDiagnostic(
192+
"Value Conversion Error",
193+
"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",
194+
),
195+
),
196+
},
197+
},
198+
}
199+
200+
for name, tc := range tests {
201+
name, tc := name, tc
202+
t.Run(name, func(t *testing.T) {
203+
t.Parallel()
204+
205+
diags := ValueFrom(context.Background(), tc.val, tc.target.Type(context.Background()), &tc.target)
206+
207+
if diff := cmp.Diff(tc.expectedDiags, diags); diff != "" {
208+
t.Fatalf("Unexpected diff in diagnostics (-wanted, +got): %s", diff)
209+
}
210+
211+
if diags.HasError() {
212+
return
213+
}
214+
215+
if diff := cmp.Diff(tc.expected, tc.target); diff != "" {
216+
t.Fatalf("Unexpected diff in results (-wanted, +got): %s", diff)
217+
}
218+
})
219+
}
220+
}

0 commit comments

Comments
 (0)