Skip to content

Commit dbaadb0

Browse files
committed
types: Deprecate Attrs, AttrTypes, Elems, ElemTypes, Null, Unknown, and Value fields
Reference: #447 When the framework type system was originally being developed, the value types were introduced with exported fields which also served as the internal details of whether a value was null, unknown, or a known value of a friendlier Go type. It was known that there was the potential for issues, but the simplified developer experience seemed to outweigh the potential for developer issues. Fast forward a few months, this decision appears to have two consequences that the framework maintainers hear about across various forums. One issue is that the value types directly expose their internal implementation details and support the three states of a Terraform type value: being null, unknown, or a known value. Only one state should ever be set, but provider developers can make a value that is any combination of those states. This makes the framework behavior potentially indeterminate from the provider developer perspective whether, for example, a null AND unknown value becomes null OR unknown as it works its way through the framework. ```go type ThingResourceModel struct{ Computed types.String `tfsdk:"computed"` } func (r ThingResource) Create(ctx context.Context, req resource.CreateResource, resp *resource.CreateResponse) { var data ThingResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) tflog.Trace(ctx, "Data Values", map[string]any{ // Unknown value: types.String{Null: false, Unknown: true, Value: ""} "computed": plan.Computed, }) // Maybe some external API responses here, but showing hardcoded updates for // brevity. This will make the value invalid by enabling Null without // disabling Unknown. data.Computed.Null = true tflog.Trace(ctx, "Data Values", map[string]any{ // Invalid value: types.String{Null: true, Unknown: true, Value: ""} "computed": data.Computed, }) // The invalid value will be either null or unknown, depending on the // type implementation. If unknown, Terraform will error, since unknown // values are never allowed in state. resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } ``` Another issue is that it is possible to create collection value types that do not match their type definition. This issue is especially more likely with `types.Object` where it is possible to accidentially omit attributes. While the framework handling would eventually catch this issue when dealing with the invalid value, it can be caught sooner. ```go // Invalid value (missing attribute and differing attribute name) types.Object{ AttrTypes: map[string]attr.Type{ "one": types.StringType, "two": types.BoolType, }, Attrs: map[string]attr.Value{ "not_one": types.String{Value: "wrong name"}, }, } ``` Another issue is that the default (zero-value) state for an "unset" value type turns into a known value, which is confusing since these values explicitly support being null. This causes Terraform errors which would surface to practitioners (especially when untested) that provider developers then have to troubleshoot the error message containing Terraform's type system details, potentially discover the reason why it is happening by looking at the framework type source code, then figure out a workable solution. It's not intuitive. ```go type ThingResourceModel struct{ // let's assume this is left unconfigured (null in config and plan) Optional types.String `tfsdk:"optional"` } func (r ThingResource) Create(ctx context.Context, req resource.CreateResource, resp *resource.CreateResponse) { // Providers can opt to use a single variable that is updated based on an // external response, however that logic can be more difficult sometimes, // so it can be easier to split them. Showing the split way to exemplify // the "unset" problem. var plan, state ThingResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) tflog.Trace(ctx, "Plan Values", map[string]any{ // Null value: types.String{Null: true, Unknown: false, Value: ""} "optional": plan.Optional, }) // Maybe some external API responses here, but intentionally not // doing any state.Optional setting, which might happen if the // external response for that data was null for example. tflog.Trace(ctx, "State Values", map[string]any{ // Zero-value: types.String{Null: false, Unknown: false, Value: ""} "optional": state.Optional, }) // The state zero-value will later cause Terraform to error, such as: // Error: Provider produced inconsistent result after apply // ... expected cty.NullVal(cty.String), got cty.StringVal("") // Since the plan value said it would be null. resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } ``` This deprecation of the fields in preference of functions and methods aims to unexport the internal details and treat the value types as immutable once they are created. When provider developers switch over to the new model, any errant changes to the deprecated exported fields will have no effect. A future version will remove the exported fields entirely and switch the zero-value implementation of these values to being null, instead of a known zero-value of the underlying type. While not recommended for production usage without extensive testing, provider developers can opt for `panic()` inducing versions of collection value creations, rather than dealing with `diag.Diagnostics` everywhere. Accessing value information after the migration can be accomplished with the following: | Prior Value Access | New Value Access | |----------------------|--------------------| | `(types.Bool).Value` | `(types.Bool).ValueBool()` | | `(types.Bool).Null` | `(types.Bool).IsNull()` | | `(types.Bool).Unknown` | `(types.Bool).IsUnknown()` | | `(types.Float64).Value` | `(types.Float64).ValueFloat64()` | | `(types.Float64).Null` | `(types.Float64).IsNull()` | | `(types.Float64).Unknown` | `(types.Float64).IsUnknown()` | | `(types.Int64).Value` | `(types.Int64).ValueInt64()` | | `(types.Int64).Null` | `(types.Int64).IsNull()` | | `(types.Int64).Unknown` | `(types.Int64).IsUnknown()` | | `(types.List).Elems` | `(types.List).Elements()` or `(types.List).ElementsAs()` | | `(types.List).ElemType` | `(types.List).ElementType()` | | `(types.List).Null` | `(types.List).IsNull()` | | `(types.List).Unknown` | `(types.List).IsUnknown()` | | `(types.Map).Elems` | `(types.Map).Elements()` or `(types.Map).ElementsAs()` | | `(types.Map).ElemType` | `(types.Map).ElementType()` | | `(types.Map).Null` | `(types.Map).IsNull()` | | `(types.Map).Unknown` | `(types.Map).IsUnknown()` | | `(types.Number).Value` | `(types.Number).ValueBigFloat()` | | `(types.Number).Null` | `(types.Number).IsNull()` | | `(types.Number).Unknown` | `(types.Number).IsUnknown()` | | `(types.Object).Attrs` | `(types.Object).Attributes()` or `(types.Object).As()` | | `(types.Object).AttrTypes` | `(types.Object).AttributeTypes()` | | `(types.Object).Null` | `(types.Object).IsNull()` | | `(types.Object).Unknown` | `(types.Object).IsUnknown()` | | `(types.Set).Elems` | `(types.Set).Elements()` or `(types.Set).ElementsAs()` | | `(types.Set).ElemType` | `(types.Set).ElementType()` | | `(types.Set).Null` | `(types.Set).IsNull()` | | `(types.Set).Unknown` | `(types.Set).IsUnknown()` | | `(types.String).Value` | `(types.String).ValueString()` | | `(types.String).Null` | `(types.String).IsNull()` | | `(types.String).Unknown` | `(types.String).IsUnknown()` | Go does not allow methods with the same name as a struct field, so a `ValueXXX()` method where XXX represents the returned type was chosen. After the `Value` struct fields are removed, there can be consideration for dropping the XXX in the method naming. Creating values after the migration can be accomplished with the following: | Prior Value Creation | New Value Creation | |----------------------|--------------------| | `types.Bool{Value: /* value */}` | `types.BoolValue(/* value */)` | | `types.Bool{Null: true}` | `types.BoolNull()` | | `types.Bool{Unknown: true}` | `types.BoolUnknown()` | | `types.Float64{Value: /* value */}` | `types.Float64Value(/* value */)` | | `types.Float64{Null: true}` | `types.Float64Null()` | | `types.Float64{Unknown: true}` | `types.Float64Unknown()` | | `types.Int64{Value: /* value */}` | `types.Int64Value(/* value */)` | | `types.Int64{Null: true}` | `types.Int64Null()` | | `types.Int64{Unknown: true}` | `types.Int64Unknown()` | | `types.List{ElemType: /* element type */, Elems: /* value */}` | `diags := types.ListValue(/* element type */, /* value */)` | | `types.List{ElemType: /* element type */, Null: true}` | `types.ListNull(/* element type */)` | | `types.List{ElemType: /* element type */, Unknown: true}` | `types.ListUnknown(/* element type */)` | | `types.Map{ElemType: /* element type */, Elems: /* value */}` | `diags := types.MapValue(/* element type */, /* value */)` | | `types.Map{ElemType: /* element type */, Null: true}` | `types.MapNull(/* element type */)` | | `types.Map{ElemType: /* element type */, Unknown: true}` | `types.MapUnknown(/* element type */)` | | `types.Number{Value: /* value */}` | `types.NumberValue(/* value */)` | | `types.Number{Null: true}` | `types.NumberNull()` | | `types.Number{Unknown: true}` | `types.NumberUnknown()` | | `types.Object{AttrTypes: /* attribute types */, Attrs: /* attribute values */}` | `diags := types.ObjectValue(/* attribute types */, /* attribute values */)` | | `types.Object{AttrTypes: /* attribute types */, Null: true}` | `types.ObjectNull(/* attribute types */)` | | `types.Object{AttrTypes: /* attribute types */, Unknown: true}` | `types.ObjectUnknown(/* attribute types */)` | | `types.Set{ElemType: /* element type */, Elems: /* value */}` | `diags := types.SetValue(/* element type */, /* value */)` | | `types.Set{ElemType: /* element type */, Null: true}` | `types.SetNull(/* element type */)` | | `types.Set{ElemType: /* element type */, Unknown: true}` | `types.SetUnknown(/* element type */)` | | `types.String{Value: /* value */}` | `types.StringValue(/* value */)` | | `types.String{Null: true}` | `types.StringNull()` | | `types.String{Unknown: true}` | `types.StringUnknown()` |
1 parent 2be6665 commit dbaadb0

