Skip to content

Commit e3e7cf1

Browse files
author
Paddy
authored
Merge pull request #16 from hashicorp/paddy_null_values
Actually handle null values.
2 parents 5568da0 + 0e75f1f commit e3e7cf1

File tree

2 files changed

+174
-3
lines changed

2 files changed

+174
-3
lines changed

tfprotov5/tftypes/value.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,48 +45,95 @@ func (val Value) As(dst interface{}) error {
4545
if ok {
4646
return unmarshaler.UnmarshalTerraform5Type(val)
4747
}
48-
if val.IsNull() {
49-
return fmt.Errorf("unmarshaling null values is not supported")
50-
}
5148
if !val.IsKnown() {
5249
return fmt.Errorf("unmarshaling unknown values is not supported")
5350
}
5451
switch target := dst.(type) {
5552
case *string:
53+
if val.IsNull() {
54+
*target = ""
55+
return nil
56+
}
5657
v, ok := val.value.(string)
5758
if !ok {
5859
return fmt.Errorf("can't unmarshal %s into %T, expected string", val.typ, dst)
5960
}
6061
*target = v
6162
return nil
63+
case **string:
64+
if val.IsNull() {
65+
*target = nil
66+
return nil
67+
}
68+
return val.As(*target)
6269
case *big.Float:
70+
if val.IsNull() {
71+
target.Set(big.NewFloat(0))
72+
return nil
73+
}
6374
v, ok := val.value.(*big.Float)
6475
if !ok {
6576
return fmt.Errorf("can't unmarshal %s into %T, expected *big.Float", val.typ, dst)
6677
}
6778
target.Set(v)
6879
return nil
80+
case **big.Float:
81+
if val.IsNull() {
82+
*target = nil
83+
return nil
84+
}
85+
return val.As(*target)
6986
case *bool:
87+
if val.IsNull() {
88+
*target = false
89+
return nil
90+
}
7091
v, ok := val.value.(bool)
7192
if !ok {
7293
return fmt.Errorf("can't unmarshal %s into %T, expected boolean", val.typ, dst)
7394
}
7495
*target = v
7596
return nil
97+
case **bool:
98+
if val.IsNull() {
99+
*target = nil
100+
return nil
101+
}
102+
return val.As(*target)
76103
case *map[string]Value:
104+
if val.IsNull() {
105+
*target = map[string]Value{}
106+
return nil
107+
}
77108
v, ok := val.value.(map[string]Value)
78109
if !ok {
79110
return fmt.Errorf("can't unmarshal %s into %T, expected map[string]tftypes.Value", val.typ, dst)
80111
}
81112
*target = v
82113
return nil
114+
case **map[string]Value:
115+
if val.IsNull() {
116+
*target = nil
117+
return nil
118+
}
119+
return val.As(*target)
83120
case *[]Value:
121+
if val.IsNull() {
122+
*target = []Value{}
123+
return nil
124+
}
84125
v, ok := val.value.([]Value)
85126
if !ok {
86127
return fmt.Errorf("can't unmarshal %s into %T expected []tftypes.Value", val.typ, dst)
87128
}
88129
*target = v
89130
return nil
131+
case **[]Value:
132+
if val.IsNull() {
133+
*target = nil
134+
return nil
135+
}
136+
return val.As(*target)
90137
}
91138
return fmt.Errorf("can't unmarshal into %T, needs UnmarshalTerraform5Type method", dst)
92139
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package tftypes_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/terraform-plugin-go/tfprotov5/tftypes"
7+
)
8+
9+
func ExampleValue_As_string() {
10+
// Values come over the wire, usually from a DynamicValue for this
11+
// example, we're just building one inline
12+
val := tftypes.NewValue(tftypes.String, "hello, world")
13+
14+
var salutation string
15+
16+
// we need to use a pointer so we can modify the value, just like
17+
// json.Unmarshal
18+
err := val.As(&salutation)
19+
if err != nil {
20+
panic(err)
21+
}
22+
23+
fmt.Println(salutation)
24+
// Output:
25+
// hello, world
26+
}
27+
28+
func ExampleValue_As_stringNull() {
29+
type exampleResource struct {
30+
salutation string
31+
nullableSalutation *string
32+
}
33+
34+
// let's see what happens when we have a null value
35+
val := tftypes.NewValue(tftypes.String, nil)
36+
37+
var res exampleResource
38+
39+
// we can use a pointer to a variable, but the variable can't hold nil,
40+
// so we'll get the empty value. You can use this if you don't care
41+
// about null, and consider it equivalent to the empty value.
42+
err := val.As(&res.salutation)
43+
if err != nil {
44+
panic(err)
45+
}
46+
47+
// we can use a pointer to a pointer to a variable, which can hold nil,
48+
// so we'll be able to distinguish between a null and an empty string
49+
err = val.As(&res.nullableSalutation)
50+
if err != nil {
51+
panic(err)
52+
}
53+
54+
fmt.Println(res.salutation)
55+
fmt.Println(res.nullableSalutation)
56+
// Output:
57+
//
58+
// <nil>
59+
}
60+
61+
type exampleResource struct {
62+
name string
63+
suppliedName *bool // true for yes, false for no, nil for we haven't asked
64+
}
65+
66+
// fill the tftypes.Unmarshaler interface to control how As works
67+
// we want a pointer to exampleResource so we can change the properties
68+
func (e *exampleResource) UnmarshalTerraform5Type(val tftypes.Value) error {
69+
// this is an object type, so we're always going to get a
70+
// `tftypes.Value` that coerces to a map[string]tftypes.Value
71+
// as input
72+
v := map[string]tftypes.Value{}
73+
err := val.As(&v)
74+
if err != nil {
75+
return err
76+
}
77+
78+
// now that we can get to the tftypes.Value for each field,
79+
// call its As method and assign the result to the appropriate
80+
// variable.
81+
82+
err = v["name"].As(&e.name)
83+
if err != nil {
84+
return err
85+
}
86+
87+
err = v["supplied_name"].As(&e.suppliedName)
88+
if err != nil {
89+
return err
90+
}
91+
92+
return nil
93+
}
94+
95+
func ExampleValue_As_interface() {
96+
// our tftypes.Value would usually come over the wire as a
97+
// DynamicValue, but for simplicity, let's just declare one inline here
98+
val := tftypes.NewValue(tftypes.Object{
99+
AttributeTypes: map[string]tftypes.Type{
100+
"name": tftypes.String,
101+
"supplied_name": tftypes.Bool,
102+
},
103+
}, map[string]tftypes.Value{
104+
"name": tftypes.NewValue(tftypes.String, "ozymandias"),
105+
"supplied_name": tftypes.NewValue(tftypes.Bool, nil),
106+
})
107+
108+
// exampleResource has UnmarshalTerraform5Type method defined on it,
109+
// see value_example_test.go for implementation details
110+
// we'd put the function and type inline here, but apparently Go can't
111+
// have methods defined on types defined inside a function
112+
var res exampleResource
113+
114+
// call As as usual
115+
err := val.As(&res)
116+
if err != nil {
117+
panic(err)
118+
}
119+
fmt.Println(res.name)
120+
fmt.Println(res.suppliedName)
121+
// Output:
122+
// ozymandias
123+
// <nil>
124+
}

0 commit comments

Comments
 (0)