Skip to content

Commit 78d1a9a

Browse files
authored
Add support for typed nested sets in autogen (#3819)
1 parent 979ca9c commit 78d1a9a

File tree

13 files changed

+491
-23
lines changed

13 files changed

+491
-23
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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 Nested Set type used in auto-generated code to enable the generic marshal/unmarshal operations to access nested attribute struct tags 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_nested_object_set": schema.SetNestedAttribute{
20+
...
21+
CustomType: customtypes.NewNestedSetType[TFSampleNestedObjectModel](ctx),
22+
NestedObject: schema.NestedAttributeObject{
23+
Attributes: map[string]schema.Attribute{
24+
"string_attribute": schema.StringAttribute{...},
25+
},
26+
},
27+
}
28+
29+
- TF Models:
30+
type TFModel struct {
31+
SampleNestedObjectSet customtypes.NestedSetValue[TFSampleNestedObjectModel] `tfsdk:"sample_nested_object_set"`
32+
...
33+
}
34+
35+
type TFSampleNestedObjectModel struct {
36+
StringAttribute types.String `tfsdk:"string_attribute"`
37+
...
38+
}
39+
*/
40+
41+
var (
42+
_ basetypes.SetTypable = NestedSetType[struct{}]{}
43+
_ basetypes.SetValuable = NestedSetValue[struct{}]{}
44+
_ NestedSetValueInterface = NestedSetValue[struct{}]{}
45+
)
46+
47+
type NestedSetType[T any] struct {
48+
basetypes.SetType
49+
}
50+
51+
func NewNestedSetType[T any](ctx context.Context) NestedSetType[T] {
52+
elemType, diags := getElementType[T](ctx)
53+
if diags.HasError() {
54+
panic(fmt.Errorf("error creating NestedSetType: %v", diags))
55+
}
56+
57+
result := NestedSetType[T]{
58+
SetType: basetypes.SetType{ElemType: elemType},
59+
}
60+
return result
61+
}
62+
63+
func (t NestedSetType[T]) Equal(o attr.Type) bool {
64+
other, ok := o.(NestedSetType[T])
65+
if !ok {
66+
return false
67+
}
68+
return t.SetType.Equal(other.SetType)
69+
}
70+
71+
func (NestedSetType[T]) String() string {
72+
var t T
73+
return fmt.Sprintf("NestedSetType[%T]", t)
74+
}
75+
76+
func (t NestedSetType[T]) ValueFromSet(ctx context.Context, in basetypes.SetValue) (basetypes.SetValuable, diag.Diagnostics) {
77+
if in.IsNull() {
78+
return NewNestedSetValueNull[T](ctx), nil
79+
}
80+
81+
if in.IsUnknown() {
82+
return NewNestedSetValueUnknown[T](ctx), nil
83+
}
84+
85+
elemType, diags := getElementType[T](ctx)
86+
if diags.HasError() {
87+
return nil, diags
88+
}
89+
90+
baseSetValue, diags := basetypes.NewSetValue(elemType, in.Elements())
91+
if diags.HasError() {
92+
return nil, diags
93+
}
94+
95+
return NestedSetValue[T]{SetValue: baseSetValue}, nil
96+
}
97+
98+
func (t NestedSetType[T]) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
99+
attrValue, err := t.SetType.ValueFromTerraform(ctx, in)
100+
101+
if err != nil {
102+
return nil, err
103+
}
104+
105+
setValue, ok := attrValue.(basetypes.SetValue)
106+
if !ok {
107+
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
108+
}
109+
110+
setValuable, diags := t.ValueFromSet(ctx, setValue)
111+
if diags.HasError() {
112+
return nil, fmt.Errorf("unexpected error converting SetValue to SetValuable: %v", diags)
113+
}
114+
115+
return setValuable, nil
116+
}
117+
118+
func (t NestedSetType[T]) ValueType(_ context.Context) attr.Value {
119+
return NestedSetValue[T]{}
120+
}
121+
122+
type NestedSetValue[T any] struct {
123+
basetypes.SetValue
124+
}
125+
126+
type NestedSetValueInterface interface {
127+
basetypes.SetValuable
128+
NewNestedSetValue(ctx context.Context, value any) NestedSetValueInterface
129+
NewNestedSetValueNull(ctx context.Context) NestedSetValueInterface
130+
SlicePtrAsAny(ctx context.Context) (any, diag.Diagnostics)
131+
NewEmptySlicePtr() any
132+
Len() int
133+
}
134+
135+
func (v NestedSetValue[T]) NewNestedSetValue(ctx context.Context, value any) NestedSetValueInterface {
136+
return NewNestedSetValue[T](ctx, value)
137+
}
138+
139+
func NewNestedSetValue[T any](ctx context.Context, value any) NestedSetValue[T] {
140+
elemType, diags := getElementType[T](ctx)
141+
if diags.HasError() {
142+
panic(fmt.Errorf("error creating NestedSetValue: %v", diags))
143+
}
144+
145+
newValue, diags := basetypes.NewSetValueFrom(ctx, elemType, value)
146+
if diags.HasError() {
147+
return NewNestedSetValueUnknown[T](ctx)
148+
}
149+
150+
return NestedSetValue[T]{SetValue: newValue}
151+
}
152+
153+
func (v NestedSetValue[T]) NewNestedSetValueNull(ctx context.Context) NestedSetValueInterface {
154+
return NewNestedSetValueNull[T](ctx)
155+
}
156+
157+
func NewNestedSetValueNull[T any](ctx context.Context) NestedSetValue[T] {
158+
elemType, diags := getElementType[T](ctx)
159+
if diags.HasError() {
160+
panic(fmt.Errorf("error creating null NestedSetValue: %v", diags))
161+
}
162+
return NestedSetValue[T]{SetValue: basetypes.NewSetNull(elemType)}
163+
}
164+
165+
func NewNestedSetValueUnknown[T any](ctx context.Context) NestedSetValue[T] {
166+
elemType, diags := getElementType[T](ctx)
167+
if diags.HasError() {
168+
panic(fmt.Errorf("error creating unknown NestedSetValue: %v", diags))
169+
}
170+
return NestedSetValue[T]{SetValue: basetypes.NewSetUnknown(elemType)}
171+
}
172+
173+
func (v NestedSetValue[T]) Equal(o attr.Value) bool {
174+
other, ok := o.(NestedSetValue[T])
175+
if !ok {
176+
return false
177+
}
178+
return v.SetValue.Equal(other.SetValue)
179+
}
180+
181+
func (v NestedSetValue[T]) Type(ctx context.Context) attr.Type {
182+
return NewNestedSetType[T](ctx)
183+
}
184+
185+
func (v NestedSetValue[T]) SlicePtrAsAny(ctx context.Context) (any, diag.Diagnostics) {
186+
valuePtr := new([]T)
187+
188+
if v.IsNull() || v.IsUnknown() {
189+
return valuePtr, nil
190+
}
191+
192+
diags := v.ElementsAs(ctx, valuePtr, false)
193+
if diags.HasError() {
194+
return nil, diags
195+
}
196+
197+
return valuePtr, diags
198+
}
199+
200+
func (v NestedSetValue[T]) NewEmptySlicePtr() any {
201+
return new([]T)
202+
}
203+
204+
func (v NestedSetValue[T]) Len() int {
205+
return len(v.Elements())
206+
}

