Skip to content

Commit acffdf8

Browse files
authored
Add support for typed maps in autogen (#3833)
1 parent baf6e28 commit acffdf8

File tree

23 files changed

+666
-108
lines changed

23 files changed

+666
-108
lines changed

internal/common/autogen/customtypes/list.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ type ListValueInterface interface {
113113
Elements() []attr.Value
114114
}
115115

116+
func (v ListValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
117+
if v.ListValue.ElementType(ctx) == nil {
118+
// ListValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
119+
v.ListValue = NewListValueNull[T](ctx).ListValue
120+
}
121+
return v.ListValue.ToTerraformValue(ctx)
122+
}
123+
116124
func (v ListValue[T]) NewListValue(ctx context.Context, value []attr.Value) ListValueInterface {
117125
return NewListValue[T](ctx, value)
118126
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package customtypes
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
)
12+
13+
/*
14+
Custom Map type used in auto-generated code to enable the generic marshal/unmarshal operations to access the elements' type during conversion.
15+
Custom types docs: https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/custom
16+
17+
Usage:
18+
- Schema definition:
19+
"sample_string_map": schema.MapAttribute{
20+
...
21+
CustomType: customtypes.NewMapType[basetypes.StringValue](ctx),
22+
ElementType: types.StringType,
23+
}
24+
25+
- TF Models:
26+
type TFModel struct {
27+
SampleStringMap customtypes.MapValue[basetypes.StringValue] `tfsdk:"sample_string_map"`
28+
...
29+
}
30+
*/
31+
32+
var (
33+
_ basetypes.MapTypable = MapType[basetypes.StringValue]{}
34+
_ basetypes.MapValuable = MapValue[basetypes.StringValue]{}
35+
_ MapValueInterface = MapValue[basetypes.StringValue]{}
36+
)
37+
38+
type MapType[T attr.Value] struct {
39+
basetypes.MapType
40+
}
41+
42+
func NewMapType[T attr.Value](ctx context.Context) MapType[T] {
43+
elemType := getElemType[T](ctx)
44+
return MapType[T]{
45+
MapType: basetypes.MapType{ElemType: elemType},
46+
}
47+
}
48+
49+
func (t MapType[T]) Equal(o attr.Type) bool {
50+
other, ok := o.(MapType[T])
51+
if !ok {
52+
return false
53+
}
54+
55+
return t.MapType.Equal(other.MapType)
56+
}
57+
58+
func (MapType[T]) String() string {
59+
var t T
60+
return fmt.Sprintf("MapType[%T]", t)
61+
}
62+
63+
func (t MapType[T]) ValueFromMap(ctx context.Context, in basetypes.MapValue) (basetypes.MapValuable, diag.Diagnostics) {
64+
if in.IsNull() {
65+
return NewMapValueNull[T](ctx), nil
66+
}
67+
68+
if in.IsUnknown() {
69+
return NewMapValueUnknown[T](ctx), nil
70+
}
71+
72+
elemType := getElemType[T](ctx)
73+
baseMapValue, diags := basetypes.NewMapValue(elemType, in.Elements())
74+
if diags.HasError() {
75+
return nil, diags
76+
}
77+
78+
return MapValue[T]{MapValue: baseMapValue}, nil
79+
}
80+
81+
func (t MapType[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
82+
attrValue, err := t.MapType.ValueFromTerraform(ctx, in)
83+
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
mapValue, ok := attrValue.(basetypes.MapValue)
89+
if !ok {
90+
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
91+
}
92+
93+
mapValuable, diags := t.ValueFromMap(ctx, mapValue)
94+
if diags.HasError() {
95+
return nil, fmt.Errorf("unexpected error converting MapValue to MapValuable: %v", diags)
96+
}
97+
98+
return mapValuable, nil
99+
}
100+
101+
func (t MapType[T]) ValueType(_ context.Context) attr.Value {
102+
return MapValue[T]{}
103+
}
104+
105+
type MapValue[T attr.Value] struct {
106+
basetypes.MapValue
107+
}
108+
109+
type MapValueInterface interface {
110+
basetypes.MapValuable
111+
NewMapValue(ctx context.Context, value map[string]attr.Value) MapValueInterface
112+
NewMapValueNull(ctx context.Context) MapValueInterface
113+
ElementType(ctx context.Context) attr.Type
114+
Elements() map[string]attr.Value
115+
}
116+
117+
func (v MapValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
118+
if v.MapValue.ElementType(ctx) == nil {
119+
// MapValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
120+
v.MapValue = NewMapValueNull[T](ctx).MapValue
121+
}
122+
return v.MapValue.ToTerraformValue(ctx)
123+
}
124+
125+
func (v MapValue[T]) NewMapValue(ctx context.Context, value map[string]attr.Value) MapValueInterface {
126+
return NewMapValue[T](ctx, value)
127+
}
128+
129+
func NewMapValue[T attr.Value](ctx context.Context, value map[string]attr.Value) MapValue[T] {
130+
elemType := getElemType[T](ctx)
131+
132+
mapValue, diags := basetypes.NewMapValue(elemType, value)
133+
if diags.HasError() {
134+
return NewMapValueUnknown[T](ctx)
135+
}
136+
137+
return MapValue[T]{MapValue: mapValue}
138+
}
139+
140+
func (v MapValue[T]) NewMapValueNull(ctx context.Context) MapValueInterface {
141+
return NewMapValueNull[T](ctx)
142+
}
143+
144+
func NewMapValueNull[T attr.Value](ctx context.Context) MapValue[T] {
145+
elemType := getElemType[T](ctx)
146+
return MapValue[T]{MapValue: basetypes.NewMapNull(elemType)}
147+
}
148+
149+
func NewMapValueUnknown[T attr.Value](ctx context.Context) MapValue[T] {
150+
elemType := getElemType[T](ctx)
151+
return MapValue[T]{MapValue: basetypes.NewMapUnknown(elemType)}
152+
}
153+
154+
func (v MapValue[T]) Equal(o attr.Value) bool {
155+
other, ok := o.(MapValue[T])
156+
if !ok {
157+
return false
158+
}
159+
return v.MapValue.Equal(other.MapValue)
160+
}
161+
162+
func (v MapValue[T]) Type(ctx context.Context) attr.Type {
163+
return NewMapType[T](ctx)
164+
}
165+
166+
func (v MapValue[T]) ElementType(ctx context.Context) attr.Type {
167+
return getElemType[T](ctx)
168+
}
169+
170+
func (v MapValue[T]) Elements() map[string]attr.Value {
171+
return v.MapValue.Elements()
172+
}

internal/common/autogen/customtypes/nested_list.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ type NestedListValueInterface interface {
133133
Len() int
134134
}
135135

136+
func (v NestedListValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
137+
if v.ElementType(ctx) == nil {
138+
// NestedListValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
139+
v.ListValue = NewNestedListValueNull[T](ctx).ListValue
140+
}
141+
return v.ListValue.ToTerraformValue(ctx)
142+
}
143+
136144
func (v NestedListValue[T]) NewNestedListValue(ctx context.Context, value any) NestedListValueInterface {
137145
return NewNestedListValue[T](ctx, value)
138146
}

internal/common/autogen/customtypes/nested_map.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ type NestedMapValueInterface interface {
131131
NewEmptyMapPtr() any
132132
}
133133

134+
func (v NestedMapValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
135+
if v.ElementType(ctx) == nil {
136+
// NestedMapValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
137+
v.MapValue = NewNestedMapValueNull[T](ctx).MapValue
138+
}
139+
return v.MapValue.ToTerraformValue(ctx)
140+
}
141+
134142
func (v NestedMapValue[T]) NewNestedMapValue(ctx context.Context, value any) NestedMapValueInterface {
135143
return NewNestedMapValue[T](ctx, value)
136144
}

internal/common/autogen/customtypes/nested_set.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ type NestedSetValueInterface interface {
132132
Len() int
133133
}
134134

135+
func (v NestedSetValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
136+
if v.ElementType(ctx) == nil {
137+
// NestedSetValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
138+
v.SetValue = NewNestedSetValueNull[T](ctx).SetValue
139+
}
140+
return v.SetValue.ToTerraformValue(ctx)
141+
}
142+
135143
func (v NestedSetValue[T]) NewNestedSetValue(ctx context.Context, value any) NestedSetValueInterface {
136144
return NewNestedSetValue[T](ctx, value)
137145
}

internal/common/autogen/customtypes/object.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ type ObjectValueInterface interface {
129129
ValuePtrAsAny(ctx context.Context) (any, diag.Diagnostics)
130130
}
131131

132+
func (v ObjectValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
133+
if v.ObjectValue.Equal(basetypes.ObjectValue{}) {
134+
// ObjectValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
135+
v.ObjectValue = NewObjectValueNull[T](ctx).ObjectValue
136+
}
137+
return v.ObjectValue.ToTerraformValue(ctx)
138+
}
139+
132140
func (v ObjectValue[T]) NewObjectValue(ctx context.Context, value any) ObjectValueInterface {
133141
return NewObjectValue[T](ctx, value)
134142
}

internal/common/autogen/customtypes/set.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ type SetValueInterface interface {
113113
Elements() []attr.Value
114114
}
115115

116+
func (v SetValue[T]) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
117+
if v.SetValue.ElementType(ctx) == nil {
118+
// SetValue created as a zero value (not explicitly initialized), initialize now so conversion does not panic.
119+
v.SetValue = NewSetValueNull[T](ctx).SetValue
120+
}
121+
return v.SetValue.ToTerraformValue(ctx)
122+
}
123+
116124
func (v SetValue[T]) NewSetValue(ctx context.Context, value []attr.Value) SetValueInterface {
117125
return NewSetValue[T](ctx, value)
118126
}

internal/common/autogen/marshal.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ func getModelAttr(val attr.Value, isUpdate bool) (any, error) {
106106
return getMapAttr(v.Attributes(), false, isUpdate)
107107
case types.Map:
108108
return getMapAttr(v.Elements(), true, isUpdate)
109+
case customtypes.MapValueInterface:
110+
return getMapAttr(v.Elements(), true, isUpdate)
109111
case types.List:
110112
return getListAttr(v.Elements(), isUpdate)
111113
case customtypes.ListValueInterface:

internal/common/autogen/marshal_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func TestMarshalNestedAllTypes(t *testing.T) {
130130
AttrCustomSet customtypes.SetValue[types.String] `tfsdk:"attr_custom_set"`
131131
AttrSetObj types.Set `tfsdk:"attr_set_obj"`
132132
AttrMapSimple types.Map `tfsdk:"attr_map_simple"`
133+
AttrCustomMap customtypes.MapValue[types.String] `tfsdk:"attr_custom_map"`
133134
AttrMapObj types.Map `tfsdk:"attr_map_obj"`
134135
}{
135136
AttrString: types.StringValue("val"),
@@ -143,6 +144,10 @@ func TestMarshalNestedAllTypes(t *testing.T) {
143144
"keyOne": types.StringValue("val1"),
144145
"KeyTwo": types.StringValue("val2"), // don't change the key case when it's a map
145146
}),
147+
AttrCustomMap: customtypes.NewMapValue[types.String](t.Context(), map[string]attr.Value{
148+
"keyOne": types.StringValue("val1"),
149+
"KeyTwo": types.StringValue("val2"),
150+
}),
146151
AttrMapObj: attrMapObj,
147152
}
148153
const expectedJSON = `
@@ -164,6 +169,10 @@ func TestMarshalNestedAllTypes(t *testing.T) {
164169
"keyOne": "val1",
165170
"KeyTwo": "val2"
166171
},
172+
"attrCustomMap": {
173+
"keyOne": "val1",
174+
"KeyTwo": "val2"
175+
},
167176
"attrMapObj": {
168177
"keyOne": { "attrString": "str1", "attrInt": 1 },
169178
"KeyTwo": { "attrString": "str2", "attrInt": 2 }
@@ -268,6 +277,8 @@ func TestMarshalUpdateNull(t *testing.T) {
268277
AttrCustomList customtypes.ListValue[types.String] `tfsdk:"attr_custom_list"`
269278
AttrSet types.Set `tfsdk:"attr_set"`
270279
AttrCustomSet customtypes.SetValue[types.String] `tfsdk:"attr_custom_set"`
280+
AttrMap types.Map `tfsdk:"attr_map"`
281+
AttrCustomMap customtypes.MapValue[types.String] `tfsdk:"attr_custom_map"`
271282
AttrString types.String `tfsdk:"attr_string"`
272283
AttrObj types.Object `tfsdk:"attr_obj"`
273284
AttrIncludeString types.String `tfsdk:"attr_include_update" autogen:"includenullonupdate"`
@@ -277,6 +288,8 @@ func TestMarshalUpdateNull(t *testing.T) {
277288
AttrCustomList: customtypes.NewListValueNull[types.String](t.Context()),
278289
AttrSet: types.SetNull(types.StringType),
279290
AttrCustomSet: customtypes.NewSetValueNull[types.String](t.Context()),
291+
AttrMap: types.MapNull(types.StringType),
292+
AttrCustomMap: customtypes.NewMapValueNull[types.String](t.Context()),
280293
AttrString: types.StringNull(),
281294
AttrObj: types.ObjectNull(objTypeTest.AttrTypes),
282295
AttrIncludeString: types.StringNull(),

internal/common/autogen/unknown.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ func prepareAttr(value attr.Value) (attr.Value, error) {
9898
}
9999
// If known, no need to process each set item since unmarshal does not generate unknown attributes.
100100
return v, nil
101+
case customtypes.MapValueInterface:
102+
if v.IsUnknown() {
103+
return v.NewMapValueNull(ctx), nil
104+
}
105+
// If known, no need to process each map entry since unmarshal does not generate unknown attributes.
106+
return v, nil
101107
case customtypes.NestedMapValueInterface:
102108
if v.IsUnknown() {
103109
return v.NewNestedMapValueNull(ctx), nil

0 commit comments

Comments
 (0)