Skip to content

Commit

Permalink
Adding alternative implementation of Unmarshal which allows supplying
Browse files Browse the repository at this point in the history
of opts which can be used to modify the unmarshalling behaviour,
such as ignoring undefined attributes (#212)
  • Loading branch information
bendbennett committed Jul 27, 2022
1 parent a325d5b commit cff7cc2
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 21 deletions.
12 changes: 12 additions & 0 deletions tfprotov5/state.go
Expand Up @@ -77,3 +77,15 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}

// 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 tftypes.UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.JsonOpts) //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
}
12 changes: 12 additions & 0 deletions tfprotov6/state.go
Expand Up @@ -77,3 +77,15 @@ func (s RawState) Unmarshal(typ tftypes.Type) (tftypes.Value, error) {
}
return tftypes.Value{}, ErrUnknownRawStateType
}

// 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 tftypes.UnmarshalOpts) (tftypes.Value, error) {
if s.JSON != nil {
return tftypes.ValueFromJSONWithOpts(s.JSON, typ, opts.JsonOpts) //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
}
72 changes: 51 additions & 21 deletions tftypes/value_json.go
Expand Up @@ -16,7 +16,28 @@ 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(), JSONOpts{})
}

// 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 {
JsonOpts JSONOpts
}

// JSONOpts contains options that can be used to modify the behaviour when
// unmarshalling JSON.
type JSONOpts struct {
IgnoreUndefinedAttributes bool
}

// ValueFromJSONWithOpts is identical to ValueFromJSON with the exception that it
// accepts JSONOpts 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 JSONOpts) (Value, error) {
return jsonUnmarshal(data, typ, NewAttributePath(), opts)
}

func jsonByteDecoder(buf []byte) *json.Decoder {
Expand All @@ -26,7 +47,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 JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand All @@ -46,18 +67,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 +160,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 JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)
tok, err := dec.Token()
if err != nil {
Expand Down Expand Up @@ -190,10 +210,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 JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -227,7 +247,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 +274,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 JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -284,7 +304,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 +330,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 JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -341,7 +361,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 +380,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 JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand Down Expand Up @@ -398,7 +418,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 +442,11 @@ 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.
// 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.
func jsonUnmarshalObject(buf []byte, attrTypes map[string]Type, p *AttributePath, opts JSONOpts) (Value, error) {
dec := jsonByteDecoder(buf)

tok, err := dec.Token()
Expand All @@ -446,6 +470,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 +485,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
44 changes: 44 additions & 0 deletions tftypes/value_json_test.go
Expand Up @@ -380,3 +380,47 @@ func TestValueFromJSON(t *testing.T) {
})
}
}

func TestValueFromJSONWithOpts(t *testing.T) {
t.Parallel()
type testCase struct {
value Value
typ Type
json string
}
tests := map[string]testCase{
"object-with-missing-attribute": {
value: NewValue(Object{
AttributeTypes: map[string]Type{
"bool": Bool,
"number": Number,
},
}, map[string]Value{
"bool": NewValue(Bool, true),
"number": NewValue(Number, big.NewFloat(0)),
}),
typ: Object{
AttributeTypes: map[string]Type{
"bool": Bool,
"number": Number,
},
},
json: `{"bool":true,"number":0,"unknown":"whatever"}`,
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()
val, err := ValueFromJSONWithOpts([]byte(test.json), test.typ, JSONOpts{
IgnoreUndefinedAttributes: true,
})
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)
}
})
}
}

0 comments on commit cff7cc2

Please sign in to comment.