internal/common/autogen/marshal.go

Lines changed: 25 additions & 14 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, customtypes.ListValueInterface, customtypes.NestedListValueInterface:
80+
case types.List, types.Set, customtypes.ListValueInterface, customtypes.NestedListValueInterface, customtypes.NestedSetValueInterface:
8181
val = []any{} // Send an empty array if it's a null root list or set
8282
}
8383
}
@@ -131,26 +131,37 @@ func getModelAttr(val attr.Value, isUpdate bool) (any, error) {
131131
return nil, fmt.Errorf("marshal failed for type: %v", diags)
132132
}
133133

134-
sliceValue := reflect.ValueOf(slicePtr).Elem()
135-
length := sliceValue.Len()
136-
137-
result := make([]any, 0, length)
138-
for i := range length {
139-
value, err := marshalAttrs(sliceValue.Index(i), isUpdate)
140-
if err != nil {
141-
return nil, err
142-
}
143-
if value != nil {
144-
result = append(result, value)
145-
}
134+
return getNestedSliceAttr(slicePtr, isUpdate)
135+
case customtypes.NestedSetValueInterface:
136+
slicePtr, diags := v.SlicePtrAsAny(context.Background())
137+
if diags.HasError() {
138+
return nil, fmt.Errorf("marshal failed for type: %v", diags)
146139
}
147140

148-
return result, nil
141+
return getNestedSliceAttr(slicePtr, isUpdate)
149142
default:
150143
return nil, fmt.Errorf("marshal not supported yet for type %T", v)
151144
}
152145
}
153146