File tree

65 files changed

+7991
-1573
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+7991
-1573
lines changed

.changelog/502.txt

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
```release-note:note
2+
types: The `Bool` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `BoolNull()`, `BoolUnknown()`, and `BoolValue()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueBool()` methods. The fields will be removed in a future release.
3+
```
4+
5+
```release-note:note
6+
types: The `Float64` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `Float64Null()`, `Float64Unknown()`, and `Float64Value()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueFloat64()` methods. The fields will be removed in a future release.
7+
```
8+
9+
```release-note:note
10+
types: The `Int64` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `Int64Null()`, `Int64Unknown()`, and `Int64Value()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueInt64()` methods. The fields will be removed in a future release.
11+
```
12+
13+
```release-note:note
14+
types: The `List` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `ListNull()`, `ListUnknown()`, and `ListValue()` creation functions and `Elements()`, `ElementsAs()`, `ElementType()`, `IsNull()`, and `IsUnknown()` methods. The fields will be removed in a future release.
15+
```
16+
17+
```release-note:note
18+
types: The `Map` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `MapNull()`, `MapUnknown()`, and `MapValue()` creation functions and `Elements()`, `ElementsAs()`, `ElementType()`, `IsNull()`, and `IsUnknown()` methods. The fields will be removed in a future release.
19+
```
20+
21+
```release-note:note
22+
types: The `Number` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `NumberNull()`, `NumberUnknown()`, and `NumberValue()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueBigFloat()` methods. The fields will be removed in a future release.
23+
```
24+
25+
```release-note:note
26+
types: The `Set` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `SetNull()`, `SetUnknown()`, and `SetValue()` creation functions and `Elements()`, `ElementsAs()`, `ElementType()`, `IsNull()`, and `IsUnknown()` methods. The fields will be removed in a future release.
27+
```
28+
29+
```release-note:note
30+
types: The `String` type `Null`, `Unknown`, and `Value` fields have been deprecated in preference of the `StringNull()`, `StringUnknown()`, and `StringValue()` creation functions and `IsNull()`, `IsUnknown()`, and `ValueString()` methods. The fields will be removed in a future release.
31+
```
32+
33+
```release-note:enhancement
34+
types: Added `BoolNull()`, `BoolUnknown()`, `BoolValue()` functions, which create immutable `Bool` values
35+
```
36+
37+
```release-note:enhancement
38+
types: Added `Float64Null()`, `Float64Unknown()`, `Float64Value()` functions, which create immutable `Float64` values
39+
```
40+
41+
```release-note:enhancement
42+
types: Added `Int64Null()`, `Int64Unknown()`, `Int64Value()` functions, which create immutable `Int64` values
43+
```
44+
45+
```release-note:enhancement
46+
types: Added `ListNull()`, `ListUnknown()`, `ListValue()` functions, which create immutable `List` values
47+
```
48+
49+
```release-note:enhancement
50+
types: Added `MapNull()`, `MapUnknown()`, `MapValue()` functions, which create immutable `Map` values
51+
```
52+
53+
```release-note:enhancement
54+
types: Added `NumberNull()`, `NumberUnknown()`, `NumberValue()` functions, which create immutable `Number` values
55+
```
56+
57+
```release-note:enhancement
58+
types: Added `SetNull()`, `SetUnknown()`, `SetValue()` functions, which create immutable `Set` values
59+
```
60+
61+
```release-note:enhancement
62+
types: Added `StringNull()`, `StringUnknown()`, `StringValue()` functions, which create immutable `String` values
63+
```
64+
65+
```release-note:enhancement
66+
types: Added `Bool` type `ValueBool()` method, which returns the `bool` of the known value or `false` if null or unknown
67+
```
68+
69+
```release-note:enhancement
70+
types: Added `Float64` type `ValueFloat64()` method, which returns the `float64` of the known value or `0.0` if null or unknown
71+
```
72+
73+
```release-note:enhancement
74+
types: Added `Int64` type `ValueInt64()` method, which returns the `int64` of the known value or `0` if null or unknown
75+
```
76+
77+
```release-note:enhancement
78+
types: Added `List` type `Elements()` method, which returns the `[]attr.Value` of the known values or `nil` if null or unknown
79+
```
80+
81+
```release-note:enhancement
82+
types: Added `Map` type `Elements()` method, which returns the `map[string]attr.Value` of the known values or `nil` if null or unknown
83+
```
84+
85+
```release-note:enhancement
86+
types: Added `Number` type `ValueBigFloat()` method, which returns the `*big.Float` of the known value or `nil` if null or unknown
87+
```
88+
89+
```release-note:enhancement
90+
types: Added `Set` type `Elements()` method, which returns the `[]attr.Value` of the known values or `nil` if null or unknown
91+
```
92+
93+
```release-note:enhancement
94+
types: Added `String` type `ValueString()` method, which returns the `string` of the known value or `""` if null or unknown
95+
```

