Skip to content

Commit 45286b2

Browse files
committed
Add support for typed lists in autogen
1 parent 14f40db commit 45286b2

File tree

18 files changed

+388
-104
lines changed

18 files changed

+388
-104
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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 List 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_list": schema.ListAttribute{
20+
...
21+
CustomType: customtypes.NewListType[basetypes.StringValue](ctx),
22+
ElementType: types.StringType,
23+
}
24+
25+
- TF Models:
26+
type TFModel struct {
27+
SampleStringList customtypes.ListValue[basetypes.StringValue] `tfsdk:"sample_string_list"`
28+
...
29+
}
30+
*/
31+
32+
var (
33+
_ basetypes.ListTypable = ListType[basetypes.StringValue]{}
34+
_ basetypes.ListValuable = ListValue[basetypes.StringValue]{}
35+
_ ListValueInterface = ListValue[basetypes.StringValue]{}
36+
)
37+
38+
type ListType[T attr.Value] struct {
39+
basetypes.ListType
40+
}
41+
42+
func NewListType[T attr.Value](ctx context.Context) ListType[T] {
43+
elemType := getElemType[T](ctx)
44+
return ListType[T]{
45+
ListType: basetypes.ListType{ElemType: elemType},
46+
}
47+
}
48+
49+
func (t ListType[T]) Equal(o attr.Type) bool {
50+
other, ok := o.(ListType[T])
51+
if !ok {
52+
return false
53+
}
54+
return t.ListType.Equal(other.ListType)
55+
}
56+
57+
func (ListType[T]) String() string {
58+
var t T
59+
return fmt.Sprintf("ListType[%T]", t)
60+
}
61+
62+
func (t ListType[T]) ValueFromList(ctx context.Context, in basetypes.ListValue) (basetypes.ListValuable, diag.Diagnostics) {
63+
if in.IsNull() {
64+
return NewListValueNull[T](ctx), nil
65+
}
66+
67+
if in.IsUnknown() {
68+
return NewListValueUnknown[T](ctx), nil
69+
}
70+
71+
elemType := getElemType[T](ctx)
72+
baseListValue, diags := basetypes.NewListValue(elemType, in.Elements())
73+
if diags.HasError() {
74+
return nil, diags
75+
}
76+
77+
return ListValue[T]{ListValue: baseListValue}, nil
78+
}
79+
80+
func (t ListType[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
81+
attrValue, err := t.ListType.ValueFromTerraform(ctx, in)
82+
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
listValue, ok := attrValue.(basetypes.ListValue)
88+
if !ok {
89+
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
90+
}
91+
92+
listValuable, diags := t.ValueFromList(ctx, listValue)
93+
if diags.HasError() {
94+
return nil, fmt.Errorf("unexpected error converting ListValue to ListValuable: %v", diags)
95+
}
96+
97+
return listValuable, nil
98+
}
99+
100+
func (t ListType[T]) ValueType(_ context.Context) attr.Value {
101+
return ListValue[T]{}
102+
}
103+
104+
type ListValue[T attr.Value] struct {
105+
basetypes.ListValue
106+
}
107+
108+
type ListValueInterface interface {
109+
basetypes.ListValuable
110+
NewListValue(ctx context.Context, value []attr.Value) ListValueInterface
111+
NewListValueNull(ctx context.Context) ListValueInterface
112+
ElementType(ctx context.Context) attr.Type
113+
Elements() []attr.Value
114+
}
115+
116+
func (v ListValue[T]) NewListValue(ctx context.Context, value []attr.Value) ListValueInterface {
117+
return NewListValue[T](ctx, value)
118+
}
119+
120+
func NewListValue[T attr.Value](ctx context.Context, value []attr.Value) ListValue[T] {
121+
elemType := getElemType[T](ctx)
122+
123+
listValue, diags := basetypes.NewListValue(elemType, value)
124+
if diags.HasError() {
125+
return NewListValueUnknown[T](ctx)
126+
}
127+
128+
return ListValue[T]{ListValue: listValue}
129+
}
130+
131+
func (v ListValue[T]) NewListValueNull(ctx context.Context) ListValueInterface {
132+
return NewListValueNull[T](ctx)
133+
}
134+
135+
func NewListValueNull[T attr.Value](ctx context.Context) ListValue[T] {
136+
elemType := getElemType[T](ctx)
137+
return ListValue[T]{ListValue: basetypes.NewListNull(elemType)}
138+
}
139+
140+
func NewListValueUnknown[T attr.Value](ctx context.Context) ListValue[T] {
141+
elemType := getElemType[T](ctx)
142+
return ListValue[T]{ListValue: basetypes.NewListUnknown(elemType)}
143+
}
144+
145+
func (v ListValue[T]) Equal(o attr.Value) bool {
146+
other, ok := o.(ListValue[T])
147+
if !ok {
148+
return false
149+
}
150+
return v.ListValue.Equal(other.ListValue)
151+
}
152+
153+
func (v ListValue[T]) Type(ctx context.Context) attr.Type {
154+
return NewListType[T](ctx)
155+
}
156+
157+
func (v ListValue[T]) ElementType(ctx context.Context) attr.Type {
158+
return getElemType[T](ctx)
159+
}
160+
161+
func (v ListValue[T]) Elements() []attr.Value {
162+
return v.ListValue.Elements()
163+
}
164+
165+
func getElemType[T attr.Value](ctx context.Context) attr.Type {
166+
var t T
167+
return t.Type(ctx)
168+
}

internal/common/autogen/marshal.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func marshalAttr(attrNameModel string, attrValModel reflect.Value, objJSON map[s
7777

7878
if val == nil && isUpdate {
7979
switch obj.(type) {
80-
case types.List, types.Set, customtypes.NestedListValueInterface:
80+
case types.List, types.Set, customtypes.ListValueInterface, customtypes.NestedListValueInterface:
8181
val = []any{} // Send an empty array if it's a null root list or set
8282
}
8383
}
@@ -107,6 +107,8 @@ func getModelAttr(val attr.Value, isUpdate bool) (any, error) {
107107
return getMapAttr(v.Elements(), true, isUpdate)
108108
case types.List:
109109
return getListAttr(v.Elements(), isUpdate)
110+
case customtypes.ListValueInterface:
111+
return getListAttr(v.Elements(), isUpdate)
110112
case types.Set:
111113
return getListAttr(v.Elements(), isUpdate)
112114
case jsontypes.Normalized:

internal/common/autogen/marshal_test.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,18 @@ func TestMarshalNestedAllTypes(t *testing.T) {
122122
})
123123
assert.False(t, diags.HasError())
124124
model := struct {
125-
AttrString types.String `tfsdk:"attr_string"`
126-
AttrListSimple types.List `tfsdk:"attr_list_simple"`
127-
AttrListObj types.List `tfsdk:"attr_list_obj"`
128-
AttrSetSimple types.Set `tfsdk:"attr_set_simple"`
129-
AttrSetObj types.Set `tfsdk:"attr_set_obj"`
130-
AttrMapSimple types.Map `tfsdk:"attr_map_simple"`
131-
AttrMapObj types.Map `tfsdk:"attr_map_obj"`
125+
AttrString types.String `tfsdk:"attr_string"`
126+
AttrListSimple types.List `tfsdk:"attr_list_simple"`
127+
AttrCustomList customtypes.ListValue[types.String] `tfsdk:"attr_custom_list"`
128+
AttrListObj types.List `tfsdk:"attr_list_obj"`
129+
AttrSetSimple types.Set `tfsdk:"attr_set_simple"`
130+
AttrSetObj types.Set `tfsdk:"attr_set_obj"`
131+
AttrMapSimple types.Map `tfsdk:"attr_map_simple"`
132+
AttrMapObj types.Map `tfsdk:"attr_map_obj"`
132133
}{
133134
AttrString: types.StringValue("val"),
134135
AttrListSimple: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("val1"), types.StringValue("val2")}),
136+
AttrCustomList: customtypes.NewListValue[types.String](t.Context(), []attr.Value{types.StringValue("val1"), types.StringValue("val2")}),
135137
AttrListObj: attrListObj,
136138
AttrSetSimple: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("val11"), types.StringValue("val22")}),
137139
AttrSetObj: attrSetObj,
@@ -145,6 +147,7 @@ func TestMarshalNestedAllTypes(t *testing.T) {
145147
{
146148
"attrString": "val",
147149
"attrListSimple": ["val1", "val2"],
150+
"attrCustomList": ["val1", "val2"],
148151
"attrListObj": [
149152
{ "attrString": "str1", "attrInt": 1 },
150153
{ "attrString": "str2", "attrInt": 2 }
@@ -258,14 +261,16 @@ func TestMarshalOmitJSONUpdate(t *testing.T) {
258261

259262
func TestMarshalUpdateNull(t *testing.T) {
260263
model := struct {
261-
AttrList types.List `tfsdk:"attr_list"`
262-
AttrSet types.Set `tfsdk:"attr_set"`
263-
AttrString types.String `tfsdk:"attr_string"`
264-
AttrObj types.Object `tfsdk:"attr_obj"`
265-
AttrIncludeString types.String `tfsdk:"attr_include_update" autogen:"includenullonupdate"`
266-
AttrIncludeObj types.Object `tfsdk:"attr_include_obj" autogen:"includenullonupdate"`
264+
AttrList types.List `tfsdk:"attr_list"`
265+
AttrCustomList customtypes.ListValue[types.String] `tfsdk:"attr_custom_list"`
266+
AttrSet types.Set `tfsdk:"attr_set"`
267+
AttrString types.String `tfsdk:"attr_string"`
268+
AttrObj types.Object `tfsdk:"attr_obj"`
269+
AttrIncludeString types.String `tfsdk:"attr_include_update" autogen:"includenullonupdate"`
270+
AttrIncludeObj types.Object `tfsdk:"attr_include_obj" autogen:"includenullonupdate"`
267271
}{
268272
AttrList: types.ListNull(types.StringType),
273+
AttrCustomList: customtypes.NewListValueNull[types.String](t.Context()),
269274
AttrSet: types.SetNull(types.StringType),
270275
AttrString: types.StringNull(),
271276
AttrObj: types.ObjectNull(objTypeTest.AttrTypes),
@@ -277,6 +282,7 @@ func TestMarshalUpdateNull(t *testing.T) {
277282
const expectedJSON = `
278283
{
279284
"attrList": [],
285+
"attrCustomList": [],
280286
"attrSet": [],
281287
"attrIncludeString": null,
282288
"attrIncludeObj": null

internal/common/autogen/unknown.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ func prepareAttr(value attr.Value) (attr.Value, error) {
6060

6161
objNew := v.NewObjectValue(ctx, valuePtr)
6262
return objNew, nil
63+
case customtypes.ListValueInterface:
64+
if v.IsUnknown() {
65+
return v.NewListValueNull(ctx), nil
66+
}
67+
// If known, no need to process each list item since unmarshal does not generate unknown attributes.
68+
return v, nil
6369
case customtypes.NestedListValueInterface:
6470
if v.IsUnknown() {
6571
return v.NewNestedListValueNull(ctx), nil

internal/common/autogen/unknown_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func TestResolveUnknowns(t *testing.T) {
3434
AttrMapUnknown types.Map `tfsdk:"attr_map_unknown"`
3535
AttrCustomObjectUnknown customtypes.ObjectValue[modelEmptyTest] `tfsdk:"attr_custom_object_unknown"`
3636
AttrCustomObject customtypes.ObjectValue[modelCustomTypeTest] `tfsdk:"attr_custom_object"`
37+
AttrCustomListUnknown customtypes.ListValue[types.String] `tfsdk:"attr_custom_list_string"`
3738
AttrCustomNestedListUnknown customtypes.NestedListValue[modelEmptyTest] `tfsdk:"attr_custom_nested_list_unknown"`
3839
}
3940

@@ -88,6 +89,7 @@ func TestResolveUnknowns(t *testing.T) {
8889
AttrMANYUpper: types.Int64Unknown(),
8990
}),
9091
AttrCustomNestedListUnknown: customtypes.NewNestedListValueUnknown[modelEmptyTest](ctx),
92+
AttrCustomListUnknown: customtypes.NewListValueUnknown[types.String](ctx),
9193
}
9294
modelExpected := modelst{
9395
AttrStringUnknown: types.StringNull(),
@@ -139,6 +141,7 @@ func TestResolveUnknowns(t *testing.T) {
139141
AttrUnknownObject: customtypes.NewObjectValueNull[modelEmptyTest](ctx),
140142
AttrMANYUpper: types.Int64Null(),
141143
}),
144+
AttrCustomListUnknown: customtypes.NewListValueNull[types.String](ctx),
142145
AttrCustomNestedListUnknown: customtypes.NewNestedListValueNull[modelEmptyTest](ctx),
143146
}
144147
require.NoError(t, autogen.ResolveUnknowns(&model))

internal/common/autogen/unmarshal.go

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -159,46 +159,18 @@ func getTfAttr(value any, valueType attr.Type, oldVal attr.Value, name string) (
159159
}
160160
return jsontypes.NewNormalizedValue(string(jsonBytes)), nil
161161
}
162-
if list, ok := oldVal.(types.List); ok {
163-
listNew, err := setListAttrModel(list, v, nameErr)
164-
if err != nil {
165-
return nil, err
166-
}
167-
return listNew, nil
168-
}
169-
if set, ok := oldVal.(types.Set); ok {
170-
setNew, err := setSetAttrModel(set, v, nameErr)
171-
if err != nil {
172-
return nil, err
173-
}
174-
return setNew, nil
175-
}
176-
if list, ok := oldVal.(customtypes.NestedListValueInterface); ok {
177-
if len(v) == 0 && list.Len() == 0 {
178-
// Keep current list if both model and JSON lists are zero-len (empty or null) so config is preserved.
179-
// It avoids inconsistent result after apply when user explicitly sets an empty list in config.
180-
return list, nil
181-
}
182-
183-
slicePtr := list.NewEmptySlicePtr()
184-
sliceVal := reflect.ValueOf(slicePtr).Elem()
185-
sliceVal.Set(reflect.MakeSlice(sliceVal.Type(), len(v), len(v)))
186-
187-
for i, item := range v {
188-
elementPtr := sliceVal.Index(i).Addr().Interface()
189-
objJSON, ok := item.(map[string]any)
190-
if !ok {
191-
return nil, fmt.Errorf("unmarshal of list item failed to convert object: %v", item)
192-
}
193-
err := unmarshalAttrs(objJSON, elementPtr)
194-
if err != nil {
195-
return nil, err
196-
}
197-
}
198162

199-
listNew := list.NewNestedListValue(context.Background(), slicePtr)
200-
return listNew, nil
163+
switch oldVal := oldVal.(type) {
164+
case types.List:
165+
return setListAttrModel(oldVal, v, nameErr)
166+
case types.Set:
167+
return setSetAttrModel(oldVal, v, nameErr)
168+
case customtypes.ListValueInterface:
169+
return getListValueTFAttr(context.Background(), v, oldVal, nameErr)
170+
case customtypes.NestedListValueInterface:
171+
return getNestedListValueTFAttr(context.Background(), v, oldVal)
201172
}
173+
202174
return nil, errUnmarshal(value, valueType, "Array", nameErr)
203175
case nil:
204176
return nil, nil // skip nil values, no need to set anything
@@ -331,3 +303,51 @@ func getObjAttrsAndTypes(obj types.Object) (mapAttrs map[string]attr.Value, mapT
331303
}
332304
return mapAttrs, mapTypes, nil
333305
}
306+
307+
func getListValueTFAttr(ctx context.Context, arrayJSON []any, list customtypes.ListValueInterface, nameErr string) (attr.Value, error) {
308+
if len(arrayJSON) == 0 && len(list.Elements()) == 0 {
309+
// Keep current list if both model and JSON lists are zero-len (empty or null) so config is preserved.
310+
// It avoids inconsistent result after apply when user explicitly sets an empty list in config.
311+
return list, nil
312+
}
313+
314+
elemType := list.ElementType(ctx)
315+
slice := make([]attr.Value, len(arrayJSON))
316+
for i, item := range arrayJSON {
317+
newValue, err := getTfAttr(item, elemType, nil, nameErr)
318+
if err != nil {
319+
return nil, err
320+
}
321+
slice[i] = newValue
322+
}
323+
324+
listNew := list.NewListValue(ctx, slice)
325+
return listNew, nil
326+
}
327+
328+
func getNestedListValueTFAttr(ctx context.Context, arrayJSON []any, list customtypes.NestedListValueInterface) (attr.Value, error) {
329+
if len(arrayJSON) == 0 && list.Len() == 0 {
330+
// Keep current list if both model and JSON lists are zero-len (empty or null) so config is preserved.
331+
// It avoids inconsistent result after apply when user explicitly sets an empty list in config.
332+
return list, nil
333+
}
334+
335+
slicePtr := list.NewEmptySlicePtr()
336+
sliceVal := reflect.ValueOf(slicePtr).Elem()
337+
sliceVal.Set(reflect.MakeSlice(sliceVal.Type(), len(arrayJSON), len(arrayJSON)))
338+
339+
for i, item := range arrayJSON {
340+
elementPtr := sliceVal.Index(i).Addr().Interface()
341+
objJSON, ok := item.(map[string]any)
342+
if !ok {
343+
return nil, fmt.Errorf("unmarshal of list item failed to convert object: %v", item)
344+
}
345+
err := unmarshalAttrs(objJSON, elementPtr)
346+
if err != nil {
347+
return nil, err
348+
}
349+
}
350+
351+
listNew := list.NewNestedListValue(ctx, slicePtr)
352+
return listNew, nil
353+
}

0 commit comments

Comments
 (0)