Skip to content

Commit fc987f9

Browse files
fix: Support ListTypable and SetTypable elem values for handling custom types (#2142)
Fixes #2020.
1 parent c47140e commit fc987f9

File tree

2 files changed

+294
-8
lines changed

2 files changed

+294
-8
lines changed

pf/internal/schemashim/type_schema.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,26 +61,23 @@ func (s *typeSchema) Elem() interface{} {
6161
case basetypes.ObjectTypable:
6262
var pseudoResource shim.Resource = newObjectPseudoResource(tt, s.nested, nil)
6363
return pseudoResource
64-
case types.ListType:
64+
case basetypes.SetTypable, basetypes.ListTypable:
65+
typeWithElementType, ok := s.t.(pfattr.TypeWithElementType)
66+
contract.Assertf(ok, "List or Set type %T expect to implement TypeWithElementType", s.t)
6567
contract.Assertf(s.nested == nil || len(s.nested) == 0,
66-
"s.t==ListType should not have any s.nested attrs")
67-
return newTypeSchema(tt.ElemType, nil)
68+
"s.t==SetTypable should not have any s.nested attrs")
69+
return newTypeSchema(typeWithElementType.ElementType(), nil)
6870
case types.MapType:
6971
contract.Assertf(s.nested == nil || len(s.nested) == 0,
7072
"s.t==MapType should not have any s.nested attrs")
7173
return newTypeSchema(tt.ElemType, nil)
72-
case types.SetType:
73-
contract.Assertf(s.nested == nil || len(s.nested) == 0,
74-
"s.t==SetType should not have any s.nested attrs")
75-
return newTypeSchema(tt.ElemType, nil)
7674
case pfattr.TypeWithElementTypes:
7775
var pseudoResource shim.Resource = newTuplePseudoResource(tt)
7876
return pseudoResource
7977
default:
8078
return nil
8179
}
8280
}
83-
8481
func (*typeSchema) MaxItems() int { return 0 }
8582
func (*typeSchema) MinItems() int { return 0 }
8683
func (*typeSchema) Deprecated() string { return "" }
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package tfbridgetests
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
12+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
13+
"github.com/hashicorp/terraform-plugin-framework/types"
14+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
15+
"github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/providerbuilder"
16+
"github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge"
17+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert"
18+
tfbridge0 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
19+
"github.com/stretchr/testify/assert"
20+
"reflect"
21+
"testing"
22+
)
23+
24+
func TestNestedCustomTypeEncoding(t *testing.T) {
25+
26+
testProvider := &providerbuilder.Provider{
27+
TypeName: "testprovider",
28+
Version: "0.0.1",
29+
// This resource is modified from AWS Bedrockagent.
30+
AllResources: []providerbuilder.Resource{{
31+
Name: "bedrockagent",
32+
ResourceSchema: schema.Schema{
33+
Attributes: map[string]schema.Attribute{
34+
"prompt_override_configuration": schema.ListAttribute{ // proto5 Optional+Computed nested block.
35+
CustomType: NewListNestedObjectTypeOf[promptOverrideConfigurationModel](context.Background()),
36+
Optional: true,
37+
Computed: true,
38+
PlanModifiers: []planmodifier.List{
39+
listplanmodifier.UseStateForUnknown(),
40+
},
41+
Validators: []validator.List{
42+
listvalidator.SizeAtMost(1),
43+
},
44+
ElementType: types.ObjectType{
45+
AttrTypes: AttributeTypesMust[promptOverrideConfigurationModel](context.Background()),
46+
},
47+
},
48+
},
49+
},
50+
}},
51+
}
52+
res := tfbridge0.ResourceInfo{
53+
Tok: "testprovider:index/bedrockagent:Bedrockagent",
54+
Docs: &tfbridge0.DocInfo{
55+
Markdown: []byte("OK"),
56+
},
57+
Fields: map[string]*tfbridge0.SchemaInfo{},
58+
}
59+
60+
info := tfbridge0.ProviderInfo{
61+
Name: "testprovider",
62+
P: tfbridge.ShimProvider(testProvider),
63+
Version: "0.0.1",
64+
MetadataInfo: &tfbridge0.MetadataInfo{},
65+
Resources: map[string]*tfbridge0.ResourceInfo{
66+
"testprovider_bedrockagent": &res,
67+
},
68+
}
69+
70+
encoding := convert.NewEncoding(info.P, &info)
71+
objType := convert.InferObjectType(info.P.ResourcesMap().Get("testprovider_bedrockagent").Schema(), nil)
72+
_, err := encoding.NewResourceEncoder("testprovider_bedrockagent", objType)
73+
assert.NoError(t, err)
74+
}
75+
76+
type promptOverrideConfigurationModel struct {
77+
PromptConfigurations SetNestedObjectValueOf[promptConfigurationModel] `tfsdk:"prompt_configurations"`
78+
}
79+
80+
type promptConfigurationModel struct {
81+
BasePromptTemplate types.String `tfsdk:"base_prompt_template"`
82+
}
83+
84+
// Implementation for set, list, and object typables.
85+
86+
// Set
87+
88+
var (
89+
_ basetypes.SetTypable = (*SetNestedObjectTypeOf[struct{}])(nil)
90+
_ basetypes.SetValuable = (*SetNestedObjectValueOf[struct{}])(nil)
91+
)
92+
93+
type SetNestedObjectTypeOf[T any] struct {
94+
basetypes.SetType
95+
}
96+
97+
type SetNestedObjectValueOf[T any] struct {
98+
basetypes.SetValue
99+
}
100+
101+
func (setNested SetNestedObjectTypeOf[T]) ValueFromSet(ctx context.Context, in basetypes.SetValue) (basetypes.SetValuable, diag.Diagnostics) {
102+
var diags diag.Diagnostics
103+
104+
if in.IsNull() {
105+
return NewSetNestedObjectValueOfNull[T](ctx), diags
106+
}
107+
if in.IsUnknown() {
108+
return NewSetNestedObjectValueOfUnknown[T](ctx), diags
109+
}
110+
111+
typ, d := newObjectTypeOf[T](ctx)
112+
diags.Append(d...)
113+
if diags.HasError() {
114+
return NewSetNestedObjectValueOfUnknown[T](ctx), diags
115+
}
116+
117+
v, d := basetypes.NewSetValue(typ, in.Elements())
118+
diags.Append(d...)
119+
if diags.HasError() {
120+
return NewSetNestedObjectValueOfUnknown[T](ctx), diags
121+
}
122+
123+
return SetNestedObjectValueOf[T]{SetValue: v}, diags
124+
}
125+
126+
func NewSetNestedObjectValueOfNull[T any](ctx context.Context) SetNestedObjectValueOf[T] {
127+
return SetNestedObjectValueOf[T]{SetValue: basetypes.NewSetNull(NewObjectTypeOf[T](ctx))}
128+
}
129+
130+
func NewSetNestedObjectValueOfUnknown[T any](ctx context.Context) SetNestedObjectValueOf[T] {
131+
return SetNestedObjectValueOf[T]{SetValue: basetypes.NewSetUnknown(NewObjectTypeOf[T](ctx))}
132+
}
133+
134+
func (setNested SetNestedObjectTypeOf[T]) ValueType(ctx context.Context) attr.Value {
135+
return SetNestedObjectValueOf[T]{}
136+
}
137+
138+
func (setNested SetNestedObjectValueOf[T]) Equal(o attr.Value) bool {
139+
other, ok := o.(SetNestedObjectValueOf[T])
140+
141+
if !ok {
142+
return false
143+
}
144+
145+
return setNested.SetValue.Equal(other.SetValue)
146+
}
147+
148+
func (setNested SetNestedObjectValueOf[T]) Type(ctx context.Context) attr.Type {
149+
return NewSetNestedObjectTypeOf[T](ctx)
150+
}
151+
func NewSetNestedObjectTypeOf[T any](ctx context.Context) SetNestedObjectTypeOf[T] {
152+
return SetNestedObjectTypeOf[T]{basetypes.SetType{ElemType: NewObjectTypeOf[T](ctx)}}
153+
}
154+
155+
/// List
156+
157+
var (
158+
_ basetypes.ListTypable = (*ListNestedObjectTypeOf[struct{}])(nil)
159+
_ basetypes.ListValuable = (*ListNestedObjectValueOf[struct{}])(nil)
160+
)
161+
162+
// ListNestedObjectValueOf represents a Terraform Plugin Framework List value whose elements are of type `ObjectTypeOf[T]`.
163+
type ListNestedObjectValueOf[T any] struct {
164+
basetypes.ListValue
165+
}
166+
167+
// ListNestedObjectTypeOf is the attribute type of a ListNestedObjectValueOf.
168+
type ListNestedObjectTypeOf[T any] struct {
169+
basetypes.ListType
170+
}
171+
172+
func NewListNestedObjectTypeOf[T any](ctx context.Context) ListNestedObjectTypeOf[T] {
173+
return ListNestedObjectTypeOf[T]{basetypes.ListType{ElemType: NewObjectTypeOf[T](ctx)}}
174+
}
175+
176+
func (listNested ListNestedObjectTypeOf[T]) ValueFromList(ctx context.Context, listval basetypes.ListValue) (basetypes.ListValuable, diag.Diagnostics) {
177+
var diags diag.Diagnostics
178+
179+
if listval.IsNull() {
180+
return NewListNestedObjectValueOfNull[T](ctx), diags
181+
}
182+
if listval.IsUnknown() {
183+
return NewListNestedObjectValueOfUnknown[T](ctx), diags
184+
}
185+
186+
typ, d := newObjectTypeOf[T](ctx)
187+
diags.Append(d...)
188+
if diags.HasError() {
189+
return NewListNestedObjectValueOfUnknown[T](ctx), diags
190+
}
191+
192+
v, d := basetypes.NewListValue(typ, listval.Elements())
193+
diags.Append(d...)
194+
if diags.HasError() {
195+
return NewListNestedObjectValueOfUnknown[T](ctx), diags
196+
}
197+
198+
return ListNestedObjectValueOf[T]{ListValue: v}, diags
199+
}
200+
201+
func NewListNestedObjectValueOfNull[T any](ctx context.Context) ListNestedObjectValueOf[T] {
202+
return ListNestedObjectValueOf[T]{ListValue: basetypes.NewListNull(NewObjectTypeOf[T](ctx))}
203+
}
204+
205+
func NewListNestedObjectValueOfUnknown[T any](ctx context.Context) ListNestedObjectValueOf[T] {
206+
return ListNestedObjectValueOf[T]{ListValue: basetypes.NewListUnknown(NewObjectTypeOf[T](ctx))}
207+
}
208+
209+
/// Object
210+
211+
var (
212+
_ basetypes.ObjectTypable = (*objectTypeOf[struct{}])(nil)
213+
_ basetypes.ObjectValuable = (*ObjectValueOf[struct{}])(nil)
214+
)
215+
216+
type objectTypeOf[T any] struct {
217+
basetypes.ObjectType
218+
}
219+
220+
type ObjectValueOf[T any] struct {
221+
basetypes.ObjectValue
222+
}
223+
224+
func newObjectTypeOf[T any](ctx context.Context) (objectTypeOf[T], diag.Diagnostics) {
225+
var diags diag.Diagnostics
226+
227+
m, d := AttributeTypes[T](ctx)
228+
diags.Append(d...)
229+
if diags.HasError() {
230+
return objectTypeOf[T]{}, diags
231+
}
232+
233+
return objectTypeOf[T]{basetypes.ObjectType{AttrTypes: m}}, diags
234+
}
235+
236+
func NewObjectTypeOf[T any](ctx context.Context) basetypes.ObjectTypable {
237+
return objectTypeOf[T]{basetypes.ObjectType{AttrTypes: AttributeTypesMust[T](ctx)}}
238+
}
239+
240+
func AttributeTypesMust[T any](ctx context.Context) map[string]attr.Type {
241+
return must(AttributeTypes[T](ctx))
242+
}
243+
244+
func must[T any, E any](t T, err E) T {
245+
if v := reflect.ValueOf(err); v.IsValid() && !v.IsZero() {
246+
panic(err)
247+
}
248+
return t
249+
}
250+
251+
// AttributeTypes returns a map of attribute types for the specified type T.
252+
// T must be a struct and reflection is used to find exported fields of T with the `tfsdk` tag.
253+
func AttributeTypes[T any](ctx context.Context) (map[string]attr.Type, diag.Diagnostics) {
254+
var diags diag.Diagnostics
255+
var t T
256+
val := reflect.ValueOf(t)
257+
typ := val.Type()
258+
259+
if typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct {
260+
val = reflect.New(typ.Elem()).Elem()
261+
typ = typ.Elem()
262+
}
263+
264+
if typ.Kind() != reflect.Struct {
265+
diags.Append(diag.NewErrorDiagnostic("Invalid type", fmt.Sprintf("%T has unsupported type: %s", t, typ)))
266+
return nil, diags
267+
}
268+
269+
attributeTypes := make(map[string]attr.Type)
270+
for i := 0; i < typ.NumField(); i++ {
271+
field := typ.Field(i)
272+
if field.PkgPath != "" {
273+
continue // Skip unexported fields.
274+
}
275+
tag := field.Tag.Get(`tfsdk`)
276+
if tag == "-" {
277+
continue // Skip explicitly excluded fields.
278+
}
279+
if tag == "" {
280+
diags.Append(diag.NewErrorDiagnostic("Invalid type", fmt.Sprintf(`%T needs a struct tag for "tfsdk" on %s`, t, field.Name)))
281+
return nil, diags
282+
}
283+
284+
if v, ok := val.Field(i).Interface().(attr.Value); ok {
285+
attributeTypes[tag] = v.Type(ctx)
286+
}
287+
}
288+
return attributeTypes, nil
289+
}

0 commit comments

Comments
 (0)