Skip to content

Commit 71dfab6

Browse files
bendbennettbflad
andauthored
Allow ignoring undefined attributes during unmarshalling (#213)
* Adding alternative implementation of Unmarshal which allows supplying of opts which can be used to modify the unmarshalling behaviour, such as ignoring undefined attributes (#212) * Upper case JSONOpts field (#212) * Code review changes (#212) * Apply suggestions from code review Co-authored-by: Brian Flad <[email protected]> Co-authored-by: Brian Flad <[email protected]>
1 parent a325d5b commit 71dfab6

File tree

7 files changed

+318
-21
lines changed

7 files changed

+318
-21
lines changed

.changelog/213.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
tfprotov5: Added `RawState` type `UnmarshalWithOpts` method to facilitate configurable behaviour during unmarshalling
3+
```
4+
5+
```release-note:enhancement
6+
tfprotov6: Added `RawState` type `UnmarshalWithOpts` method to facilitate configurable behaviour during unmarshalling
7+
```

tfprotov5/state.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
7777
}
7878
return tftypes.Value{}, ErrUnknownRawStateType
7979
}
80+
81+
// UnmarshalOpts contains options that can be used to modify the behaviour when
82+
// unmarshalling. Currently, this only contains a struct for opts for JSON but
83+
// could have a field for Flatmap in the future.
84+
type UnmarshalOpts struct {
85+
ValueFromJSONOpts tftypes.ValueFromJSONOpts
86+
}
87+
88+
// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
89+
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
90+
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
91+
if s.JSON != nil {
92+
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
93+
}
94+
if s.Flatmap != nil {
95+
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
96+
}
97+
return tftypes.Value{}, ErrUnknownRawStateType
98+
}

tfprotov5/state_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package tfprotov5_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
)
12+
13+
func TestRawStateUnmarshalWithOpts(t *testing.T) {
14+
t.Parallel()
15+
type testCase struct {
16+
rawState tfprotov5.RawState
17+
value tftypes.Value
18+
typ tftypes.Type
19+
opts tfprotov5.UnmarshalOpts
20+
}
21+
tests := map[string]testCase{
22+
"object-of-bool-number": {
23+
rawState: tfprotov5.RawState{
24+
JSON: []byte(`{"bool":true,"number":0}`),
25+
},
26+
value: tftypes.NewValue(tftypes.Object{
27+
AttributeTypes: map[string]tftypes.Type{
28+
"bool": tftypes.Bool,
29+
"number": tftypes.Number,
30+
},
31+
}, map[string]tftypes.Value{
32+
"bool": tftypes.NewValue(tftypes.Bool, true),
33+
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
34+
}),
35+
typ: tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"bool": tftypes.Bool,
38+
"number": tftypes.Number,
39+
},
40+
},
41+
},
42+
"object-with-missing-attribute": {
43+
rawState: tfprotov5.RawState{
44+
JSON: []byte(`{"bool":true,"number":0,"unknown":"whatever"}`),
45+
},
46+
value: tftypes.NewValue(tftypes.Object{
47+
AttributeTypes: map[string]tftypes.Type{
48+
"bool": tftypes.Bool,
49+
"number": tftypes.Number,
50+
},
51+
}, map[string]tftypes.Value{
52+
"bool": tftypes.NewValue(tftypes.Bool, true),
53+
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
54+
}),
55+
typ: tftypes.Object{
56+
AttributeTypes: map[string]tftypes.Type{
57+
"bool": tftypes.Bool,
58+
"number": tftypes.Number,
59+
},
60+
},
61+
opts: tfprotov5.UnmarshalOpts{
62+
ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
63+
IgnoreUndefinedAttributes: true,
64+
},
65+
},
66+
},
67+
}
68+
for name, test := range tests {
69+
name, test := name, test
70+
t.Run(name, func(t *testing.T) {
71+
t.Parallel()
72+
73+
val, err := test.rawState.UnmarshalWithOpts(test.typ, test.opts)
74+
if err != nil {
75+
t.Fatalf("unexpected error unmarshaling: %s", err)
76+
}
77+
78+
if diff := cmp.Diff(test.value, val); diff != "" {
79+
t.Errorf("Unexpected results (-wanted +got): %s", diff)
80+
}
81+
})
82+
}
83+
}

tfprotov6/state.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
7777
}
7878
return tftypes.Value{}, ErrUnknownRawStateType
7979
}
80+
81+
// UnmarshalOpts contains options that can be used to modify the behaviour when
82+
// unmarshalling. Currently, this only contains a struct for opts for JSON but
83+
// could have a field for Flatmap in the future.
84+
type UnmarshalOpts struct {
85+
ValueFromJSONOpts tftypes.ValueFromJSONOpts
86+
}
87+
88+
// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
89+
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
90+
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
91+
if s.JSON != nil {
92+
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
93+
}
94+
if s.Flatmap != nil {
95+
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
96+
}
97+
return tftypes.Value{}, ErrUnknownRawStateType
98+
}

tfprotov6/state_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package tfprotov6_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
"github.com/hashicorp/terraform-plugin-go/tftypes"
11+
)
12+
13+
func TestRawStateUnmarshalWithOpts(t *testing.T) {
14+
t.Parallel()
15+
type testCase struct {
16+
rawState tfprotov6.RawState
17+
value tftypes.Value
18+
typ tftypes.Type
19+
opts tfprotov6.UnmarshalOpts
20+
}
21+
tests := map[string]testCase{
22+
"object-of-bool-number": {
23+
rawState: tfprotov6.RawState{
24+
JSON: []byte(`{"bool":true,"number":0}`),
25+
},
26+
value: tftypes.NewValue(tftypes.Object{
27+
AttributeTypes: map[string]tftypes.Type{
28+
"bool": tftypes.Bool,
29+
"number": tftypes.Number,
30+
},
31+
}, map[string]tftypes.Value{
32+
"bool": tftypes.NewValue(tftypes.Bool, true),
33+
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
34+
}),
35+
typ: tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"bool": tftypes.Bool,
38+
"number": tftypes.Number,
39+
},
40+
},
41+
},
42+
"object-with-missing-attribute": {
43+
rawState: tfprotov6.RawState{
44+
JSON: []byte(`{"bool":true,"number":0,"unknown":"whatever"}`),
45+
},
46+
value: tftypes.NewValue(tftypes.Object{
47+
AttributeTypes: map[string]tftypes.Type{
48+
"bool": tftypes.Bool,
49+
"number": tftypes.Number,
50+
},
51+
}, map[string]tftypes.Value{
52+
"bool": tftypes.NewValue(tftypes.Bool, true),
53+
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
54+
}),
55+
typ: tftypes.Object{
56+
AttributeTypes: map[string]tftypes.Type{
57+
"bool": tftypes.Bool,
58+
"number": tftypes.Number,
59+
},
60+
},
61+
opts: tfprotov6.UnmarshalOpts{
62+
ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
63+
IgnoreUndefinedAttributes: true,
64+
},
65+
},
66+
},
67+
}
68+
for name, test := range tests {
69+
name, test := name, test
70+
t.Run(name, func(t *testing.T) {
71+
t.Parallel()
72+
73+
val, err := test.rawState.UnmarshalWithOpts(test.typ, test.opts)
74+
if err != nil {
75+
t.Fatalf("unexpected error unmarshaling: %s", err)
76+
}
77+
78+
if diff := cmp.Diff(test.value, val); diff != "" {
79+
t.Errorf("Unexpected results (-wanted +got): %s", diff)
80+
}
81+
})
82+
}
83+
}

tftypes/value_json.go

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,24 @@ import (
1616
// terraform-plugin-go. Third parties should not use it, and its behavior is
1717
// not covered under the API compatibility guarantees. Don't use this.
1818
func ValueFromJSON(data []byte, typ Type) (Value, error) {
19-
return jsonUnmarshal(data, typ, NewAttributePath())
19+
return jsonUnmarshal(data, typ, NewAttributePath(), ValueFromJSONOpts{})
20+
}
21+
22+
// ValueFromJSONOpts contains options that can be used to modify the behaviour when
23+
// unmarshalling JSON.
24+
type ValueFromJSONOpts struct {
25+
// IgnoreUndefinedAttributes is used to ignore any attributes which appear in the
26+
// JSON but do not have a corresponding entry in the schema. For example, raw state
27+
// where an attribute has been removed from the schema.
28+
IgnoreUndefinedAttributes bool
29+
}
30+
31+
// ValueFromJSONWithOpts is identical to ValueFromJSON with the exception that it
32+
// accepts ValueFromJSONOpts which can be used to modify the unmarshalling behaviour, such
33+
// as ignoring undefined attributes, for instance. This can occur when the JSON
34+
// being unmarshalled does not have a corresponding attribute in the schema.
35+
func ValueFromJSONWithOpts(data []byte, typ Type, opts ValueFromJSONOpts) (Value, error) {
36+
return jsonUnmarshal(data, typ, NewAttributePath(), opts)
2037
}
2138

2239
func jsonByteDecoder(buf []byte) *json.Decoder {
@@ -26,7 +43,7 @@ func jsonByteDecoder(buf []byte) *json.Decoder {
2643
return dec
2744
}
2845

29-
func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
46+
func jsonUnmarshal(buf []byte, typ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
3047
dec := jsonByteDecoder(buf)
3148

3249
tok, err := dec.Token()
@@ -46,18 +63,17 @@ func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
4663
case typ.Is(Bool):
4764
return jsonUnmarshalBool(buf, typ, p)
4865
case typ.Is(DynamicPseudoType):
49-
return jsonUnmarshalDynamicPseudoType(buf, typ, p)
66+
return jsonUnmarshalDynamicPseudoType(buf, typ, p, opts)
5067
case typ.Is(List{}):
51-
return jsonUnmarshalList(buf, typ.(List).ElementType, p)
68+
return jsonUnmarshalList(buf, typ.(List).ElementType, p, opts)
5269
case typ.Is(Set{}):
53-
return jsonUnmarshalSet(buf, typ.(Set).ElementType, p)
54-
70+
return jsonUnmarshalSet(buf, typ.(Set).ElementType, p, opts)
5571
case typ.Is(Map{}):
56-
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p)
72+
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p, opts)
5773
case typ.Is(Tuple{}):
58-
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p)
74+
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p, opts)
5975
case typ.Is(Object{}):
60-
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p)
76+
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p, opts)
6177
}
6278
return Value{}, p.NewErrorf("unknown type %s", typ)
6379
}
@@ -140,7 +156,7 @@ func jsonUnmarshalBool(buf []byte, _ Type, p *AttributePath) (Value, error) {
140156
return Value{}, p.NewErrorf("unsupported type %T sent as %s", tok, Bool)
141157
}
142158

143-
func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value, error) {
159+
func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
144160
dec := jsonByteDecoder(buf)
145161
tok, err := dec.Token()
146162
if err != nil {
@@ -190,10 +206,10 @@ func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value
190206
if valBody == nil {
191207
return Value{}, p.NewErrorf("missing value in dynamically-typed value")
192208
}
193-
return jsonUnmarshal(valBody, t, p)
209+
return jsonUnmarshal(valBody, t, p, opts)
194210
}
195211

196-
func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, error) {
212+
func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
197213
dec := jsonByteDecoder(buf)
198214

199215
tok, err := dec.Token()
@@ -227,7 +243,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
227243
if err != nil {
228244
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
229245
}
230-
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
246+
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
231247
if err != nil {
232248
return Value{}, err
233249
}
@@ -254,7 +270,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
254270
}, vals), nil
255271
}
256272

257-
func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, error) {
273+
func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
258274
dec := jsonByteDecoder(buf)
259275

260276
tok, err := dec.Token()
@@ -284,7 +300,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
284300
if err != nil {
285301
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
286302
}
287-
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
303+
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
288304
if err != nil {
289305
return Value{}, err
290306
}
@@ -310,7 +326,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
310326
}, vals), nil
311327
}
312328

313-
func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error) {
329+
func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
314330
dec := jsonByteDecoder(buf)
315331

316332
tok, err := dec.Token()
@@ -341,7 +357,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
341357
if err != nil {
342358
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
343359
}
344-
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
360+
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
345361
if err != nil {
346362
return Value{}, err
347363
}
@@ -360,7 +376,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
360376
}, vals), nil
361377
}
362378

363-
func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Value, error) {
379+
func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
364380
dec := jsonByteDecoder(buf)
365381

366382
tok, err := dec.Token()
@@ -398,7 +414,7 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
398414
if err != nil {
399415
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
400416
}
401-
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
417+
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
402418
if err != nil {
403419
return Value{}, err
404420
}
@@ -422,7 +438,9 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
422438
}, vals), nil
423439
}
424440

425-
func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath) (Value, error) {
441+
// jsonUnmarshalObject attempts to decode JSON object structure to tftypes.Value object.
442+
// opts contains fields that can be used to modify the behaviour of JSON unmarshalling.
443+
func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
426444
dec := jsonByteDecoder(buf)
427445

428446
tok, err := dec.Token()
@@ -446,6 +464,12 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
446464
}
447465
attrType, ok := attrTypes[key]
448466
if !ok {
467+
if opts.IgnoreUndefinedAttributes {
468+
// We are trying to ignore the key and value of any unsupported attribute.
469+
_ = dec.Decode(new(json.RawMessage))
470+
continue
471+
}
472+
449473
return Value{}, innerPath.NewErrorf("unsupported attribute %q", key)
450474
}
451475
innerPath = p.WithAttributeName(key)
@@ -455,7 +479,7 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
455479
if err != nil {
456480
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
457481
}
458-
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
482+
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
459483
if err != nil {
460484
return Value{}, err
461485
}

0 commit comments

Comments
 (0)