Skip to content

Commit f87e24a

Browse files
committed
Add support for typed nested lists in autogen
1 parent 21083a0 commit f87e24a

27 files changed

+1044
-549
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package customtype
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
)
13+
14+
/*
15+
Custom Nested List type used in auto-generated code to enable the generic marshal/unmarshal operations to access nested attribute struct tags during conversion.
16+
Custom types docs: https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/custom
17+
18+
Usage:
19+
- Schema definition:
20+
"sample_nested_object_list": schema.ListNestedAttribute{
21+
...
22+
CustomType: customtype.NewNestedListType[TFSampleNestedObjectModel](ctx),
23+
NestedObject: schema.NestedAttributeObject{
24+
Attributes: map[string]schema.Attribute{
25+
"string_attribute": schema.StringAttribute{...},
26+
},
27+
},
28+
}
29+
30+
- TF Models:
31+
type TFModel struct {
32+
SampleNestedObjectList customtype.NestedListValue[TFSampleNestedObjectModel] `tfsdk:"sample_nested_object_list"`
33+
...
34+
}
35+
36+
type TFSampleNestedObjectModel struct {
37+
StringAttribute types.String `tfsdk:"string_attribute"`
38+
...
39+
}
40+
*/
41+
42+
var (
43+
_ basetypes.ListTypable = NestedListType[struct{}]{}
44+
_ basetypes.ListValuable = NestedListValue[struct{}]{}
45+
_ NestedListValueInterface = NestedListValue[struct{}]{}
46+
)
47+
48+
type NestedListType[T any] struct {
49+
basetypes.ListType
50+
}
51+
52+
func NewNestedListType[T any](ctx context.Context) NestedListType[T] {
53+
elemType, diags := getElementType[T](ctx)
54+
if diags.HasError() {
55+
panic(fmt.Errorf("error creating NestedListType: %v", diags))
56+
}
57+
58+
result := NestedListType[T]{
59+
ListType: basetypes.ListType{ElemType: elemType},
60+
}
61+
return result
62+
}
63+
64+
func (t NestedListType[T]) Equal(o attr.Type) bool {
65+
other, ok := o.(NestedListType[T])
66+
if !ok {
67+
return false
68+
}
69+
return t.ListType.Equal(other.ListType)
70+
}
71+
72+
func (NestedListType[T]) String() string {
73+
var t T
74+
return fmt.Sprintf("NestedListType[%T]", t)
75+
}
76+
77+
func (t NestedListType[T]) ValueFromList(ctx context.Context, in basetypes.ListValue) (basetypes.ListValuable, diag.Diagnostics) {
78+
if in.IsNull() {
79+
return NewNestedListValueNull[T](ctx), nil
80+
}
81+
82+
if in.IsUnknown() {
83+
return NewNestedListValueUnknown[T](ctx), nil
84+
}
85+
86+
elemType, diags := getElementType[T](ctx)
87+
if diags.HasError() {
88+
return nil, diags
89+
}
90+
91+
baseListValue, diags := basetypes.NewListValue(elemType, in.Elements())
92+
if diags.HasError() {
93+
return nil, diags
94+
}
95+
96+
return NestedListValue[T]{ListValue: baseListValue}, nil
97+
}
98+
99+
func (t NestedListType[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
100+
attrValue, err := t.ListType.ValueFromTerraform(ctx, in)
101+
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
listValue, ok := attrValue.(basetypes.ListValue)
107+
if !ok {
108+
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
109+
}
110+
111+
listValuable, diags := t.ValueFromList(ctx, listValue)
112+
if diags.HasError() {
113+
return nil, fmt.Errorf("unexpected error converting ListValue to ListValuable: %v", diags)
114+
}
115+
116+
return listValuable, nil
117+
}
118+
119+
func (t NestedListType[T]) ValueType(_ context.Context) attr.Value {
120+
return NestedListValue[T]{}
121+
}
122+
123+
type NestedListValue[T any] struct {
124+
basetypes.ListValue
125+
}
126+
127+
type NestedListValueInterface interface {
128+
basetypes.ListValuable
129+
NewNestedListValue(ctx context.Context, value any) NestedListValueInterface
130+
NewNestedListValueNull(ctx context.Context) NestedListValueInterface
131+
SlicePtrAsAny(ctx context.Context) (any, diag.Diagnostics)
132+
NewEmptySlicePtr() any
133+
Len() int
134+
}
135+
136+
func (v NestedListValue[T]) NewNestedListValue(ctx context.Context, value any) NestedListValueInterface {
137+
return NewNestedListValue[T](ctx, value)
138+
}
139+
140+
func NewNestedListValue[T any](ctx context.Context, value any) NestedListValue[T] {
141+
elemType, diags := getElementType[T](ctx)
142+
if diags.HasError() {
143+
panic(fmt.Errorf("error creating NestedListValue: %v", diags))
144+
}
145+
146+
newValue, diags := basetypes.NewListValueFrom(ctx, elemType, value)
147+
if diags.HasError() {
148+
return NewNestedListValueUnknown[T](ctx)
149+
}
150+
151+
return NestedListValue[T]{ListValue: newValue}
152+
}
153+
154+
func (v NestedListValue[T]) NewNestedListValueNull(ctx context.Context) NestedListValueInterface {
155+
return NewNestedListValueNull[T](ctx)
156+
}
157+
158+
func NewNestedListValueNull[T any](ctx context.Context) NestedListValue[T] {
159+
elemType, diags := getElementType[T](ctx)
160+
if diags.HasError() {
161+
panic(fmt.Errorf("error creating null NestedListValue: %v", diags))
162+
}
163+
return NestedListValue[T]{ListValue: basetypes.NewListNull(elemType)}
164+
}
165+
166+
func NewNestedListValueUnknown[T any](ctx context.Context) NestedListValue[T] {
167+
elemType, diags := getElementType[T](ctx)
168+
if diags.HasError() {
169+
panic(fmt.Errorf("error creating unknown NestedListValue: %v", diags))
170+
}
171+
return NestedListValue[T]{ListValue: basetypes.NewListUnknown(elemType)}
172+
}
173+
174+
func (v NestedListValue[T]) Equal(o attr.Value) bool {
175+
other, ok := o.(NestedListValue[T])
176+
if !ok {
177+
return false
178+
}
179+
return v.ListValue.Equal(other.ListValue)
180+
}
181+
182+
func (v NestedListValue[T]) Type(ctx context.Context) attr.Type {
183+
return NewNestedListType[T](ctx)
184+
}
185+
186+
func (v NestedListValue[T]) SlicePtrAsAny(ctx context.Context) (any, diag.Diagnostics) {
187+
valuePtr := new([]T)
188+
189+
if v.IsNull() || v.IsUnknown() {
190+
return valuePtr, nil
191+
}
192+
193+
diags := v.ElementsAs(ctx, valuePtr, false)
194+
if diags.HasError() {
195+
return nil, diags
196+
}
197+
198+
return valuePtr, diags
199+
}
200+
201+
func (v NestedListValue[T]) NewEmptySlicePtr() any {
202+
return new([]T)
203+
}
204+
205+
func (v NestedListValue[T]) Len() int {
206+
return len(v.Elements())
207+
}
208+
209+
func getElementType[T any](ctx context.Context) (attr.Type, diag.Diagnostics) {
210+
var t T
211+
attrTypes, diags := valueToAttributeTypes(ctx, reflect.ValueOf(t))
212+
if diags.HasError() {
213+
return nil, diags
214+
}
215+
216+
return basetypes.ObjectType{AttrTypes: attrTypes}, nil
217+
}

internal/common/autogen/customtype/object.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ type ObjectType[T any] struct {
4848
}
4949

5050
func NewObjectType[T any](ctx context.Context) ObjectType[T] {
51-
result := ObjectType[T]{}
52-
5351
attrTypes, diags := getAttributeTypes[T](ctx)
5452
if diags.HasError() {
5553
panic(fmt.Errorf("error creating ObjectType: %v", diags))
5654
}
5755

58-
result.ObjectType = basetypes.ObjectType{AttrTypes: attrTypes}
56+
result := ObjectType[T]{
57+
ObjectType: basetypes.ObjectType{AttrTypes: attrTypes},
58+
}
5959
return result
6060
}
6161

@@ -124,9 +124,9 @@ type ObjectValue[T any] struct {
124124

125125
type ObjectValueInterface interface {
126126
basetypes.ObjectValuable
127-
ValuePtrAsAny(ctx context.Context) (any, diag.Diagnostics)
128127
NewObjectValue(ctx context.Context, value any) ObjectValueInterface
129128
NewObjectValueNull(ctx context.Context) ObjectValueInterface
129+
ValuePtrAsAny(ctx context.Context) (any, diag.Diagnostics)
130130
}
131131

132132
func (v ObjectValue[T]) NewObjectValue(ctx context.Context, value any) ObjectValueInterface {

internal/common/autogen/marshal.go

Lines changed: 23 additions & 2 deletions
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:
80+
case types.List, types.Set, customtype.NestedListValueInterface:
8181
val = []any{} // Send an empty array if it's a null root list or set
8282
}
8383
}
@@ -123,13 +123,34 @@ func getModelAttr(val attr.Value, isUpdate bool) (any, error) {
123123

124124
result, err := marshalAttrs(reflect.ValueOf(valuePtr).Elem(), isUpdate)
125125
return result, err
126+
case customtype.NestedListValueInterface:
127+
slicePtr, diags := v.SlicePtrAsAny(context.Background())
128+
if diags.HasError() {
129+
return nil, fmt.Errorf("marshal failed for type: %v", diags)
130+
}
131+
132+
sliceValue := reflect.ValueOf(slicePtr).Elem()
133+
length := sliceValue.Len()
134+
135+
result := make([]any, 0, length)
136+
for i := range length {
137+
value, err := marshalAttrs(sliceValue.Index(i), isUpdate)
138+
if err != nil {
139+
return nil, err
140+
}
141+
if value != nil {
142+
result = append(result, value)
143+
}
144+
}
145+
146+
return result, nil
126147
default:
127148
return nil, fmt.Errorf("marshal not supported yet for type %T", v)
128149
}
129150
}
130151

131152
func getListAttr(elms []attr.Value, isUpdate bool) (any, error) {
132-
arr := make([]any, 0)
153+
arr := make([]any, 0, len(elms))
133154
for _, attr := range elms {
134155
valChild, err := getModelAttr(attr, isUpdate)
135156
if err != nil {

0 commit comments

Comments
 (0)