Skip to content

Commit

Permalink
Allow ignoring undefined attributes during unmarshalling (#213)
Browse files Browse the repository at this point in the history
* 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 <bflad417@gmail.com>

Co-authored-by: Brian Flad <bflad417@gmail.com>
  • Loading branch information
bendbennett and bflad committed Jul 28, 2022
1 parent a325d5b commit 71dfab6
Show file tree
Hide file tree
Showing 7 changed files with 318 additions and 21 deletions.
7 changes: 7 additions & 0 deletions .changelog/213.txt
@@ -0,0 +1,7 @@
```release-note:enhancement
tfprotov5: Added `RawState` type `UnmarshalWithOpts` method to facilitate configurable behaviour during unmarshalling
```

```release-note:enhancement
tfprotov6: Added `RawState` type `UnmarshalWithOpts` method to facilitate configurable behaviour during unmarshalling
```
19 changes: 19 additions & 0 deletions tfprotov5/state.go
Expand Up @@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}

// UnmarshalOpts contains options that can be used to modify the behaviour when
// unmarshalling. Currently, this only contains a struct for opts for JSON but
// could have a field for Flatmap in the future.
type UnmarshalOpts struct {
ValueFromJSONOpts tftypes.ValueFromJSONOpts
}

// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
}
if s.Flatmap != nil {
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
}
return tftypes.Value{}, ErrUnknownRawStateType
}
83 changes: 83 additions & 0 deletions tfprotov5/state_test.go
@@ -0,0 +1,83 @@
package tfprotov5_test

import (
"math/big"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestRawStateUnmarshalWithOpts(t *testing.T) {
t.Parallel()
type testCase struct {
rawState tfprotov5.RawState
value tftypes.Value
typ tftypes.Type
opts tfprotov5.UnmarshalOpts
}
tests := map[string]testCase{
"object-of-bool-number": {
rawState: tfprotov5.RawState{
JSON: []byte(`{"bool":true,"number":0}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
},
"object-with-missing-attribute": {
rawState: tfprotov5.RawState{
JSON: []byte(`{"bool":true,"number":0,"unknown":"whatever"}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
opts: tfprotov5.UnmarshalOpts{
ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
IgnoreUndefinedAttributes: true,
},
},
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()

val, err := test.rawState.UnmarshalWithOpts(test.typ, test.opts)
if err != nil {
t.Fatalf("unexpected error unmarshaling: %s", err)
}

if diff := cmp.Diff(test.value, val); diff != "" {
t.Errorf("Unexpected results (-wanted +got): %s", diff)
}
})
}
}
19 changes: 19 additions & 0 deletions tfprotov6/state.go
Expand Up @@ -77,3 +77,22 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}

// UnmarshalOpts contains options that can be used to modify the behaviour when
// unmarshalling. Currently, this only contains a struct for opts for JSON but
// could have a field for Flatmap in the future.
type UnmarshalOpts struct {
ValueFromJSONOpts tftypes.ValueFromJSONOpts
}

// UnmarshalWithOpts is identical to Unmarshal but also accepts a tftypes.UnmarshalOpts which contains
// options that can be used to modify the behaviour when unmarshalling JSON or Flatmap.
func (s RawState) UnmarshalWithOpts(typ tftypes.Type, opts UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.ValueFromJSONOpts) //nolint:staticcheck
}
if s.Flatmap != nil {
return tftypes.Value{}, fmt.Errorf("flatmap states cannot be unmarshaled, only states written by Terraform 0.12 and higher can be unmarshaled")
}
return tftypes.Value{}, ErrUnknownRawStateType
}
83 changes: 83 additions & 0 deletions tfprotov6/state_test.go
@@ -0,0 +1,83 @@
package tfprotov6_test