internal/fwserver/attr_value.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,16 @@ func listElemObject(ctx context.Context, schemaPath path.Path, list types.List,
6868
return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, tftypes.UnknownValue)
6969
}
7070

71-
if index >= len(list.Elems) {
71+
if index >= len(list.Elements()) {
7272
return listElemObjectFromTerraformValue(ctx, schemaPath, list, description, nil)
7373
}
7474

75-
return coerceObjectValue(schemaPath, list.Elems[index])
75+
return coerceObjectValue(schemaPath, list.Elements()[index])
7676
}
7777

7878
func listElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, list types.List, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) {
79-
elemValue, err := list.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(list.ElemType.TerraformType(ctx), tfValue))
79+
elemType := list.ElementType(ctx)
80+
elemValue, err := elemType.ValueFromTerraform(ctx, tftypes.NewValue(elemType.TerraformType(ctx), tfValue))
8081

8182
if err != nil {
8283
return types.Object{Null: true}, diag.Diagnostics{
@@ -96,7 +97,7 @@ func mapElemObject(ctx context.Context, schemaPath path.Path, m types.Map, key s
9697
return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, tftypes.UnknownValue)
9798
}
9899

99-
elemValue, ok := m.Elems[key]
100+
elemValue, ok := m.Elements()[key]
100101

101102
if !ok {
102103
return mapElemObjectFromTerraformValue(ctx, schemaPath, m, description, nil)
@@ -106,7 +107,8 @@ func mapElemObject(ctx context.Context, schemaPath path.Path, m types.Map, key s
106107
}
107108

108109
func mapElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, m types.Map, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) {
109-
elemValue, err := m.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(m.ElemType.TerraformType(ctx), tfValue))
110+
elemType := m.ElementType(ctx)
111+
elemValue, err := elemType.ValueFromTerraform(ctx, tftypes.NewValue(elemType.TerraformType(ctx), tfValue))
110112

111113
if err != nil {
112114
return types.Object{Null: true}, diag.Diagnostics{
@@ -128,13 +130,13 @@ func objectAttributeValue(ctx context.Context, object types.Object, attributeNam
128130

129131
// A panic here indicates a bug somewhere else in the framework or an
130132
// invalid test case.
131-
return object.Attrs[attributeName], nil
133+
return object.Attributes()[attributeName], nil
132134
}
133135

134136
func objectAttributeValueFromTerraformValue(ctx context.Context, object types.Object, attributeName string, description fwschemadata.DataDescription, tfValue any) (attr.Value, diag.Diagnostics) {
135137
// A panic here indicates a bug somewhere else in the framework or an
136138
// invalid test case.
137-
attrType := object.AttrTypes[attributeName]
139+
attrType := object.AttributeTypes(ctx)[attributeName]
138140

139141
elemValue, err := attrType.ValueFromTerraform(ctx, tftypes.NewValue(attrType.TerraformType(ctx), tfValue))
140142

@@ -156,15 +158,16 @@ func setElemObject(ctx context.Context, schemaPath path.Path, set types.Set, ind
156158
return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, tftypes.UnknownValue)
157159
}
158160

159-
if index >= len(set.Elems) {
161+
if index >= len(set.Elements()) {
160162
return setElemObjectFromTerraformValue(ctx, schemaPath, set, description, nil)
161163
}
162164

163-
return coerceObjectValue(schemaPath, set.Elems[index])
165+
return coerceObjectValue(schemaPath, set.Elements()[index])
164166
}
165167

166168
func setElemObjectFromTerraformValue(ctx context.Context, schemaPath path.Path, set types.Set, description fwschemadata.DataDescription, tfValue any) (types.Object, diag.Diagnostics) {
167-
elemValue, err := set.ElemType.ValueFromTerraform(ctx, tftypes.NewValue(set.ElemType.TerraformType(ctx), tfValue))
169+
elemType := set.ElementType(ctx)
170+
elemValue, err := elemType.ValueFromTerraform(ctx, tftypes.NewValue(elemType.TerraformType(ctx), tfValue))
168171

169172
if err != nil {
170173
return types.Object{Null: true}, diag.Diagnostics{

internal/fwserver/attribute_plan_modification.go

+35-15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
1414
"github.com/hashicorp/terraform-plugin-framework/path"
1515
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
16+
"github.com/hashicorp/terraform-plugin-framework/types"
1617
)
1718

1819
type ModifyAttributePlanResponse struct {
@@ -85,6 +86,11 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
8586
return
8687
}
8788

89+
// Null and unknown values should not have nested schema to modify.
90+
if req.AttributePlan.IsNull() || req.AttributePlan.IsUnknown() {
91+
return
92+
}
93+
8894
if a.GetAttributes() == nil || len(a.GetAttributes().GetAttributes()) == 0 {
8995
return
9096
}
@@ -116,7 +122,9 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
116122
return
117123
}
118124

119-
for idx, planElem := range planList.Elems {
125+
planElements := planList.Elements()
126+
127+
for idx, planElem := range planElements {
120128
attrPath := req.AttributePath.AtListIndex(idx)
121129

122130
configObject, diags := listElemObject(ctx, attrPath, configList, idx, fwschemadata.DataDescriptionConfiguration)
@@ -143,6 +151,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
143151
return
144152
}
145153

154+
planAttributes := planObject.Attributes()
155+
146156
for name, attr := range a.GetAttributes().GetAttributes() {
147157
attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration)
148158

@@ -187,16 +197,16 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
187197

188198
AttributeModifyPlan(ctx, attr, attrReq, &attrResp)
189199

190-
planObject.Attrs[name] = attrResp.AttributePlan
200+
planAttributes[name] = attrResp.AttributePlan
191201
resp.Diagnostics.Append(attrResp.Diagnostics...)
192202
resp.RequiresReplace = attrResp.RequiresReplace
193203
resp.Private = attrResp.Private
194204
}
195205

196-
planList.Elems[idx] = planObject
206+
planElements[idx] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes)
197207
}
198208

199-
resp.AttributePlan = planList
209+
resp.AttributePlan = types.ListValue(planList.ElementType(ctx), planElements)
200210
case fwschema.NestingModeSet:
201211
configSet, diags := coerceSetValue(req.AttributePath, req.AttributeConfig)
202212

@@ -222,7 +232,9 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
222232
return
223233
}
224234

225-
for idx, planElem := range planSet.Elems {
235+
planElements := planSet.Elements()
236+
237+
for idx, planElem := range planElements {
226238
attrPath := req.AttributePath.AtSetValue(planElem)
227239

228240
configObject, diags := setElemObject(ctx, attrPath, configSet, idx, fwschemadata.DataDescriptionConfiguration)
@@ -249,6 +261,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
249261
return
250262
}
251263

264+
planAttributes := planObject.Attributes()
265+
252266
for name, attr := range a.GetAttributes().GetAttributes() {
253267
attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration)
254268

@@ -293,16 +307,16 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
293307

294308
AttributeModifyPlan(ctx, attr, attrReq, &attrResp)
295309

296-
planObject.Attrs[name] = attrResp.AttributePlan
310+
planAttributes[name] = attrResp.AttributePlan
297311
resp.Diagnostics.Append(attrResp.Diagnostics...)
298312
resp.RequiresReplace = attrResp.RequiresReplace
299313
resp.Private = attrResp.Private
300314
}
301315

302-
planSet.Elems[idx] = planObject
316+
planElements[idx] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes)
303317
}
304318

