Skip to content

Commit 4398d48

Browse files
committed
improve nullable type
Signed-off-by: Ashutosh Kumar <sonasingh46@gmail.com>
1 parent 8bbf023 commit 4398d48

File tree

2 files changed

+259
-8
lines changed

2 files changed

+259
-8
lines changed

types/nullable.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,35 @@ package types
22

33
import "encoding/json"
44

5+
// Nullable type which can distinguish between an explicit `null` vs not provided
6+
// in JSON when un-marshaled to go type.
57
type Nullable[T any] struct {
6-
Value T
7-
Set bool
8-
Null bool
8+
// Value is the actual value of the field.
9+
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
915
}
1016

17+
// UnmarshalJSON implements the Unmarshaler interface.
1118
func (t *Nullable[T]) UnmarshalJSON(data []byte) error {
12-
t.Set = true
19+
t.Defined = true
1320
return json.Unmarshal(data, &t.Value)
1421
}
1522

23+
// MarshalJSON implements the Marshaler interface.
1624
func (t Nullable[T]) MarshalJSON() ([]byte, error) {
1725
return json.Marshal(t.Value)
1826
}
1927

20-
func (t *Nullable[T]) IsNullDefined() bool {
21-
return t.Set && t.Value == nil
28+
// IsNull returns true if the value is explicitly provided `null` in json
29+
func (t *Nullable[T]) IsNull() bool {
30+
return t.IsDefined() && t.Value == nil
2231
}
2332

24-
func (t *Nullable[T]) HasValue() bool {
25-
return t.Set && t.Value != nil
33+
// IsDefined returns true if the value is explicitly provided in json
34+
func (t *Nullable[T]) IsDefined() bool {
35+
return t.Defined
2636
}

types/nullable_test.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,242 @@
11
package types
2+
3+
import (
4+
"encoding/json"
5+
"github.com/stretchr/testify/assert"
6+
"testing"
7+
)
8+
9+
type SimpleString struct {
10+
Name Nullable[string] `json:"name"`
11+
}
12+
13+
func TestSimpleString_IsDefined(t *testing.T) {
14+
type testCase struct {
15+
name string
16+
jsonInput []byte
17+
wantNull bool
18+
wantDefined bool
19+
}
20+
tests := []testCase{
21+
{
22+
name: "simple object: set name to some non null value",
23+
jsonInput: []byte(`{"name":"yolo"}`),
24+
// since name field is present in JSON and is NOT null, want null to be false
25+
wantNull: false,
26+
// since name field is present in JSON, want defined to be true
27+
wantDefined: true,
28+
},
29+
30+
{
31+
name: "simple object: set name to empty string value",
32+
jsonInput: []byte(`{"name":""}`),
33+
// since name field is present in JSON and is NOT null, want null to be false
34+
wantNull: false,
35+
// since name field is present in JSON, want defined to be true
36+
wantDefined: true,
37+
},
38+
39+
{
40+
name: "simple object: set name to null value",
41+
jsonInput: []byte(`{"name":null}`),
42+
// since name field is present in JSON and is null, want null to be true
43+
wantNull: true,
44+
// since name field is present in JSON, want defined to be true
45+
wantDefined: true,
46+
},
47+
48+
{
49+
name: "simple object: do not provide name in json data",
50+
jsonInput: []byte(`{}`),
51+
// since name field is NOT present in JSON, want null to be false
52+
wantNull: false,
53+
// since name field is present in JSON, want defined to be false
54+
wantDefined: false,
55+
},
56+
}
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t1 *testing.T) {
59+
var obj SimpleString
60+
err := json.Unmarshal(tt.jsonInput, &obj)
61+
assert.NoError(t, err)
62+
assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()")
63+
assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()")
64+
})
65+
}
66+
}
67+
68+
type SimpleStringPointer struct {
69+
Name Nullable[*string] `json:"name"`
70+
}
71+
72+
func TestSimpleStringPointer_IsDefined(t *testing.T) {
73+
type testCase struct {
74+
name string
75+
jsonInput []byte
76+
wantNull bool
77+
wantDefined bool
78+
}
79+
tests := []testCase{
80+
{
81+
name: "simple object: set name to some non null value",
82+
jsonInput: []byte(`{"name":"yolo"}`),
83+
// since name field is present in JSON and is NOT null, want null to be false
84+
wantNull: false,
85+
// since name field is present in JSON, want defined to be true
86+
wantDefined: true,
87+
},
88+
89+
{
90+
name: "simple object: set name to empty string value",
91+
jsonInput: []byte(`{"name":""}`),
92+
// since name field is present in JSON and is NOT null, want null to be false
93+
wantNull: false,
94+
// since name field is present in JSON, want defined to be true
95+
wantDefined: true,
96+
},
97+
98+
{
99+
name: "simple object: set name to null value",
100+
jsonInput: []byte(`{"name":null}`),
101+
// since name field is present in JSON and is null, want null to be true
102+
wantNull: true,
103+
// since name field is present in JSON, want defined to be true
104+
wantDefined: true,
105+
},
106+
107+
{
108+
name: "simple object: do not provide name in json data",
109+
jsonInput: []byte(`{}`),
110+
// since name field is NOT present in JSON, want null to be false
111+
wantNull: false,
112+
// since name field is present in JSON, want defined to be false
113+
wantDefined: false,
114+
},
115+
}
116+
for _, tt := range tests {
117+
t.Run(tt.name, func(t1 *testing.T) {
118+
var obj SimpleStringPointer
119+
err := json.Unmarshal(tt.jsonInput, &obj)
120+
assert.NoError(t, err)
121+
assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()")
122+
assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()")
123+
})
124+
}
125+
}
126+
127+
type SimpleInt struct {
128+
ReplicaCount Nullable[int] `json:"replicaCount"`
129+
}
130+
131+
func TestSimpleInt_IsDefined(t *testing.T) {
132+
type testCase struct {
133+
name string
134+
jsonInput []byte
135+
wantNull bool
136+
wantDefined bool
137+
}
138+
tests := []testCase{
139+
{
140+
name: "simple object: set name to some non null value",
141+
jsonInput: []byte(`{"replicaCount":1}`),
142+
// since replicaCount field is present in JSON but is NOT null want null to be false
143+
wantNull: false,
144+
// since name field is present in JSON want defined to be true
145+
wantDefined: true,
146+
},
147+
148+
{
149+
name: "simple object: set name to empty value",
150+
jsonInput: []byte(`{"replicaCount":0}`),
151+
// since replicaCount field is present in JSON but is NOT null want null to be false
152+
wantNull: false,
153+
// since name field is present in JSON want defined to be true
154+
wantDefined: true,
155+
},
156+
157+
{
158+
name: "simple object: set name to null value",
159+
jsonInput: []byte(`{"replicaCount":null}`),
160+
// since replicaCount field is present in JSON and is null, want null to be true
161+
wantNull: true,
162+
// since name field is present in JSON want defined to be true
163+
wantDefined: true,
164+
},
165+
166+
{
167+
name: "simple object: do not provide name in json data",
168+
jsonInput: []byte(`{}`),
169+
// since name field is NOT present in JSON, want null to be false
170+
wantNull: false,
171+
// since name field is NOT present in JSON want defined to be false
172+
wantDefined: false,
173+
},
174+
}
175+
for _, tt := range tests {
176+
t.Run(tt.name, func(t1 *testing.T) {
177+
var obj SimpleInt
178+
err := json.Unmarshal(tt.jsonInput, &obj)
179+
assert.NoError(t, err)
180+
assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()")
181+
assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()")
182+
})
183+
}
184+
}
185+
186+
type SimpleIntPointer struct {
187+
ReplicaCount Nullable[*int] `json:"replicaCount"`
188+
}
189+
190+
func TestSimpleIntPointer_IsDefined(t *testing.T) {
191+
type testCase struct {
192+
name string
193+
jsonInput []byte
194+
wantNull bool
195+
wantDefined bool
196+
}
197+
tests := []testCase{
198+
{
199+
name: "simple object: set name to some non null value",
200+
jsonInput: []byte(`{"replicaCount":1}`),
201+
// since replicaCount field is present in JSON but is NOT null, want null false
202+
wantNull: false,
203+
// since replicaCount field is present in JSON want defined to be true
204+
wantDefined: true,
205+
},
206+
207+
{
208+
name: "simple object: set name to empty value",
209+
jsonInput: []byte(`{"replicaCount":0}`),
210+
// since replicaCount field is present in JSON but is NOT null, want null false
211+
wantNull: false,
212+
// since replicaCount field is present in JSON want defined to be true
213+
wantDefined: true,
214+
},
215+
216+
{
217+
name: "simple object: set name to null value",
218+
jsonInput: []byte(`{"replicaCount":null}`),
219+
// since replicaCount field is present in JSON and is null, want null true
220+
wantNull: true,
221+
// since replicaCount field is present in JSON want defined to be true
222+
wantDefined: true,
223+
},
224+
225+
{
226+
name: "simple object: do not provide name in json data",
227+
jsonInput: []byte(`{}`),
228+
// since replicaCount field is NOT present in JSON, want null false
229+
wantNull: false,
230+
// since replicaCount field is NOT present in JSON want defined to be false
231+
wantDefined: false,
232+
},
233+
}
234+
for _, tt := range tests {
235+
t.Run(tt.name, func(t1 *testing.T) {
236+
var obj SimpleIntPointer
237+
err := json.Unmarshal(tt.jsonInput, &obj)
238+
assert.NoError(t, err)
239+
assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()")
240+
})
241+
}
242+
}

0 commit comments

Comments
 (0)