Skip to content

Commit

Permalink
tfprotov5+tfprotov6: Add DynamicValue type IsNull method (#306)
Browse files Browse the repository at this point in the history
Reference: #305
  • Loading branch information
bflad authored Jun 28, 2023
1 parent 18c198e commit e339254
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20230627-125806.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: ENHANCEMENTS
body: 'tfprotov5: Added `DynamicValue` type `IsNull` method, which enables checking
if the value is null without type information and fully decoding underlying data'
time: 2023-06-27T12:58:06.917152-04:00
custom:
Issue: "305"
6 changes: 6 additions & 0 deletions .changes/unreleased/ENHANCEMENTS-20230627-125912.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: ENHANCEMENTS
body: 'tfprotov6: Added `DynamicValue` type `IsNull` method, which enables checking
if the value is null without type information and fully decoding underlying data'
time: 2023-06-27T12:59:12.941648-04:00
custom:
Issue: "305"
42 changes: 42 additions & 0 deletions tfprotov5/dynamic_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
package tfprotov5

import (
"bytes"
"encoding/json"
"errors"
"fmt"

"github.com/hashicorp/terraform-plugin-go/tftypes"
msgpack "github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
)

// ErrUnknownDynamicValueType is returned when a DynamicValue has no MsgPack or
Expand Down Expand Up @@ -47,6 +52,43 @@ type DynamicValue struct {
JSON []byte
}

// IsNull returns true if the DynamicValue represents a null value based on the
// underlying JSON or MessagePack data.
func (d DynamicValue) IsNull() (bool, error) {
if d.JSON != nil {
decoder := json.NewDecoder(bytes.NewReader(d.JSON))
token, err := decoder.Token()

if err != nil {
return false, fmt.Errorf("unable to read DynamicValue JSON token: %w", err)
}

if token != nil {
return false, nil
}

return true, nil
}

if d.MsgPack != nil {
decoder := msgpack.NewDecoder(bytes.NewReader(d.MsgPack))
code, err := decoder.PeekCode()

if err != nil {
return false, fmt.Errorf("unable to read DynamicValue MsgPack code: %w", err)
}

// Extensions are considered unknown
if msgpcode.IsExt(code) || code != msgpcode.Nil {
return false, nil
}

return true, nil
}

return false, fmt.Errorf("unable to read DynamicValue: %w", ErrUnknownDynamicValueType)
}

// Unmarshal returns a `tftypes.Value` that represents the information
// contained in the DynamicValue in an easy-to-interact-with way. It is the
// main purpose of the DynamicValue type, and is how provider developers should
Expand Down
107 changes: 107 additions & 0 deletions tfprotov5/dynamic_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfprotov5_test

import (
"fmt"
"strings"
"testing"

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

func TestDynamicValueIsNull(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
dynamicValue tfprotov5.DynamicValue
expected bool
expectedError error
}{
"empty-dynamic-value": {
dynamicValue: tfprotov5.DynamicValue{},
expected: false,
expectedError: fmt.Errorf("unable to read DynamicValue: DynamicValue had no JSON or msgpack data set"),
},
"null": {
dynamicValue: testNewDynamicValueMust(t,
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
nil,
),
),
expected: true,
},
"known": {
dynamicValue: testNewDynamicValueMust(t,
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
map[string]tftypes.Value{
"test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"),
},
),
),
expected: false,
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got, err := testCase.dynamicValue.IsNull()

if err != nil {
if testCase.expectedError == nil {
t.Fatalf("wanted no error, got error: %s", err)
}

if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error())
}
}

if err == nil && testCase.expectedError != nil {
t.Fatalf("got no error, wanted err: %s", testCase.expectedError)
}

if got != testCase.expected {
t.Errorf("expected %t, got %t", testCase.expected, got)
}
})
}
}

func testNewDynamicValueMust(t *testing.T, typ tftypes.Type, value tftypes.Value) tfprotov5.DynamicValue {
t.Helper()

dynamicValue, err := tfprotov5.NewDynamicValue(typ, value)

if err != nil {
t.Fatalf("unable to create DynamicValue: %s", err)
}

return dynamicValue
}
42 changes: 42 additions & 0 deletions tfprotov6/dynamic_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
package tfprotov6

import (
"bytes"
"encoding/json"
"errors"
"fmt"

"github.com/hashicorp/terraform-plugin-go/tftypes"
msgpack "github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
)

// ErrUnknownDynamicValueType is returned when a DynamicValue has no MsgPack or
Expand Down Expand Up @@ -47,6 +52,43 @@ type DynamicValue struct {
JSON []byte
}

// IsNull returns true if the DynamicValue represents a null value based on the
// underlying JSON or MessagePack data.
func (d DynamicValue) IsNull() (bool, error) {
if d.JSON != nil {
decoder := json.NewDecoder(bytes.NewReader(d.JSON))
token, err := decoder.Token()

if err != nil {
return false, fmt.Errorf("unable to read DynamicValue JSON token: %w", err)
}

if token != nil {
return false, nil
}

return true, nil
}

if d.MsgPack != nil {
decoder := msgpack.NewDecoder(bytes.NewReader(d.MsgPack))
code, err := decoder.PeekCode()

if err != nil {
return false, fmt.Errorf("unable to read DynamicValue MsgPack code: %w", err)
}

// Extensions are considered unknown
if msgpcode.IsExt(code) || code != msgpcode.Nil {
return false, nil
}

return true, nil
}

return false, fmt.Errorf("unable to read DynamicValue: %w", ErrUnknownDynamicValueType)
}

// Unmarshal returns a `tftypes.Value` that represents the information
// contained in the DynamicValue in an easy-to-interact-with way. It is the
// main purpose of the DynamicValue type, and is how provider developers should
Expand Down
107 changes: 107 additions & 0 deletions tfprotov6/dynamic_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfprotov6_test

import (
"fmt"
"strings"
"testing"

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

func TestDynamicValueIsNull(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
dynamicValue tfprotov6.DynamicValue
expected bool
expectedError error
}{
"empty-dynamic-value": {
dynamicValue: tfprotov6.DynamicValue{},
expected: false,
expectedError: fmt.Errorf("unable to read DynamicValue: DynamicValue had no JSON or msgpack data set"),
},
"null": {
dynamicValue: testNewDynamicValueMust(t,
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
nil,
),
),
expected: true,
},
"known": {
dynamicValue: testNewDynamicValueMust(t,
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test_string_attribute": tftypes.String,
},
},
map[string]tftypes.Value{
"test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"),
},
),
),
expected: false,
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

got, err := testCase.dynamicValue.IsNull()

if err != nil {
if testCase.expectedError == nil {
t.Fatalf("wanted no error, got error: %s", err)
}

if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error())
}
}

if err == nil && testCase.expectedError != nil {
t.Fatalf("got no error, wanted err: %s", testCase.expectedError)
}

if got != testCase.expected {
t.Errorf("expected %t, got %t", testCase.expected, got)
}
})
}
}

func testNewDynamicValueMust(t *testing.T, typ tftypes.Type, value tftypes.Value) tfprotov6.DynamicValue {
t.Helper()

dynamicValue, err := tfprotov6.NewDynamicValue(typ, value)

if err != nil {
t.Fatalf("unable to create DynamicValue: %s", err)
}

return dynamicValue
}

0 comments on commit e339254

Please sign in to comment.