Skip to content

Commit e339254

Browse files
authored
tfprotov5+tfprotov6: Add DynamicValue type IsNull method (#306)
Reference: #305
1 parent 18c198e commit e339254

File tree

6 files changed

+310
-0
lines changed

6 files changed

+310
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'tfprotov5: Added `DynamicValue` type `IsNull` method, which enables checking
3+
if the value is null without type information and fully decoding underlying data'
4+
time: 2023-06-27T12:58:06.917152-04:00
5+
custom:
6+
Issue: "305"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ENHANCEMENTS
2+
body: 'tfprotov6: Added `DynamicValue` type `IsNull` method, which enables checking
3+
if the value is null without type information and fully decoding underlying data'
4+
time: 2023-06-27T12:59:12.941648-04:00
5+
custom:
6+
Issue: "305"

tfprotov5/dynamic_value.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
package tfprotov5
55

66
import (
7+
"bytes"
8+
"encoding/json"
79
"errors"
10+
"fmt"
811

912
"github.com/hashicorp/terraform-plugin-go/tftypes"
13+
msgpack "github.com/vmihailenco/msgpack/v5"
14+
"github.com/vmihailenco/msgpack/v5/msgpcode"
1015
)
1116

1217
// ErrUnknownDynamicValueType is returned when a DynamicValue has no MsgPack or
@@ -47,6 +52,43 @@ type DynamicValue struct {
4752
JSON []byte
4853
}
4954

55+
// IsNull returns true if the DynamicValue represents a null value based on the
56+
// underlying JSON or MessagePack data.
57+
func (d DynamicValue) IsNull() (bool, error) {
58+
if d.JSON != nil {
59+
decoder := json.NewDecoder(bytes.NewReader(d.JSON))
60+
token, err := decoder.Token()
61+
62+
if err != nil {
63+
return false, fmt.Errorf("unable to read DynamicValue JSON token: %w", err)
64+
}
65+
66+
if token != nil {
67+
return false, nil
68+
}
69+
70+
return true, nil
71+
}
72+
73+
if d.MsgPack != nil {
74+
decoder := msgpack.NewDecoder(bytes.NewReader(d.MsgPack))
75+
code, err := decoder.PeekCode()
76+
77+
if err != nil {
78+
return false, fmt.Errorf("unable to read DynamicValue MsgPack code: %w", err)
79+
}
80+
81+
// Extensions are considered unknown
82+
if msgpcode.IsExt(code) || code != msgpcode.Nil {
83+
return false, nil
84+
}
85+
86+
return true, nil
87+
}
88+
89+
return false, fmt.Errorf("unable to read DynamicValue: %w", ErrUnknownDynamicValueType)
90+
}
91+
5092
// Unmarshal returns a `tftypes.Value` that represents the information
5193
// contained in the DynamicValue in an easy-to-interact-with way. It is the
5294
// main purpose of the DynamicValue type, and is how provider developers should

tfprotov5/dynamic_value_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfprotov5_test
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
12+
"github.com/hashicorp/terraform-plugin-go/tftypes"
13+
)
14+
15+
func TestDynamicValueIsNull(t *testing.T) {
16+
t.Parallel()
17+
18+
testCases := map[string]struct {
19+
dynamicValue tfprotov5.DynamicValue
20+
expected bool
21+
expectedError error
22+
}{
23+
"empty-dynamic-value": {
24+
dynamicValue: tfprotov5.DynamicValue{},
25+
expected: false,
26+
expectedError: fmt.Errorf("unable to read DynamicValue: DynamicValue had no JSON or msgpack data set"),
27+
},
28+
"null": {
29+
dynamicValue: testNewDynamicValueMust(t,
30+
tftypes.Object{
31+
AttributeTypes: map[string]tftypes.Type{
32+
"test_string_attribute": tftypes.String,
33+
},
34+
},
35+
tftypes.NewValue(
36+
tftypes.Object{
37+
AttributeTypes: map[string]tftypes.Type{
38+
"test_string_attribute": tftypes.String,
39+
},
40+
},
41+
nil,
42+
),
43+
),
44+
expected: true,
45+
},
46+
"known": {
47+
dynamicValue: testNewDynamicValueMust(t,
48+
tftypes.Object{
49+
AttributeTypes: map[string]tftypes.Type{
50+
"test_string_attribute": tftypes.String,
51+
},
52+
},
53+
tftypes.NewValue(
54+
tftypes.Object{
55+
AttributeTypes: map[string]tftypes.Type{
56+
"test_string_attribute": tftypes.String,
57+
},
58+
},
59+
map[string]tftypes.Value{
60+
"test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"),
61+
},
62+
),
63+
),
64+
expected: false,
65+
},
66+
}
67+
68+
for name, testCase := range testCases {
69+
name, testCase := name, testCase
70+
71+
t.Run(name, func(t *testing.T) {
72+
t.Parallel()
73+
74+
got, err := testCase.dynamicValue.IsNull()
75+
76+
if err != nil {
77+
if testCase.expectedError == nil {
78+
t.Fatalf("wanted no error, got error: %s", err)
79+
}
80+
81+
if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
82+
t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error())
83+
}
84+
}
85+
86+
if err == nil && testCase.expectedError != nil {
87+
t.Fatalf("got no error, wanted err: %s", testCase.expectedError)
88+
}
89+
90+
if got != testCase.expected {
91+
t.Errorf("expected %t, got %t", testCase.expected, got)
92+
}
93+
})
94+
}
95+
}
96+
97+
func testNewDynamicValueMust(t *testing.T, typ tftypes.Type, value tftypes.Value) tfprotov5.DynamicValue {
98+
t.Helper()
99+
100+
dynamicValue, err := tfprotov5.NewDynamicValue(typ, value)
101+
102+
if err != nil {
103+
t.Fatalf("unable to create DynamicValue: %s", err)
104+
}
105+
106+
return dynamicValue
107+
}

