Skip to content

Commit 6e6803b

Browse files
committed
make optional type
Signed-off-by: Ashutosh Kumar <sonasingh46@gmail.com>
1 parent 4398d48 commit 6e6803b

File tree

2 files changed

+44
-59
lines changed

2 files changed

+44
-59
lines changed

types/nullable.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ 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.
7-
type Nullable[T any] struct {
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 {
88
// Value is the actual value of the field.
9-
Value *T
9+
Value T
1010
// Defined indicates that the field was provided in JSON if it is true.
1111
// If a field is not provided in JSON, then `Defined` is false and `Value`
1212
// contains the `zero-value` of the field type e.g "" for string,
@@ -15,22 +15,17 @@ type Nullable[T any] struct {
1515
}
1616

1717
// UnmarshalJSON implements the Unmarshaler interface.
18-
func (t *Nullable[T]) UnmarshalJSON(data []byte) error {
18+
func (t *Optional[T]) UnmarshalJSON(data []byte) error {
1919
t.Defined = true
2020
return json.Unmarshal(data, &t.Value)
2121
}
2222

2323
// MarshalJSON implements the Marshaler interface.
24-
func (t Nullable[T]) MarshalJSON() ([]byte, error) {
24+
func (t Optional[T]) MarshalJSON() ([]byte, error) {
2525
return json.Marshal(t.Value)
2626
}
2727

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
31-
}
32-
3328
// IsDefined returns true if the value is explicitly provided in json
34-
func (t *Nullable[T]) IsDefined() bool {
29+
func (t *Optional[T]) IsDefined() bool {
3530
return t.Defined
3631
}

types/nullable_test.go

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import (
77
)
88

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

1314
func TestSimpleString_IsDefined(t *testing.T) {
@@ -21,35 +22,31 @@ func TestSimpleString_IsDefined(t *testing.T) {
2122
{
2223
name: "simple object: set name to some non null value",
2324
jsonInput: []byte(`{"name":"yolo"}`),
24-
// since name field is present in JSON and is NOT null, want null to be false
25-
wantNull: false,
2625
// since name field is present in JSON, want defined to be true
2726
wantDefined: true,
2827
},
2928

3029
{
3130
name: "simple object: set name to empty string value",
3231
jsonInput: []byte(`{"name":""}`),
33-
// since name field is present in JSON and is NOT null, want null to be false
34-
wantNull: false,
3532
// since name field is present in JSON, want defined to be true
3633
wantDefined: true,
3734
},
3835

3936
{
4037
name: "simple object: set name to null value",
4138
jsonInput: []byte(`{"name":null}`),
42-
// since name field is present in JSON and is null, want null to be true
43-
wantNull: true,
4439
// since name field is present in JSON, want defined to be true
4540
wantDefined: true,
4641
},
47-
42+
/*
43+
Note that it is not possible to differentiate b/w `{"name":""}` and `{"name":null}`
44+
as both will result in defined to be true but the value will always be the zero
45+
value and hence cannot tell which one was null
46+
*/
4847
{
4948
name: "simple object: do not provide name in json data",
5049
jsonInput: []byte(`{}`),
51-
// since name field is NOT present in JSON, want null to be false
52-
wantNull: false,
5350
// since name field is present in JSON, want defined to be false
5451
wantDefined: false,
5552
},
@@ -59,56 +56,56 @@ func TestSimpleString_IsDefined(t *testing.T) {
5956
var obj SimpleString
6057
err := json.Unmarshal(tt.jsonInput, &obj)
6158
assert.NoError(t, err)
62-
assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()")
6359
assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()")
60+
6461
})
6562
}
6663
}
6764

6865
type SimpleStringPointer struct {
69-
Name Nullable[*string] `json:"name"`
66+
// can decide if it was provided with `null` value in json
67+
Name Optional[*string] `json:"name"`
7068
}
7169

7270
func TestSimpleStringPointer_IsDefined(t *testing.T) {
7371
type testCase struct {
7472
name string
7573
jsonInput []byte
76-
wantNull bool
7774
wantDefined bool
75+
wantNull bool
7876
}
7977
tests := []testCase{
8078
{
8179
name: "simple object: set name to some non null value",
8280
jsonInput: []byte(`{"name":"yolo"}`),
83-
// since name field is present in JSON and is NOT null, want null to be false
84-
wantNull: false,
8581
// since name field is present in JSON, want defined to be true
8682
wantDefined: true,
8783
},
8884

8985
{
9086
name: "simple object: set name to empty string value",
9187
jsonInput: []byte(`{"name":""}`),
92-
// since name field is present in JSON and is NOT null, want null to be false
93-
wantNull: false,
9488
// since name field is present in JSON, want defined to be true
9589
wantDefined: true,
9690
},
9791

9892
{
9993
name: "simple object: set name to null value",
10094
jsonInput: []byte(`{"name":null}`),
101-
// since name field is present in JSON and is null, want null to be true
102-
wantNull: true,
10395
// since name field is present in JSON, want defined to be true
10496
wantDefined: true,
97+
wantNull: true,
10598
},
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+
*/
106105

107106
{
108107
name: "simple object: do not provide name in json data",
109108
jsonInput: []byte(`{}`),
110-
// since name field is NOT present in JSON, want null to be false
111-
wantNull: false,
112109
// since name field is present in JSON, want defined to be false
113110
wantDefined: false,
114111
},
@@ -118,56 +115,51 @@ func TestSimpleStringPointer_IsDefined(t *testing.T) {
118115
var obj SimpleStringPointer
119116
err := json.Unmarshal(tt.jsonInput, &obj)
120117
assert.NoError(t, err)
121-
assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()")
122118
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")
123124
})
124125
}
125126
}
126127

127128
type SimpleInt struct {
128-
ReplicaCount Nullable[int] `json:"replicaCount"`
129+
// cannot decide if it was provided with `null` value in json
130+
ReplicaCount Optional[int] `json:"replicaCount"`
129131
}
130132

131133
func TestSimpleInt_IsDefined(t *testing.T) {
132134
type testCase struct {
133135
name string
134136
jsonInput []byte
135-
wantNull bool
136137
wantDefined bool
137138
}
138139
tests := []testCase{
139140
{
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
141+
name: "simple object: set name to some non null value",
142+
jsonInput: []byte(`{"replicaCount":1}`),
145143
wantDefined: true,
146144
},
147145

148146
{
149147
name: "simple object: set name to empty value",
150148
jsonInput: []byte(`{"replicaCount":0}`),
151-
// since replicaCount field is present in JSON but is NOT null want null to be false
152-
wantNull: false,
153149
// since name field is present in JSON want defined to be true
154150
wantDefined: true,
155151
},
156152

157153
{
158154
name: "simple object: set name to null value",
159155
jsonInput: []byte(`{"replicaCount":null}`),
160-
// since replicaCount field is present in JSON and is null, want null to be true
161-
wantNull: true,
162156
// since name field is present in JSON want defined to be true
163157
wantDefined: true,
164158
},
165159

166160
{
167161
name: "simple object: do not provide name in json data",
168162
jsonInput: []byte(`{}`),
169-
// since name field is NOT present in JSON, want null to be false
170-
wantNull: false,
171163
// since name field is NOT present in JSON want defined to be false
172164
wantDefined: false,
173165
},
@@ -177,56 +169,49 @@ func TestSimpleInt_IsDefined(t *testing.T) {
177169
var obj SimpleInt
178170
err := json.Unmarshal(tt.jsonInput, &obj)
179171
assert.NoError(t, err)
180-
assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()")
181172
assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()")
182173
})
183174
}
184175
}
185176

186177
type SimpleIntPointer struct {
187-
ReplicaCount Nullable[*int] `json:"replicaCount"`
178+
// can decide if it was provided with `null` value in json
179+
ReplicaCount Optional[*int] `json:"replicaCount"`
188180
}
189181

190182
func TestSimpleIntPointer_IsDefined(t *testing.T) {
191183
type testCase struct {
192184
name string
193185
jsonInput []byte
194-
wantNull bool
195186
wantDefined bool
187+
wantNull bool
196188
}
197189
tests := []testCase{
198190
{
199191
name: "simple object: set name to some non null value",
200192
jsonInput: []byte(`{"replicaCount":1}`),
201-
// since replicaCount field is present in JSON but is NOT null, want null false
202-
wantNull: false,
203193
// since replicaCount field is present in JSON want defined to be true
204194
wantDefined: true,
205195
},
206196

207197
{
208198
name: "simple object: set name to empty value",
209199
jsonInput: []byte(`{"replicaCount":0}`),
210-
// since replicaCount field is present in JSON but is NOT null, want null false
211-
wantNull: false,
212200
// since replicaCount field is present in JSON want defined to be true
213201
wantDefined: true,
214202
},
215203

216204
{
217205
name: "simple object: set name to null value",
218206
jsonInput: []byte(`{"replicaCount":null}`),
219-
// since replicaCount field is present in JSON and is null, want null true
220-
wantNull: true,
221207
// since replicaCount field is present in JSON want defined to be true
222208
wantDefined: true,
209+
wantNull: true,
223210
},
224211

225212
{
226213
name: "simple object: do not provide name in json data",
227214
jsonInput: []byte(`{}`),
228-
// since replicaCount field is NOT present in JSON, want null false
229-
wantNull: false,
230215
// since replicaCount field is NOT present in JSON want defined to be false
231216
wantDefined: false,
232217
},
@@ -236,7 +221,12 @@ func TestSimpleIntPointer_IsDefined(t *testing.T) {
236221
var obj SimpleIntPointer
237222
err := json.Unmarshal(tt.jsonInput, &obj)
238223
assert.NoError(t, err)
239-
assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()")
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")
240230
})
241231
}
242232
}

0 commit comments

Comments
 (0)