Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ignoring undefined attributes during unmarshalling #213

Merged
merged 4 commits into from Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
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 {
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
JSONOpts JSONOpts
}

// JSONOpts contains options that can be used to modify the behaviour when
// unmarshalling JSON.
type JSONOpts struct {
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
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.
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
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": {
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
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)
}
})
}
}