/
object.go
331 lines (295 loc) · 8.92 KB
/
object.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package types
import (
"context"
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
var (
_ attr.Type = ObjectType{}
_ attr.Value = &Object{}
)
// ObjectType is an AttributeType representing an object.
type ObjectType struct {
AttrTypes map[string]attr.Type
}
// WithAttributeTypes returns a new copy of the type with its attribute types
// set.
func (o ObjectType) WithAttributeTypes(typs map[string]attr.Type) attr.TypeWithAttributeTypes {
return ObjectType{
AttrTypes: typs,
}
}
// AttributeTypes returns the type's attribute types.
func (o ObjectType) AttributeTypes() map[string]attr.Type {
return o.AttrTypes
}
// TerraformType returns the tftypes.Type that should be used to
// represent this type. This constrains what user input will be
// accepted and what kind of data can be set in state. The framework
// will use this to translate the AttributeType to something Terraform
// can understand.
func (o ObjectType) TerraformType(ctx context.Context) tftypes.Type {
attributeTypes := map[string]tftypes.Type{}
for k, v := range o.AttrTypes {
attributeTypes[k] = v.TerraformType(ctx)
}
return tftypes.Object{
AttributeTypes: attributeTypes,
}
}
// ValueFromTerraform returns an attr.Value given a tftypes.Value.
// This is meant to convert the tftypes.Value into a more convenient Go
// type for the provider to consume the data with.
func (o ObjectType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
object := Object{
AttrTypes: o.AttrTypes,
}
if in.Type() == nil {
object.Null = true
return object, nil
}
if !in.Type().Equal(o.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", o.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
object.Unknown = true
return object, nil
}
if in.IsNull() {
object.Null = true
return object, nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := object.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
object.Attrs = attributes
return object, nil
}
// Equal returns true if `candidate` is also an ObjectType and has the same
// AttributeTypes.
func (o ObjectType) Equal(candidate attr.Type) bool {
other, ok := candidate.(ObjectType)
if !ok {
return false
}
if len(other.AttrTypes) != len(o.AttrTypes) {
return false
}
for k, v := range o.AttrTypes {
attr, ok := other.AttrTypes[k]
if !ok {
return false
}
if !v.Equal(attr) {
return false
}
}
return true
}
// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the
// object.
func (o ObjectType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) {
if _, ok := step.(tftypes.AttributeName); !ok {
return nil, fmt.Errorf("cannot apply step %T to ObjectType", step)
}
return o.AttrTypes[string(step.(tftypes.AttributeName))], nil
}
// String returns a human-friendly description of the ObjectType.
func (o ObjectType) String() string {
var res strings.Builder
res.WriteString("types.ObjectType[")
keys := make([]string, 0, len(o.AttrTypes))
for k := range o.AttrTypes {
keys = append(keys, k)
}
sort.Strings(keys)
for pos, key := range keys {
if pos != 0 {
res.WriteString(", ")
}
res.WriteString(`"` + key + `":`)
res.WriteString(o.AttrTypes[key].String())
}
res.WriteString("]")
return res.String()
}
// ValueType returns the Value type.
func (t ObjectType) ValueType(_ context.Context) attr.Value {
return Object{
AttrTypes: t.AttrTypes,
}
}
// Object represents an object
type Object struct {
// Unknown will be set to true if the entire object is an unknown value.
// If only some of the elements in the object are unknown, their known or
// unknown status will be represented however that attr.Value
// surfaces that information. The Object's Unknown property only tracks
// if the number of elements in a Object is known, not whether the
// elements that are in the object are known.
Unknown bool
// Null will be set to true if the object is null, either because it was
// omitted from the configuration, state, or plan, or because it was
// explicitly set to null.
Null bool
Attrs map[string]attr.Value
AttrTypes map[string]attr.Type
}
// ObjectAsOptions is a collection of toggles to control the behavior of
// Object.As.
type ObjectAsOptions struct {
// UnhandledNullAsEmpty controls what happens when As needs to put a
// null value in a type that has no way to preserve that distinction.
// When set to true, the type's empty value will be used. When set to
// false, an error will be returned.
UnhandledNullAsEmpty bool
// UnhandledUnknownAsEmpty controls what happens when As needs to put
// an unknown value in a type that has no way to preserve that
// distinction. When set to true, the type's empty value will be used.
// When set to false, an error will be returned.
UnhandledUnknownAsEmpty bool
}
// As populates `target` with the data in the Object, throwing an error if the
// data cannot be stored in `target`.
func (o Object) As(ctx context.Context, target interface{}, opts ObjectAsOptions) diag.Diagnostics {
// we need a tftypes.Value for this Object to be able to use it with
// our reflection code
obj := ObjectType{AttrTypes: o.AttrTypes}
val, err := o.ToTerraformValue(ctx)
if err != nil {
return diag.Diagnostics{
diag.NewErrorDiagnostic(
"Object Conversion Error",
"An unexpected error was encountered trying to convert object. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(),
),
}
}
return reflect.Into(ctx, obj, val, target, reflect.Options{
UnhandledNullAsEmpty: opts.UnhandledNullAsEmpty,
UnhandledUnknownAsEmpty: opts.UnhandledUnknownAsEmpty,
}, path.Empty())
}
// Type returns an ObjectType with the same attribute types as `o`.
func (o Object) Type(_ context.Context) attr.Type {
return ObjectType{AttrTypes: o.AttrTypes}
}
// ToTerraformValue returns the data contained in the attr.Value as
// a tftypes.Value.
func (o Object) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
if o.AttrTypes == nil {
return tftypes.Value{}, fmt.Errorf("cannot convert Object to tftypes.Value if AttrTypes field is not set")
}
attrTypes := map[string]tftypes.Type{}
for attr, typ := range o.AttrTypes {
attrTypes[attr] = typ.TerraformType(ctx)
}
objectType := tftypes.Object{AttributeTypes: attrTypes}
if o.Unknown {
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
}
if o.Null {
return tftypes.NewValue(objectType, nil), nil
}
vals := map[string]tftypes.Value{}
for k, v := range o.Attrs {
val, err := v.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals[k] = val
}
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
}
// Equal returns true if the Object is considered semantically equal
// (same type and same value) to the attr.Value passed as an argument.
func (o Object) Equal(c attr.Value) bool {
other, ok := c.(Object)
if !ok {
return false
}
if o.Unknown != other.Unknown {
return false
}
if o.Null != other.Null {
return false
}
if len(o.AttrTypes) != len(other.AttrTypes) {
return false
}
for k, v := range o.AttrTypes {
attr, ok := other.AttrTypes[k]
if !ok {
return false
}
if !v.Equal(attr) {
return false
}
}
if len(o.Attrs) != len(other.Attrs) {
return false
}
for k, v := range o.Attrs {
attr, ok := other.Attrs[k]
if !ok {
return false
}
if !v.Equal(attr) {
return false
}
}
return true
}
// IsNull returns true if the Object represents a null value.
func (o Object) IsNull() bool {
return o.Null
}
// IsUnknown returns true if the Object represents a currently unknown value.
func (o Object) IsUnknown() bool {
return o.Unknown
}
// String returns a human-readable representation of the Object value.
// The string returned here is not protected by any compatibility guarantees,
// and is intended for logging and error reporting.
func (o Object) String() string {
if o.Unknown {
return attr.UnknownValueString
}
if o.Null {
return attr.NullValueString
}
// We want the output to be consistent, so we sort the output by key
keys := make([]string, 0, len(o.Attrs))
for k := range o.Attrs {
keys = append(keys, k)
}
sort.Strings(keys)
var res strings.Builder
res.WriteString("{")
for i, k := range keys {
if i != 0 {
res.WriteString(",")
}
res.WriteString(fmt.Sprintf(`"%s":%s`, k, o.Attrs[k].String()))
}
res.WriteString("}")
return res.String()
}