305-
resp.AttributePlan = planSet
319+
resp.AttributePlan = types.SetValue(planSet.ElementType(ctx), planElements)
306320
case fwschema.NestingModeMap:
307321
configMap, diags := coerceMapValue(req.AttributePath, req.AttributeConfig)
308322

@@ -328,7 +342,9 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
328342
return
329343
}
330344

331-
for key, planElem := range planMap.Elems {
345+
planElements := planMap.Elements()
346+
347+
for key, planElem := range planElements {
332348
attrPath := req.AttributePath.AtMapKey(key)
333349

334350
configObject, diags := mapElemObject(ctx, attrPath, configMap, key, fwschemadata.DataDescriptionConfiguration)
@@ -355,6 +371,8 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
355371
return
356372
}
357373

374+
planAttributes := planObject.Attributes()
375+
358376
for name, attr := range a.GetAttributes().GetAttributes() {
359377
attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration)
360378

@@ -399,16 +417,16 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
399417

400418
AttributeModifyPlan(ctx, attr, attrReq, &attrResp)
401419

402-
planObject.Attrs[name] = attrResp.AttributePlan
420+
planAttributes[name] = attrResp.AttributePlan
403421
resp.Diagnostics.Append(attrResp.Diagnostics...)
404422
resp.RequiresReplace = attrResp.RequiresReplace
405423
resp.Private = attrResp.Private
406424
}
407425