tfprotov6/dynamic_value.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
package tfprotov6
55

66
import (
7+
"bytes"
8+
"encoding/json"
79
"errors"
10+
"fmt"
811

912
"github.com/hashicorp/terraform-plugin-go/tftypes"
13+
msgpack "github.com/vmihailenco/msgpack/v5"
14+
"github.com/vmihailenco/msgpack/v5/msgpcode"
1015
)
1116

1217
// ErrUnknownDynamicValueType is returned when a DynamicValue has no MsgPack or
@@ -47,6 +52,43 @@ type DynamicValue struct {
4752
JSON []byte
4853
}
4954

55+
// IsNull returns true if the DynamicValue represents a null value based on the
56+
// underlying JSON or MessagePack data.
57+
func (d DynamicValue) IsNull() (bool, error) {
58+
if d.JSON != nil {
59+
decoder := json.NewDecoder(bytes.NewReader(d.JSON))
60+
token, err := decoder.Token()
61+
62+
if err != nil {
63+
return false, fmt.Errorf("unable to read DynamicValue JSON token: %w", err)
64+
}
65+
66+
if token != nil {
67+
return false, nil
68+
}
69+
70+
return true, nil
71+
}
72+
73+
if d.MsgPack != nil {
74+
decoder := msgpack.NewDecoder(bytes.NewReader(d.MsgPack))
75+
code, err := decoder.PeekCode()
76+
77+
if err != nil {
78+
return false, fmt.Errorf("unable to read DynamicValue MsgPack code: %w", err)
79+
}
80+
81+
// Extensions are considered unknown
82+
if msgpcode.IsExt(code) || code != msgpcode.Nil {
83+
return false, nil
84+
}
85+
86+
return true, nil
87+
}
88+
89+
return false, fmt.Errorf("unable to read DynamicValue: %w", ErrUnknownDynamicValueType)
90+
}
91+
5092
// Unmarshal returns a `tftypes.Value` that represents the information
5193
// contained in the DynamicValue in an easy-to-interact-with way. It is the
5294
// main purpose of the DynamicValue type, and is how provider developers should

tfprotov6/dynamic_value_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfprotov6_test
5+
6+
import (
7+
"fmt"
8+
"strings"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
12+
"github.com/hashicorp/terraform-plugin-go/tftypes"
13+
)
14+
15+
func TestDynamicValueIsNull(t *testing.T) {
16+
t.Parallel()
17+
18+
testCases := map[string]struct {
19+
dynamicValue tfprotov6.DynamicValue
20+
expected bool
21+
expectedError error
22+
}{
23+
"empty-dynamic-value": {
24+
dynamicValue: tfprotov6.DynamicValue{},
25+
expected: false,
26+
expectedError: fmt.Errorf("unable to read DynamicValue: DynamicValue had no JSON or msgpack data set"),
27+
},
28+
"null": {
29+
dynamicValue: testNewDynamicValueMust(t,
30+
tftypes.Object{
31+
AttributeTypes: map[string]tftypes.Type{
32+
"test_string_attribute": tftypes.String,
33+
},
34+
},
35+
tftypes.NewValue(
36+
tftypes.Object{
37+
AttributeTypes: map[string]tftypes.Type{
38+
"test_string_attribute": tftypes.String,
39+
},
40+
},
41+
nil,
42+
),
43+
),
44+
expected: true,
45+
},
46+
"known": {
47+
dynamicValue: testNewDynamicValueMust(t,
48+
tftypes.Object{
49+
AttributeTypes: map[string]tftypes.Type{
50+
"test_string_attribute": tftypes.String,
51+
},
52+
},
53+
tftypes.NewValue(
54+
tftypes.Object{
55+
AttributeTypes: map[string]tftypes.Type{
56+
"test_string_attribute": tftypes.String,
57+
},
58+
},
59+
map[string]tftypes.Value{
60+
"test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"),
61+
},
62+
),
63+
),
64+
expected: false,
65+
},
66+
}
67+
68+
for name, testCase := range testCases {
69+
name, testCase := name, testCase
70+
71+
t.Run(name, func(t *testing.T) {
72+
t.Parallel()
73+
74+
got, err := testCase.dynamicValue.IsNull()
75+
76+
if err != nil {
77+
if testCase.expectedError == nil {
78+
t.Fatalf("wanted no error, got error: %s", err)
79+
}
80+
81+
if !strings.Contains(err.Error(), testCase.expectedError.Error()) {
82+
t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error())
83+
}
84+
}
85+
86+
if err == nil && testCase.expectedError != nil {
87+
t.Fatalf("got no error, wanted err: %s", testCase.expectedError)
88+
}
89+
90+
if got != testCase.expected {
91+
t.Errorf("expected %t, got %t", testCase.expected, got)
92+
}
93+
})
94+
}
95+
}
96+
97+
func testNewDynamicValueMust(t *testing.T, typ tftypes.Type, value tftypes.Value) tfprotov6.DynamicValue {
98+
t.Helper()
99+
100+
dynamicValue, err := tfprotov6.NewDynamicValue(typ, value)
101+
102+
if err != nil {
103+
t.Fatalf("unable to create DynamicValue: %s", err)
104+
}
105+
106+
return dynamicValue
107+
}

0 commit comments

Comments
 (0)