Skip to content

Commit bf7e2c8

Browse files
authored
tfsdk: Update Config, Plan, and State type GetAttribute methods to use reflection (#167)
Reference: #134
1 parent d6d71f6 commit bf7e2c8

File tree

10 files changed

+783
-15
lines changed

10 files changed

+783
-15
lines changed

.changelog/167.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:breaking-change
2+
tfsdk: The `Config`, `Plan`, and `State` type `GetAttribute` methods now return diagnostics only and require the target as the last parameter, similar to the `Get` method.
3+
```
4+
5+
```release-note:enhancement
6+
tfsdk: The `Config`, `Plan`, and `State` type `GetAttribute` methods can now be used to fetch values directly into `attr.Value` implementations and Go types.
7+
```

tfsdk/attribute.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,7 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r
262262
return
263263
}
264264

265-
attributeConfig, diags := req.Config.GetAttribute(ctx, req.AttributePath)
266-
265+
attributeConfig, diags := req.Config.getAttributeValue(ctx, req.AttributePath)
267266
resp.Diagnostics.Append(diags...)
268267

269268
if diags.HasError() {
@@ -447,23 +446,23 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r
447446

448447
// modifyPlan runs all AttributePlanModifiers
449448
func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
450-
attrConfig, diags := req.Config.GetAttribute(ctx, req.AttributePath)
449+
attrConfig, diags := req.Config.getAttributeValue(ctx, req.AttributePath)
451450
resp.Diagnostics.Append(diags...)
452451
// Only on new errors.
453452
if diags.HasError() {
454453
return
455454
}
456455
req.AttributeConfig = attrConfig
457456

458-
attrState, diags := req.State.GetAttribute(ctx, req.AttributePath)
457+
attrState, diags := req.State.getAttributeValue(ctx, req.AttributePath)
459458
resp.Diagnostics.Append(diags...)
460459
// Only on new errors.
461460
if diags.HasError() {
462461
return
463462
}
464463
req.AttributeState = attrState
465464

466-
attrPlan, diags := req.Plan.GetAttribute(ctx, req.AttributePath)
465+
attrPlan, diags := req.Plan.getAttributeValue(ctx, req.AttributePath)
467466
resp.Diagnostics.Append(diags...)
468467
// Only on new errors.
469468
if diags.HasError() {

tfsdk/config.go

+47-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,55 @@ func (c Config) Get(ctx context.Context, target interface{}) diag.Diagnostics {
2222
return reflect.Into(ctx, c.Schema.AttributeType(), c.Raw, target, reflect.Options{})
2323
}
2424

25-
// GetAttribute retrieves the attribute found at `path` and returns it as an
25+
// GetAttribute retrieves the attribute found at `path` and populates the
26+
// `target` with the value.
27+
func (c Config) GetAttribute(ctx context.Context, path *tftypes.AttributePath, target interface{}) diag.Diagnostics {
28+
attrValue, diags := c.getAttributeValue(ctx, path)
29+
30+
if diags.HasError() {
31+
return diags
32+
}
33+
34+
if attrValue == nil {
35+
diags.AddAttributeError(
36+
path,
37+
"Config Read Error",
38+
"An unexpected error was encountered trying to read an attribute from the configuration. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
39+
"Missing attribute value, however no error was returned. Preventing the panic from this situation.",
40+
)
41+
return diags
42+
}
43+
44+
valueAsDiags := ValueAs(ctx, attrValue, target)
45+
46+
// ValueAs does not have path information for its Diagnostics.
47+
// TODO: Update to use diagnostic SetPath method.
48+
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/169
49+
for idx, valueAsDiag := range valueAsDiags {
50+
if valueAsDiag.Severity() == diag.SeverityError {
51+
valueAsDiags[idx] = diag.NewAttributeErrorDiagnostic(
52+
path,
53+
valueAsDiag.Summary(),
54+
valueAsDiag.Detail(),
55+
)
56+
} else if valueAsDiag.Severity() == diag.SeverityWarning {
57+
valueAsDiags[idx] = diag.NewAttributeWarningDiagnostic(
58+
path,
59+
valueAsDiag.Summary(),
60+
valueAsDiag.Detail(),
61+
)
62+
}
63+
}
64+
65+
diags.Append(valueAsDiags...)
66+
67+
return diags
68+
}
69+
70+
// getAttributeValue retrieves the attribute found at `path` and returns it as an
2671
// attr.Value. Consumers should assert the type of the returned value with the
2772
// desired attr.Type.
28-
func (c Config) GetAttribute(ctx context.Context, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) {
73+
func (c Config) getAttributeValue(ctx context.Context, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) {
2974
var diags diag.Diagnostics
3075

3176
attrType, err := c.Schema.AttributeTypeAtPath(path)

tfsdk/config_test.go

+209-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package tfsdk
22

33
import (
44
"context"
5+
"fmt"
6+
"reflect"
57
"testing"
68

79
"github.com/google/go-cmp/cmp"
810
"github.com/hashicorp/terraform-plugin-framework/attr"
911
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
intreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect"
1013
testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types"
1114
"github.com/hashicorp/terraform-plugin-framework/types"
1215
"github.com/hashicorp/terraform-plugin-go/tftypes"
@@ -155,6 +158,210 @@ func TestConfigGet_testTypes(t *testing.T) {
155158
func TestConfigGetAttribute(t *testing.T) {
156159
t.Parallel()
157160

161+
type testCase struct {
162+
config Config
163+
target interface{}
164+
expected interface{}
165+
expectedDiags diag.Diagnostics
166+
}
167+
168+
testCases := map[string]testCase{
169+
"string": {
170+
config: Config{
171+
Raw: tftypes.NewValue(tftypes.Object{
172+
AttributeTypes: map[string]tftypes.Type{
173+
"name": tftypes.String,
174+
},
175+
}, map[string]tftypes.Value{
176+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
177+
}),
178+
Schema: Schema{
179+
Attributes: map[string]Attribute{
180+
"name": {
181+
Type: types.StringType,
182+
Required: true,
183+
},
184+
},
185+
},
186+
},
187+
target: new(string),
188+
expected: newStringPointer("namevalue"),
189+
},
190+
"*string": {
191+
config: Config{
192+
Raw: tftypes.NewValue(tftypes.Object{
193+
AttributeTypes: map[string]tftypes.Type{
194+
"name": tftypes.String,
195+
},
196+
}, map[string]tftypes.Value{
197+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
198+
}),
199+
Schema: Schema{
200+
Attributes: map[string]Attribute{
201+
"name": {
202+
Type: types.StringType,
203+
Required: true,
204+
},
205+
},
206+
},
207+
},
208+
target: new(*string),
209+
expected: newStringPointerPointer("namevalue"),
210+
},
211+
"types.String": {
212+
config: Config{
213+
Raw: tftypes.NewValue(tftypes.Object{
214+
AttributeTypes: map[string]tftypes.Type{
215+
"name": tftypes.String,
216+
},
217+
}, map[string]tftypes.Value{
218+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
219+
}),
220+
Schema: Schema{
221+
Attributes: map[string]Attribute{
222+
"name": {
223+
Type: types.StringType,
224+
Required: true,
225+
},
226+
},
227+
},
228+
},
229+
target: new(types.String),
230+
expected: &types.String{Value: "namevalue"},
231+
},
232+
"incompatible-target": {
233+
config: Config{
234+
Raw: tftypes.NewValue(tftypes.Object{
235+
AttributeTypes: map[string]tftypes.Type{
236+
"name": tftypes.String,
237+
},
238+
}, map[string]tftypes.Value{
239+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
240+
}),
241+
Schema: Schema{
242+
Attributes: map[string]Attribute{
243+
"name": {
244+
Type: types.StringType,
245+
Required: true,
246+
},
247+
},
248+
},
249+
},
250+
target: new(testtypes.String),
251+
expected: new(testtypes.String),
252+
expectedDiags: diag.Diagnostics{
253+
diag.NewAttributeErrorDiagnostic(
254+
tftypes.NewAttributePath().WithAttributeName("name"),
255+
"Value Conversion Error",
256+
intreflect.DiagNewAttributeValueIntoWrongType{
257+
ValType: reflect.TypeOf(types.String{Value: "namevalue"}),
258+
TargetType: reflect.TypeOf(testtypes.String{}),
259+
AttrPath: tftypes.NewAttributePath().WithAttributeName("name"),
260+
SchemaType: types.StringType,
261+
}.Detail(),
262+
),
263+
},
264+
},
265+
"incompatible-type": {
266+
config: Config{
267+
Raw: tftypes.NewValue(tftypes.Object{
268+
AttributeTypes: map[string]tftypes.Type{
269+
"name": tftypes.String,
270+
},
271+
}, map[string]tftypes.Value{
272+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
273+
}),
274+
Schema: Schema{
275+
Attributes: map[string]Attribute{
276+
"name": {
277+
Type: types.StringType,
278+
Required: true,
279+
},
280+
},
281+
},
282+
},
283+
target: new(bool),
284+
expected: new(bool),
285+
expectedDiags: diag.Diagnostics{
286+
diag.NewAttributeErrorDiagnostic(
287+
tftypes.NewAttributePath().WithAttributeName("name"),
288+
"Value Conversion Error",
289+
intreflect.DiagIntoIncompatibleType{
290+
Val: tftypes.NewValue(tftypes.String, "namevalue"),
291+
TargetType: reflect.TypeOf(false),
292+
Err: fmt.Errorf("can't unmarshal %s into *%T, expected boolean", tftypes.String, false),
293+
AttrPath: tftypes.NewAttributePath().WithAttributeName("name"),
294+
}.Detail(),
295+
),
296+
},
297+
},
298+
"AttrTypeWithValidateError": {
299+
config: Config{
300+
Raw: tftypes.NewValue(tftypes.Object{
301+
AttributeTypes: map[string]tftypes.Type{
302+
"name": tftypes.String,
303+
},
304+
}, map[string]tftypes.Value{
305+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
306+
}),
307+
Schema: Schema{
308+
Attributes: map[string]Attribute{
309+
"name": {
310+
Type: testtypes.StringTypeWithValidateError{},
311+
Required: true,
312+
},
313+
},
314+
},
315+
},
316+
target: new(testtypes.String),
317+
expected: new(testtypes.String),
318+
expectedDiags: diag.Diagnostics{testtypes.TestErrorDiagnostic(tftypes.NewAttributePath().WithAttributeName("name"))},
319+
},
320+
"AttrTypeWithValidateWarning": {
321+
config: Config{
322+
Raw: tftypes.NewValue(tftypes.Object{
323+
AttributeTypes: map[string]tftypes.Type{
324+
"name": tftypes.String,
325+
},
326+
}, map[string]tftypes.Value{
327+
"name": tftypes.NewValue(tftypes.String, "namevalue"),
328+
}),
329+
Schema: Schema{
330+
Attributes: map[string]Attribute{
331+
"name": {
332+
Type: testtypes.StringTypeWithValidateWarning{},
333+
Required: true,
334+
},
335+
},
336+
},
337+
},
338+
target: new(testtypes.String),
339+
expected: &testtypes.String{String: types.String{Value: "namevalue"}, CreatedBy: testtypes.StringTypeWithValidateWarning{}},
340+
expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(tftypes.NewAttributePath().WithAttributeName("name"))},
341+
},
342+
}
343+
344+
for name, tc := range testCases {
345+
name, tc := name, tc
346+
t.Run(name, func(t *testing.T) {
347+
t.Parallel()
348+
349+
diags := tc.config.GetAttribute(context.Background(), tftypes.NewAttributePath().WithAttributeName("name"), tc.target)
350+
351+
if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" {
352+
t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff)
353+
}
354+
355+
if diff := cmp.Diff(tc.target, tc.expected, cmp.Transformer("testtypes", func(in *testtypes.String) testtypes.String { return *in }), cmp.Transformer("types", func(in *types.String) types.String { return *in })); diff != "" {
356+
t.Errorf("unexpected value (+wanted, -got): %s", diff)
357+
}
358+
})
359+
}
360+
}
361+
362+
func TestConfigGetAttributeValue(t *testing.T) {
363+
t.Parallel()
364+
158365
type testCase struct {
159366
config Config
160367
path *tftypes.AttributePath
@@ -1025,7 +1232,8 @@ func TestConfigGetAttribute(t *testing.T) {
10251232
t.Run(name, func(t *testing.T) {
10261233
t.Parallel()
10271234

1028-
val, diags := tc.config.GetAttribute(context.Background(), tc.path)
1235+
val, diags := tc.config.getAttributeValue(context.Background(), tc.path)
1236+
10291237
if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" {
10301238
t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff)
10311239
}

tfsdk/plan.go

+47-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,55 @@ func (p Plan) Get(ctx context.Context, target interface{}) diag.Diagnostics {
2222
return reflect.Into(ctx, p.Schema.AttributeType(), p.Raw, target, reflect.Options{})
2323
}
2424

25-
// GetAttribute retrieves the attribute found at `path` and returns it as an
25+
// GetAttribute retrieves the attribute found at `path` and populates the
26+
// `target` with the value.
27+
func (p Plan) GetAttribute(ctx context.Context, path *tftypes.AttributePath, target interface{}) diag.Diagnostics {
28+
attrValue, diags := p.getAttributeValue(ctx, path)
29+
30+
if diags.HasError() {
31+
return diags
32+
}
33+
34+
if attrValue == nil {
35+
diags.AddAttributeError(
36+
path,
37+
"Plan Read Error",
38+
"An unexpected error was encountered trying to read an attribute from the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
39+
"Missing attribute value, however no error was returned. Preventing the panic from this situation.",
40+
)
41+
return diags
42+
}
43+
44+
valueAsDiags := ValueAs(ctx, attrValue, target)
45+
46+
// ValueAs does not have path information for its Diagnostics.
47+
// TODO: Update to use diagnostic SetPath method.
48+
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/169
49+
for idx, valueAsDiag := range valueAsDiags {
50+
if valueAsDiag.Severity() == diag.SeverityError {
51+
valueAsDiags[idx] = diag.NewAttributeErrorDiagnostic(
52+
path,
53+
valueAsDiag.Summary(),
54+
valueAsDiag.Detail(),
55+
)
56+
} else if valueAsDiag.Severity() == diag.SeverityWarning {
57+
valueAsDiags[idx] = diag.NewAttributeWarningDiagnostic(
58+
path,
59+
valueAsDiag.Summary(),
60+
valueAsDiag.Detail(),
61+
)
62+
}
63+
}
64+
65+
diags.Append(valueAsDiags...)
66+
67+
return diags
68+
}
69+
70+
// getAttributeValue retrieves the attribute found at `path` and returns it as an
2671
// attr.Value. Consumers should assert the type of the returned value with the
2772
// desired attr.Type.
28-
func (p Plan) GetAttribute(ctx context.Context, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) {
73+
func (p Plan) getAttributeValue(ctx context.Context, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) {
2974
var diags diag.Diagnostics
3075

3176
attrType, err := p.Schema.AttributeTypeAtPath(path)

0 commit comments

Comments
 (0)