408-
planMap.Elems[key] = planObject
426+
planElements[key] = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes)
409427
}
410428

411-
resp.AttributePlan = planMap
429+
resp.AttributePlan = types.MapValue(planMap.ElementType(ctx), planElements)
412430
case fwschema.NestingModeSingle:
413431
configObject, diags := coerceObjectValue(req.AttributePath, req.AttributeConfig)
414432

@@ -434,10 +452,12 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
434452
return
435453
}
436454

437-
if len(planObject.Attrs) == 0 {
455+
if len(planObject.Attributes()) == 0 {
438456
return
439457
}
440458

459+
planAttributes := planObject.Attributes()
460+
441461
for name, attr := range a.GetAttributes().GetAttributes() {
442462
attrConfig, diags := objectAttributeValue(ctx, configObject, name, fwschemadata.DataDescriptionConfiguration)
443463

@@ -482,13 +502,13 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req tfsdk.Mo
482502

483503
AttributeModifyPlan(ctx, attr, attrReq, &attrResp)
484504

485-
planObject.Attrs[name] = attrResp.AttributePlan
505+
planAttributes[name] = attrResp.AttributePlan
486506
resp.Diagnostics.Append(attrResp.Diagnostics...)
487507
resp.RequiresReplace = attrResp.RequiresReplace
488508
resp.Private = attrResp.Private
489509
}
490510

491-
resp.AttributePlan = planObject
511+
resp.AttributePlan = types.ObjectValue(planObject.AttributeTypes(ctx), planAttributes)
492512
default:
493513
err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath)
494514
resp.Diagnostics.AddAttributeError(

0 commit comments

Comments
 (0)