147+
func getNestedSliceAttr(slicePtr any, isUpdate bool) (any, error) {
148+
sliceValue := reflect.ValueOf(slicePtr).Elem()
149+
length := sliceValue.Len()
150+
151+
result := make([]any, 0, length)
152+
for i := range length {
153+
value, err := marshalAttrs(sliceValue.Index(i), isUpdate)
154+
if err != nil {
155+
return nil, err
156+
}
157+
if value != nil {
158+
result = append(result, value)
159+
}
160+
}
161+
162+
return result, nil
163+
}
164+
154165
func getListAttr(elms []attr.Value, isUpdate bool) (any, error) {
155166
arr := make([]any, 0, len(elms))
156167
for _, attr := range elms {

internal/common/autogen/marshal_test.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func TestMarshalBasic(t *testing.T) {
4848
AttrFloat types.Float64 `tfsdk:"attr_float"`
4949
AttrString types.String `tfsdk:"attr_string"`
5050
AttrOmit types.String `tfsdk:"attr_omit" autogen:"omitjson"`
51-
AttrUnkown types.String `tfsdk:"attr_unknown"`
51+
AttrUnknown types.String `tfsdk:"attr_unknown"`
5252
AttrNull types.String `tfsdk:"attr_null"`
5353
AttrJSON jsontypes.Normalized `tfsdk:"attr_json"`
5454
AttrOmitNoTerraform string `autogen:"omitjson"`
@@ -62,7 +62,7 @@ func TestMarshalBasic(t *testing.T) {
6262
AttrString: types.StringValue("hello"),
6363
AttrOmit: types.StringValue("omit"),
6464
AttrOmitNoTerraform: "omit",
65-
AttrUnkown: types.StringUnknown(), // unknown values are not marshaled
65+
AttrUnknown: types.StringUnknown(), // unknown values are not marshaled
6666
AttrNull: types.StringNull(), // null values are not marshaled
6767
AttrInt: types.Int64Value(1),
6868
AttrBoolTrue: types.BoolValue(true),
@@ -494,6 +494,106 @@ func TestMarshalCustomTypeNestedList(t *testing.T) {
494494
assert.JSONEq(t, expectedUpdateJSON, string(rawUpdate))
495495
}
496496

497+
func TestMarshalCustomTypeNestedSet(t *testing.T) {
498+
ctx := context.Background()
499+
500+
type modelEmptyTest struct{}
501+
502+
type modelNestedObject struct {
503+
AttrNestedInt types.Int64 `tfsdk:"attr_nested_int"`
504+
}
505+
506+
type modelNestedSetItem struct {
507+
AttrOmit customtypes.NestedSetValue[modelEmptyTest] `tfsdk:"attr_omit" autogen:"omitjson"`
508+
AttrOmitUpdate customtypes.NestedSetValue[modelEmptyTest] `tfsdk:"attr_omit_update" autogen:"omitjsonupdate"`
509+
AttrPrimitive types.String `tfsdk:"attr_primitive"`
510+
AttrObject customtypes.ObjectValue[modelNestedObject] `tfsdk:"attr_object"`
511+
AttrMANYUpper types.Int64 `tfsdk:"attr_many_upper"`
512+
}
513+
514+
model := struct {
515+
AttrNestedSet customtypes.NestedSetValue[modelNestedSetItem] `tfsdk:"attr_nested_set"`
516+
AttrNestedSetNull customtypes.NestedSetValue[modelNestedSetItem] `tfsdk:"attr_nested_set_null"`
517+
AttrNestedSetEmpty customtypes.NestedSetValue[modelNestedSetItem] `tfsdk:"attr_nested_set_empty"`
518+
}{
519+
AttrNestedSet: customtypes.NewNestedSetValue[modelNestedSetItem](ctx, []modelNestedSetItem{
520+
{
521+
AttrPrimitive: types.StringValue("string1"),
522+
AttrMANYUpper: types.Int64Value(1),
523+
AttrObject: customtypes.NewObjectValue[modelNestedObject](ctx, modelNestedObject{
524+
AttrNestedInt: types.Int64Value(2),
525+
}),
526+
AttrOmit: customtypes.NewNestedSetValue[modelEmptyTest](ctx, []modelEmptyTest{}),
527+
AttrOmitUpdate: customtypes.NewNestedSetValue[modelEmptyTest](ctx, []modelEmptyTest{}),
528+
},
529+
{
530+
AttrPrimitive: types.StringValue("string2"),
531+
AttrMANYUpper: types.Int64Value(3),
532+
AttrObject: customtypes.NewObjectValue[modelNestedObject](ctx, modelNestedObject{
533+
AttrNestedInt: types.Int64Value(4),
534+
}),
535+
AttrOmit: customtypes.NewNestedSetValue[modelEmptyTest](ctx, []modelEmptyTest{}),
536+
AttrOmitUpdate: customtypes.NewNestedSetValue[modelEmptyTest](ctx, []modelEmptyTest{}),
537+
},
538+
}),
539+
AttrNestedSetNull: customtypes.NewNestedSetValueNull[modelNestedSetItem](ctx),
540+
AttrNestedSetEmpty: customtypes.NewNestedSetValue[modelNestedSetItem](ctx, []modelNestedSetItem{}),
541+
}
542+
543+
const expectedCreateJSON = `
544+
{
545+
"attrNestedSet": [
546+
{
547+
"attrPrimitive": "string1",
548+
"attrMANYUpper": 1,
549+
"attrObject": {
550+
"attrNestedInt": 2
551+
},
552+
"attrOmitUpdate": []
553+
},
554+
{
555+
"attrPrimitive": "string2",
556+
"attrMANYUpper": 3,
557+
"attrObject": {
558+
"attrNestedInt": 4
559+
},
560+
"attrOmitUpdate": []
561+
}
562+
],
563+
"attrNestedSetEmpty": []
564+
}
565+
`
566+
rawCreate, err := autogen.Marshal(&model, false)
567+
require.NoError(t, err)
568+
assert.JSONEq(t, expectedCreateJSON, string(rawCreate))
569+
570+
const expectedUpdateJSON = `
571+
{
572+
"attrNestedSet": [
573+
{
574+
"attrPrimitive": "string1",
575+
"attrMANYUpper": 1,
576+
"attrObject": {
577+
"attrNestedInt": 2
578+
}
579+
},
580+
{
581+
"attrPrimitive": "string2",
582+
"attrMANYUpper": 3,
583+
"attrObject": {
584+
"attrNestedInt": 4
585+
}
586+
}
587+
],
588+
"attrNestedSetNull": [],
589+
"attrNestedSetEmpty": []
590+
}
591+
`
592+
rawUpdate, err := autogen.Marshal(&model, true)
593+
require.NoError(t, err)
594+
assert.JSONEq(t, expectedUpdateJSON, string(rawUpdate))
595+
}
596+
497597
func TestMarshalUnsupported(t *testing.T) {
498598
testCases := map[string]any{
499599
"Int32 not supported yet as it's not being used in any model": &struct {

0 commit comments

Comments
 (0)