Skip to content

Commit 34d6233

Browse files
committed
add nullable type
Signed-off-by: Ashutosh Kumar <sonasingh46@gmail.com>
1 parent 6e6803b commit 34d6233

File tree

2 files changed

+68
-130
lines changed

2 files changed

+68
-130
lines changed

types/nullable.go

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,47 @@
11
package types
22

3-
import "encoding/json"
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
)
48

5-
// Optional type which can help distinguish between if a value was explicitly
6-
// provided in JSON or not
7-
type Optional[T any] struct {
8-
// Value is the actual value of the field.
9+
// nullBytes is a JSON null literal
10+
var nullBytes = []byte("null")
11+
12+
// Nullable type which can help distinguish between if a value was explicitly
13+
// provided `null` in JSON or not
14+
type Nullable[T any] struct {
915
Value T
10-
// Defined indicates that the field was provided in JSON if it is true.
11-
// If a field is not provided in JSON, then `Defined` is false and `Value`
12-
// contains the `zero-value` of the field type e.g "" for string,
13-
// 0 for int, nil for pointer etc
14-
Defined bool
16+
Null bool
1517
}
1618

1719
// UnmarshalJSON implements the Unmarshaler interface.
18-
func (t *Optional[T]) UnmarshalJSON(data []byte) error {
19-
t.Defined = true
20-
return json.Unmarshal(data, &t.Value)
20+
func (t *Nullable[T]) UnmarshalJSON(data []byte) error {
21+
if bytes.Equal(data, nullBytes) {
22+
t.Null = true
23+
return nil
24+
}
25+
if err := json.Unmarshal(data, &t.Value); err != nil {
26+
return fmt.Errorf("couldn't unmarshal JSON: %w", err)
27+
}
28+
t.Null = false
29+
return nil
2130
}
2231

2332
// MarshalJSON implements the Marshaler interface.
24-
func (t Optional[T]) MarshalJSON() ([]byte, error) {
33+
func (t Nullable[T]) MarshalJSON() ([]byte, error) {
34+
if t.IsNull() {
35+
return []byte("null"), nil
36+
}
2537
return json.Marshal(t.Value)
2638
}
2739

28-
// IsDefined returns true if the value is explicitly provided in json
29-
func (t *Optional[T]) IsDefined() bool {
30-
return t.Defined
40+
// IsNull returns true if the value is explicitly provided `null` in json
41+
func (t *Nullable[T]) IsNull() bool {
42+
return t.Null
43+
}
44+
45+
func (t *Nullable[T]) Get() (value T, null bool) {
46+
return t.Value, t.IsNull()
3147
}

types/nullable_test.go

Lines changed: 35 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,41 @@ package types
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"github.com/stretchr/testify/assert"
67
"testing"
78
)
89

910
type SimpleString struct {
1011
// cannot decide if it was provided with `null` value in json
11-
Name Optional[string] `json:"name"`
12+
Name Nullable[string] `json:"name"`
1213
}
1314

1415
func TestSimpleString_IsDefined(t *testing.T) {
1516
type testCase struct {
16-
name string
17-
jsonInput []byte
18-
wantNull bool
19-
wantDefined bool
17+
name string
18+
jsonInput []byte
19+
wantNull bool
2020
}
2121
tests := []testCase{
2222
{
2323
name: "simple object: set name to some non null value",
2424
jsonInput: []byte(`{"name":"yolo"}`),
25-
// since name field is present in JSON, want defined to be true
26-
wantDefined: true,
25+
wantNull: false,
2726
},
2827

2928
{
3029
name: "simple object: set name to empty string value",
3130
jsonInput: []byte(`{"name":""}`),
3231
// since name field is present in JSON, want defined to be true
33-
wantDefined: true,
32+
wantNull: false,
3433
},
3534

3635
{
3736
name: "simple object: set name to null value",
3837
jsonInput: []byte(`{"name":null}`),
3938
// since name field is present in JSON, want defined to be true
40-
wantDefined: true,
39+
wantNull: true,
4140
},
4241
/*
4342
Note that it is not possible to differentiate b/w `{"name":""}` and `{"name":null}`
@@ -48,185 +47,108 @@ func TestSimpleString_IsDefined(t *testing.T) {
4847
name: "simple object: do not provide name in json data",
4948
jsonInput: []byte(`{}`),
5049
// since name field is present in JSON, want defined to be false
51-
wantDefined: false,
50+
wantNull: false,
5251
},
5352
}
5453
for _, tt := range tests {
5554
t.Run(tt.name, func(t1 *testing.T) {
5655
var obj SimpleString
5756
err := json.Unmarshal(tt.jsonInput, &obj)
5857
assert.NoError(t, err)
59-
assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()")
60-
61-
})
62-
}
63-
}
64-
65-
type SimpleStringPointer struct {
66-
// can decide if it was provided with `null` value in json
67-
Name Optional[*string] `json:"name"`
68-
}
69-
70-
func TestSimpleStringPointer_IsDefined(t *testing.T) {
71-
type testCase struct {
72-
name string
73-
jsonInput []byte
74-
wantDefined bool
75-
wantNull bool
76-
}
77-
tests := []testCase{
78-
{
79-
name: "simple object: set name to some non null value",
80-
jsonInput: []byte(`{"name":"yolo"}`),
81-
// since name field is present in JSON, want defined to be true
82-
wantDefined: true,
83-
},
84-
85-
{
86-
name: "simple object: set name to empty string value",
87-
jsonInput: []byte(`{"name":""}`),
88-
// since name field is present in JSON, want defined to be true
89-
wantDefined: true,
90-
},
91-
92-
{
93-
name: "simple object: set name to null value",
94-
jsonInput: []byte(`{"name":null}`),
95-
// since name field is present in JSON, want defined to be true
96-
wantDefined: true,
97-
wantNull: true,
98-
},
99-
/*
100-
Note that it is possible to differentiate b/w `{"name":""}` and `{"name":null}`
101-
as both will result in defined to be true but the value will always be zero
102-
value for `{"name":""}` and nil for `{"name":null}`.
103-
We could tell which one was null because of (pointer) Nullable[*string]
104-
*/
105-
106-
{
107-
name: "simple object: do not provide name in json data",
108-
jsonInput: []byte(`{}`),
109-
// since name field is present in JSON, want defined to be false
110-
wantDefined: false,
111-
},
112-
}
113-
for _, tt := range tests {
114-
t.Run(tt.name, func(t1 *testing.T) {
115-
var obj SimpleStringPointer
116-
err := json.Unmarshal(tt.jsonInput, &obj)
117-
assert.NoError(t, err)
118-
assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()")
119-
gotNull := false
120-
if obj.Name.IsDefined() && obj.Name.Value == nil {
121-
gotNull = true
122-
}
123-
assert.Equalf(t, tt.wantNull, gotNull, "Null Check")
58+
assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()")
59+
fmt.Println(obj.Name.Get())
12460
})
12561
}
12662
}
12763

12864
type SimpleInt struct {
12965
// cannot decide if it was provided with `null` value in json
130-
ReplicaCount Optional[int] `json:"replicaCount"`
66+
ReplicaCount Nullable[int] `json:"replicaCount"`
13167
}
13268

13369
func TestSimpleInt_IsDefined(t *testing.T) {
13470
type testCase struct {
135-
name string
136-
jsonInput []byte
137-
wantDefined bool
71+
name string
72+
jsonInput []byte
73+
wantNull bool
13874
}
13975
tests := []testCase{
14076
{
141-
name: "simple object: set name to some non null value",
142-
jsonInput: []byte(`{"replicaCount":1}`),
143-
wantDefined: true,
77+
name: "simple object: set name to some non null value",
78+
jsonInput: []byte(`{"replicaCount":1}`),
79+
wantNull: false,
14480
},
14581

14682
{
14783
name: "simple object: set name to empty value",
14884
jsonInput: []byte(`{"replicaCount":0}`),
149-
// since name field is present in JSON want defined to be true
150-
wantDefined: true,
85+
wantNull: false,
15186
},
15287

15388
{
15489
name: "simple object: set name to null value",
15590
jsonInput: []byte(`{"replicaCount":null}`),
156-
// since name field is present in JSON want defined to be true
157-
wantDefined: true,
91+
wantNull: true,
15892
},
15993

16094
{
16195
name: "simple object: do not provide name in json data",
16296
jsonInput: []byte(`{}`),
163-
// since name field is NOT present in JSON want defined to be false
164-
wantDefined: false,
97+
wantNull: false,
16598
},
16699
}
167100
for _, tt := range tests {
168101
t.Run(tt.name, func(t1 *testing.T) {
169102
var obj SimpleInt
170103
err := json.Unmarshal(tt.jsonInput, &obj)
171104
assert.NoError(t, err)
172-
assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()")
105+
assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()")
173106
})
174107
}
175108
}
176109

177-
type SimpleIntPointer struct {
178-
// can decide if it was provided with `null` value in json
179-
ReplicaCount Optional[*int] `json:"replicaCount"`
110+
type SimplePointerInt struct {
111+
// cannot decide if it was provided with `null` value in json
112+
ReplicaCount Nullable[*int] `json:"replicaCount"`
180113
}
181114

182-
func TestSimpleIntPointer_IsDefined(t *testing.T) {
115+
func TestSimplePointerInt_IsDefined(t *testing.T) {
183116
type testCase struct {
184-
name string
185-
jsonInput []byte
186-
wantDefined bool
187-
wantNull bool
117+
name string
118+
jsonInput []byte
119+
wantNull bool
188120
}
189121
tests := []testCase{
190122
{
191123
name: "simple object: set name to some non null value",
192124
jsonInput: []byte(`{"replicaCount":1}`),
193-
// since replicaCount field is present in JSON want defined to be true
194-
wantDefined: true,
125+
wantNull: false,
195126
},
196127

197128
{
198129
name: "simple object: set name to empty value",
199130
jsonInput: []byte(`{"replicaCount":0}`),
200-
// since replicaCount field is present in JSON want defined to be true
201-
wantDefined: true,
131+
wantNull: false,
202132
},
203133

204134
{
205135
name: "simple object: set name to null value",
206136
jsonInput: []byte(`{"replicaCount":null}`),
207-
// since replicaCount field is present in JSON want defined to be true
208-
wantDefined: true,
209-
wantNull: true,
137+
wantNull: true,
210138
},
211139

212140
{
213141
name: "simple object: do not provide name in json data",
214142
jsonInput: []byte(`{}`),
215-
// since replicaCount field is NOT present in JSON want defined to be false
216-
wantDefined: false,
143+
wantNull: false,
217144
},
218145
}
219146
for _, tt := range tests {
220147
t.Run(tt.name, func(t1 *testing.T) {
221-
var obj SimpleIntPointer
148+
var obj SimplePointerInt
222149
err := json.Unmarshal(tt.jsonInput, &obj)
223150
assert.NoError(t, err)
224-
assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()")
225-
gotNull := false
226-
if obj.ReplicaCount.IsDefined() && obj.ReplicaCount.Value == nil {
227-
gotNull = true
228-
}
229-
assert.Equalf(t, tt.wantNull, gotNull, "Null Check")
151+
assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()")
230152
})
231153
}
232154
}

0 commit comments

Comments
 (0)