import (
"math/big"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestRawStateUnmarshalWithOpts(t *testing.T) {
t.Parallel()
type testCase struct {
rawState tfprotov6.RawState
value tftypes.Value
typ tftypes.Type
opts tfprotov6.UnmarshalOpts
}
tests := map[string]testCase{
"object-of-bool-number": {
rawState: tfprotov6.RawState{
JSON: []byte(`{"bool":true,"number":0}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
},
"object-with-missing-attribute": {
rawState: tfprotov6.RawState{
JSON: []byte(`{"bool":true,"number":0,"unknown":"whatever"}`),
},
value: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
}, map[string]tftypes.Value{
"bool": tftypes.NewValue(tftypes.Bool, true),
"number": tftypes.NewValue(tftypes.Number, big.NewFloat(0)),
}),
typ: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"bool": tftypes.Bool,
"number": tftypes.Number,
},
},
opts: tfprotov6.UnmarshalOpts{
ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
IgnoreUndefinedAttributes: true,
},
},
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()

val, err := test.rawState.UnmarshalWithOpts(test.typ, test.opts)
if err != nil {
t.Fatalf("unexpected error unmarshaling: %s", err)
}

if diff := cmp.Diff(test.value, val); diff != "" {
t.Errorf("Unexpected results (-wanted +got): %s", diff)
}
})
}
}
66 changes: 45 additions & 21 deletions tftypes/value_json.go
Expand Up @@ -16,7 +16,24 @@ import (
// terraform-plugin-go. Third parties should not use it, and its behavior is
// not covered under the API compatibility guarantees. Don't use this.
func ValueFromJSON(data []byte, typ Type) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath())
return jsonUnmarshal(data, typ, NewAttributePath(), ValueFromJSONOpts{})
}

// ValueFromJSONOpts contains options that can be used to modify the behaviour when
// unmarshalling JSON.
type ValueFromJSONOpts struct {
// IgnoreUndefinedAttributes is used to ignore any attributes which appear in the
// JSON but do not have a corresponding entry in the schema. For example, raw state
// where an attribute has been removed from the schema.
IgnoreUndefinedAttributes bool
}

// ValueFromJSONWithOpts is identical to ValueFromJSON with the exception that it
// accepts ValueFromJSONOpts which can be used to modify the unmarshalling behaviour, such
// as ignoring undefined attributes, for instance. This can occur when the JSON
// being unmarshalled does not have a corresponding attribute in the schema.
func ValueFromJSONWithOpts(data []byte, typ Type, opts ValueFromJSONOpts) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath(), opts)
}

func jsonByteDecoder(buf []byte) *json.Decoder {
Expand All @@ -26,7 +43,7 @@ func jsonByteDecoder(buf []byte) *json.Decoder {
return dec
}

func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
func jsonUnmarshal(buf []byte, typ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand All @@ -46,18 +63,17 @@ func jsonUnmarshal(buf []byte, typ Type, p *AttributePath) (Value, error) {
case typ.Is(Bool):
return jsonUnmarshalBool(buf, typ, p)
case typ.Is(DynamicPseudoType):
return jsonUnmarshalDynamicPseudoType(buf, typ, p)
return jsonUnmarshalDynamicPseudoType(buf, typ, p, opts)
case typ.Is(List{}):
return jsonUnmarshalList(buf, typ.(List).ElementType, p)
return jsonUnmarshalList(buf, typ.(List).ElementType, p, opts)
case typ.Is(Set{}):
return jsonUnmarshalSet(buf, typ.(Set).ElementType, p)

return jsonUnmarshalSet(buf, typ.(Set).ElementType, p, opts)
case typ.Is(Map{}):
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p)
return jsonUnmarshalMap(buf, typ.(Map).ElementType, p, opts)
case typ.Is(Tuple{}):
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p)
return jsonUnmarshalTuple(buf, typ.(Tuple).ElementTypes, p, opts)
case typ.Is(Object{}):
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p)
return jsonUnmarshalObject(buf, typ.(Object).AttributeTypes, p, opts)
}
return Value{}, p.NewErrorf("unknown type %s", typ)
}
Expand Down Expand Up @@ -140,7 +156,7 @@ func jsonUnmarshalBool(buf []byte, _ Type, p *AttributePath) (Value, error) {
return Value{}, p.NewErrorf("unsupported type %T sent as %s", tok, Bool)
}

func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value, error) {
func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
if err != nil {
Expand Down Expand Up @@ -190,10 +206,10 @@ func jsonUnmarshalDynamicPseudoType(buf []byte, _ Type, p *AttributePath) (Value
if valBody == nil {
return Value{}, p.NewErrorf("missing value in dynamically-typed value")
}
return jsonUnmarshal(valBody, t, p)
return jsonUnmarshal(valBody, t, p, opts)
}

func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -227,7 +243,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -254,7 +270,7 @@ func jsonUnmarshalList(buf []byte, elementType Type, p *AttributePath) (Value, e
}, vals), nil
}

func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -284,7 +300,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -310,7 +326,7 @@ func jsonUnmarshalSet(buf []byte, elementType Type, p *AttributePath) (Value, er
}, vals), nil
}

func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error) {
func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -341,7 +357,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -360,7 +376,7 @@ func jsonUnmarshalMap(buf []byte, attrType Type, p *AttributePath) (Value, error
}, vals), nil
}

func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Value, error) {
func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath, opts ValueFromJSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -398,7 +414,7 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, elementType, innerPath)
val, err := jsonUnmarshal(rawVal, elementType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand All @@ -422,7 +438,9 @@ func jsonUnmarshalTuple(buf []byte, elementTypes []Type, p *AttributePath) (Valu
}, vals), nil
}

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

tok, err := dec.Token()
Expand All @@ -446,6 +464,12 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
}
attrType, ok := attrTypes[key]
if !ok {
if opts.IgnoreUndefinedAttributes {
// We are trying to ignore the key and value of any unsupported attribute.
_ = dec.Decode(new(json.RawMessage))
continue
}

return Value{}, innerPath.NewErrorf("unsupported attribute %q", key)
}
innerPath = p.WithAttributeName(key)
Expand All @@ -455,7 +479,7 @@ func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath
if err != nil {
return Value{}, innerPath.NewErrorf("error decoding value: %w", err)
}
val, err := jsonUnmarshal(rawVal, attrType, innerPath)
val, err := jsonUnmarshal(rawVal, attrType, innerPath, opts)
if err != nil {
return Value{}, err
}
Expand Down

0 comments on commit 71dfab6

Please sign in to comment.