From 8bbf0233c3867a522fade46dce29f1b096154937 Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Wed, 20 Dec 2023 22:56:17 +0530 Subject: [PATCH 01/11] add nullable type Signed-off-by: Ashutosh Kumar --- types/nullable.go | 26 ++++++++++++++++++++++++++ types/nullable_test.go | 1 + 2 files changed, 27 insertions(+) create mode 100644 types/nullable.go create mode 100644 types/nullable_test.go diff --git a/types/nullable.go b/types/nullable.go new file mode 100644 index 00000000..6cac2d2c --- /dev/null +++ b/types/nullable.go @@ -0,0 +1,26 @@ +package types + +import "encoding/json" + +type Nullable[T any] struct { + Value T + Set bool + Null bool +} + +func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + t.Set = true + return json.Unmarshal(data, &t.Value) +} + +func (t Nullable[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(t.Value) +} + +func (t *Nullable[T]) IsNullDefined() bool { + return t.Set && t.Value == nil +} + +func (t *Nullable[T]) HasValue() bool { + return t.Set && t.Value != nil +} diff --git a/types/nullable_test.go b/types/nullable_test.go new file mode 100644 index 00000000..ab1254f4 --- /dev/null +++ b/types/nullable_test.go @@ -0,0 +1 @@ +package types From 4398d48b46b2f285e020fe7fe0a88c10de07092f Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Thu, 21 Dec 2023 20:36:03 +0530 Subject: [PATCH 02/11] improve nullable type Signed-off-by: Ashutosh Kumar --- types/nullable.go | 26 +++-- types/nullable_test.go | 241 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 8 deletions(-) diff --git a/types/nullable.go b/types/nullable.go index 6cac2d2c..84779d6b 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -2,25 +2,35 @@ package types import "encoding/json" +// Nullable type which can distinguish between an explicit `null` vs not provided +// in JSON when un-marshaled to go type. type Nullable[T any] struct { - Value T - Set bool - Null bool + // Value is the actual value of the field. + Value *T + // Defined indicates that the field was provided in JSON if it is true. + // If a field is not provided in JSON, then `Defined` is false and `Value` + // contains the `zero-value` of the field type e.g "" for string, + // 0 for int, nil for pointer etc + Defined bool } +// UnmarshalJSON implements the Unmarshaler interface. func (t *Nullable[T]) UnmarshalJSON(data []byte) error { - t.Set = true + t.Defined = true return json.Unmarshal(data, &t.Value) } +// MarshalJSON implements the Marshaler interface. func (t Nullable[T]) MarshalJSON() ([]byte, error) { return json.Marshal(t.Value) } -func (t *Nullable[T]) IsNullDefined() bool { - return t.Set && t.Value == nil +// IsNull returns true if the value is explicitly provided `null` in json +func (t *Nullable[T]) IsNull() bool { + return t.IsDefined() && t.Value == nil } -func (t *Nullable[T]) HasValue() bool { - return t.Set && t.Value != nil +// IsDefined returns true if the value is explicitly provided in json +func (t *Nullable[T]) IsDefined() bool { + return t.Defined } diff --git a/types/nullable_test.go b/types/nullable_test.go index ab1254f4..007cb64b 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -1 +1,242 @@ package types + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "testing" +) + +type SimpleString struct { + Name Nullable[string] `json:"name"` +} + +func TestSimpleString_IsDefined(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantDefined bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + // since name field is present in JSON and is NOT null, want null to be false + wantNull: false, + // since name field is present in JSON, want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + // since name field is present in JSON and is NOT null, want null to be false + wantNull: false, + // since name field is present in JSON, want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + // since name field is present in JSON and is null, want null to be true + wantNull: true, + // since name field is present in JSON, want defined to be true + wantDefined: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + // since name field is NOT present in JSON, want null to be false + wantNull: false, + // since name field is present in JSON, want defined to be false + wantDefined: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t1 *testing.T) { + var obj SimpleString + err := json.Unmarshal(tt.jsonInput, &obj) + assert.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()") + }) + } +} + +type SimpleStringPointer struct { + Name Nullable[*string] `json:"name"` +} + +func TestSimpleStringPointer_IsDefined(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantDefined bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"name":"yolo"}`), + // since name field is present in JSON and is NOT null, want null to be false + wantNull: false, + // since name field is present in JSON, want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to empty string value", + jsonInput: []byte(`{"name":""}`), + // since name field is present in JSON and is NOT null, want null to be false + wantNull: false, + // since name field is present in JSON, want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"name":null}`), + // since name field is present in JSON and is null, want null to be true + wantNull: true, + // since name field is present in JSON, want defined to be true + wantDefined: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + // since name field is NOT present in JSON, want null to be false + wantNull: false, + // since name field is present in JSON, want defined to be false + wantDefined: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t1 *testing.T) { + var obj SimpleStringPointer + err := json.Unmarshal(tt.jsonInput, &obj) + assert.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()") + }) + } +} + +type SimpleInt struct { + ReplicaCount Nullable[int] `json:"replicaCount"` +} + +func TestSimpleInt_IsDefined(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantDefined bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replicaCount":1}`), + // since replicaCount field is present in JSON but is NOT null want null to be false + wantNull: false, + // since name field is present in JSON want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replicaCount":0}`), + // since replicaCount field is present in JSON but is NOT null want null to be false + wantNull: false, + // since name field is present in JSON want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"replicaCount":null}`), + // since replicaCount field is present in JSON and is null, want null to be true + wantNull: true, + // since name field is present in JSON want defined to be true + wantDefined: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + // since name field is NOT present in JSON, want null to be false + wantNull: false, + // since name field is NOT present in JSON want defined to be false + wantDefined: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t1 *testing.T) { + var obj SimpleInt + err := json.Unmarshal(tt.jsonInput, &obj) + assert.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()") + }) + } +} + +type SimpleIntPointer struct { + ReplicaCount Nullable[*int] `json:"replicaCount"` +} + +func TestSimpleIntPointer_IsDefined(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + wantNull bool + wantDefined bool + } + tests := []testCase{ + { + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replicaCount":1}`), + // since replicaCount field is present in JSON but is NOT null, want null false + wantNull: false, + // since replicaCount field is present in JSON want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to empty value", + jsonInput: []byte(`{"replicaCount":0}`), + // since replicaCount field is present in JSON but is NOT null, want null false + wantNull: false, + // since replicaCount field is present in JSON want defined to be true + wantDefined: true, + }, + + { + name: "simple object: set name to null value", + jsonInput: []byte(`{"replicaCount":null}`), + // since replicaCount field is present in JSON and is null, want null true + wantNull: true, + // since replicaCount field is present in JSON want defined to be true + wantDefined: true, + }, + + { + name: "simple object: do not provide name in json data", + jsonInput: []byte(`{}`), + // since replicaCount field is NOT present in JSON, want null false + wantNull: false, + // since replicaCount field is NOT present in JSON want defined to be false + wantDefined: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t1 *testing.T) { + var obj SimpleIntPointer + err := json.Unmarshal(tt.jsonInput, &obj) + assert.NoError(t, err) + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + }) + } +} From 6e6803bb6b415986b21f1eb979792c11065a726a Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Fri, 22 Dec 2023 02:01:03 +0530 Subject: [PATCH 03/11] make optional type Signed-off-by: Ashutosh Kumar --- types/nullable.go | 19 ++++------ types/nullable_test.go | 84 +++++++++++++++++++----------------------- 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/types/nullable.go b/types/nullable.go index 84779d6b..32e5a651 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -2,11 +2,11 @@ package types import "encoding/json" -// Nullable type which can distinguish between an explicit `null` vs not provided -// in JSON when un-marshaled to go type. -type Nullable[T any] struct { +// Optional type which can help distinguish between if a value was explicitly +// provided in JSON or not +type Optional[T any] struct { // Value is the actual value of the field. - Value *T + Value T // Defined indicates that the field was provided in JSON if it is true. // If a field is not provided in JSON, then `Defined` is false and `Value` // contains the `zero-value` of the field type e.g "" for string, @@ -15,22 +15,17 @@ type Nullable[T any] struct { } // UnmarshalJSON implements the Unmarshaler interface. -func (t *Nullable[T]) UnmarshalJSON(data []byte) error { +func (t *Optional[T]) UnmarshalJSON(data []byte) error { t.Defined = true return json.Unmarshal(data, &t.Value) } // MarshalJSON implements the Marshaler interface. -func (t Nullable[T]) MarshalJSON() ([]byte, error) { +func (t Optional[T]) MarshalJSON() ([]byte, error) { return json.Marshal(t.Value) } -// IsNull returns true if the value is explicitly provided `null` in json -func (t *Nullable[T]) IsNull() bool { - return t.IsDefined() && t.Value == nil -} - // IsDefined returns true if the value is explicitly provided in json -func (t *Nullable[T]) IsDefined() bool { +func (t *Optional[T]) IsDefined() bool { return t.Defined } diff --git a/types/nullable_test.go b/types/nullable_test.go index 007cb64b..4bb43567 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -7,7 +7,8 @@ import ( ) type SimpleString struct { - Name Nullable[string] `json:"name"` + // cannot decide if it was provided with `null` value in json + Name Optional[string] `json:"name"` } func TestSimpleString_IsDefined(t *testing.T) { @@ -21,8 +22,6 @@ func TestSimpleString_IsDefined(t *testing.T) { { name: "simple object: set name to some non null value", jsonInput: []byte(`{"name":"yolo"}`), - // since name field is present in JSON and is NOT null, want null to be false - wantNull: false, // since name field is present in JSON, want defined to be true wantDefined: true, }, @@ -30,8 +29,6 @@ func TestSimpleString_IsDefined(t *testing.T) { { name: "simple object: set name to empty string value", jsonInput: []byte(`{"name":""}`), - // since name field is present in JSON and is NOT null, want null to be false - wantNull: false, // since name field is present in JSON, want defined to be true wantDefined: true, }, @@ -39,17 +36,17 @@ func TestSimpleString_IsDefined(t *testing.T) { { name: "simple object: set name to null value", jsonInput: []byte(`{"name":null}`), - // since name field is present in JSON and is null, want null to be true - wantNull: true, // since name field is present in JSON, want defined to be true wantDefined: true, }, - + /* + Note that it is not possible to differentiate b/w `{"name":""}` and `{"name":null}` + as both will result in defined to be true but the value will always be the zero + value and hence cannot tell which one was null + */ { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since name field is NOT present in JSON, want null to be false - wantNull: false, // since name field is present in JSON, want defined to be false wantDefined: false, }, @@ -59,29 +56,28 @@ func TestSimpleString_IsDefined(t *testing.T) { var obj SimpleString err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()") + }) } } type SimpleStringPointer struct { - Name Nullable[*string] `json:"name"` + // can decide if it was provided with `null` value in json + Name Optional[*string] `json:"name"` } func TestSimpleStringPointer_IsDefined(t *testing.T) { type testCase struct { name string jsonInput []byte - wantNull bool wantDefined bool + wantNull bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"name":"yolo"}`), - // since name field is present in JSON and is NOT null, want null to be false - wantNull: false, // since name field is present in JSON, want defined to be true wantDefined: true, }, @@ -89,8 +85,6 @@ func TestSimpleStringPointer_IsDefined(t *testing.T) { { name: "simple object: set name to empty string value", jsonInput: []byte(`{"name":""}`), - // since name field is present in JSON and is NOT null, want null to be false - wantNull: false, // since name field is present in JSON, want defined to be true wantDefined: true, }, @@ -98,17 +92,20 @@ func TestSimpleStringPointer_IsDefined(t *testing.T) { { name: "simple object: set name to null value", jsonInput: []byte(`{"name":null}`), - // since name field is present in JSON and is null, want null to be true - wantNull: true, // since name field is present in JSON, want defined to be true wantDefined: true, + wantNull: true, }, + /* + Note that it is possible to differentiate b/w `{"name":""}` and `{"name":null}` + as both will result in defined to be true but the value will always be zero + value for `{"name":""}` and nil for `{"name":null}`. + We could tell which one was null because of (pointer) Nullable[*string] + */ { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since name field is NOT present in JSON, want null to be false - wantNull: false, // since name field is present in JSON, want defined to be false wantDefined: false, }, @@ -118,38 +115,37 @@ func TestSimpleStringPointer_IsDefined(t *testing.T) { var obj SimpleStringPointer err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()") + gotNull := false + if obj.Name.IsDefined() && obj.Name.Value == nil { + gotNull = true + } + assert.Equalf(t, tt.wantNull, gotNull, "Null Check") }) } } type SimpleInt struct { - ReplicaCount Nullable[int] `json:"replicaCount"` + // cannot decide if it was provided with `null` value in json + ReplicaCount Optional[int] `json:"replicaCount"` } func TestSimpleInt_IsDefined(t *testing.T) { type testCase struct { name string jsonInput []byte - wantNull bool wantDefined bool } tests := []testCase{ { - name: "simple object: set name to some non null value", - jsonInput: []byte(`{"replicaCount":1}`), - // since replicaCount field is present in JSON but is NOT null want null to be false - wantNull: false, - // since name field is present in JSON want defined to be true + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replicaCount":1}`), wantDefined: true, }, { name: "simple object: set name to empty value", jsonInput: []byte(`{"replicaCount":0}`), - // since replicaCount field is present in JSON but is NOT null want null to be false - wantNull: false, // since name field is present in JSON want defined to be true wantDefined: true, }, @@ -157,8 +153,6 @@ func TestSimpleInt_IsDefined(t *testing.T) { { name: "simple object: set name to null value", jsonInput: []byte(`{"replicaCount":null}`), - // since replicaCount field is present in JSON and is null, want null to be true - wantNull: true, // since name field is present in JSON want defined to be true wantDefined: true, }, @@ -166,8 +160,6 @@ func TestSimpleInt_IsDefined(t *testing.T) { { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since name field is NOT present in JSON, want null to be false - wantNull: false, // since name field is NOT present in JSON want defined to be false wantDefined: false, }, @@ -177,29 +169,27 @@ func TestSimpleInt_IsDefined(t *testing.T) { var obj SimpleInt err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()") }) } } type SimpleIntPointer struct { - ReplicaCount Nullable[*int] `json:"replicaCount"` + // can decide if it was provided with `null` value in json + ReplicaCount Optional[*int] `json:"replicaCount"` } func TestSimpleIntPointer_IsDefined(t *testing.T) { type testCase struct { name string jsonInput []byte - wantNull bool wantDefined bool + wantNull bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"replicaCount":1}`), - // since replicaCount field is present in JSON but is NOT null, want null false - wantNull: false, // since replicaCount field is present in JSON want defined to be true wantDefined: true, }, @@ -207,8 +197,6 @@ func TestSimpleIntPointer_IsDefined(t *testing.T) { { name: "simple object: set name to empty value", jsonInput: []byte(`{"replicaCount":0}`), - // since replicaCount field is present in JSON but is NOT null, want null false - wantNull: false, // since replicaCount field is present in JSON want defined to be true wantDefined: true, }, @@ -216,17 +204,14 @@ func TestSimpleIntPointer_IsDefined(t *testing.T) { { name: "simple object: set name to null value", jsonInput: []byte(`{"replicaCount":null}`), - // since replicaCount field is present in JSON and is null, want null true - wantNull: true, // since replicaCount field is present in JSON want defined to be true wantDefined: true, + wantNull: true, }, { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since replicaCount field is NOT present in JSON, want null false - wantNull: false, // since replicaCount field is NOT present in JSON want defined to be false wantDefined: false, }, @@ -236,7 +221,12 @@ func TestSimpleIntPointer_IsDefined(t *testing.T) { var obj SimpleIntPointer err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()") + gotNull := false + if obj.ReplicaCount.IsDefined() && obj.ReplicaCount.Value == nil { + gotNull = true + } + assert.Equalf(t, tt.wantNull, gotNull, "Null Check") }) } } From 34d623364b3978c92e8b6b3a6f39f529ecc5ae6d Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Fri, 22 Dec 2023 19:04:18 +0530 Subject: [PATCH 04/11] add nullable type Signed-off-by: Ashutosh Kumar --- types/nullable.go | 50 +++++++++----- types/nullable_test.go | 148 ++++++++++------------------------------- 2 files changed, 68 insertions(+), 130 deletions(-) diff --git a/types/nullable.go b/types/nullable.go index 32e5a651..0fbd5042 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -1,31 +1,47 @@ package types -import "encoding/json" +import ( + "bytes" + "encoding/json" + "fmt" +) -// Optional type which can help distinguish between if a value was explicitly -// provided in JSON or not -type Optional[T any] struct { - // Value is the actual value of the field. +// nullBytes is a JSON null literal +var nullBytes = []byte("null") + +// Nullable type which can help distinguish between if a value was explicitly +// provided `null` in JSON or not +type Nullable[T any] struct { Value T - // Defined indicates that the field was provided in JSON if it is true. - // If a field is not provided in JSON, then `Defined` is false and `Value` - // contains the `zero-value` of the field type e.g "" for string, - // 0 for int, nil for pointer etc - Defined bool + Null bool } // UnmarshalJSON implements the Unmarshaler interface. -func (t *Optional[T]) UnmarshalJSON(data []byte) error { - t.Defined = true - return json.Unmarshal(data, &t.Value) +func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, nullBytes) { + t.Null = true + return nil + } + if err := json.Unmarshal(data, &t.Value); err != nil { + return fmt.Errorf("couldn't unmarshal JSON: %w", err) + } + t.Null = false + return nil } // MarshalJSON implements the Marshaler interface. -func (t Optional[T]) MarshalJSON() ([]byte, error) { +func (t Nullable[T]) MarshalJSON() ([]byte, error) { + if t.IsNull() { + return []byte("null"), nil + } return json.Marshal(t.Value) } -// IsDefined returns true if the value is explicitly provided in json -func (t *Optional[T]) IsDefined() bool { - return t.Defined +// IsNull returns true if the value is explicitly provided `null` in json +func (t *Nullable[T]) IsNull() bool { + return t.Null +} + +func (t *Nullable[T]) Get() (value T, null bool) { + return t.Value, t.IsNull() } diff --git a/types/nullable_test.go b/types/nullable_test.go index 4bb43567..82387a16 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -2,42 +2,41 @@ package types import ( "encoding/json" + "fmt" "github.com/stretchr/testify/assert" "testing" ) type SimpleString struct { // cannot decide if it was provided with `null` value in json - Name Optional[string] `json:"name"` + Name Nullable[string] `json:"name"` } func TestSimpleString_IsDefined(t *testing.T) { type testCase struct { - name string - jsonInput []byte - wantNull bool - wantDefined bool + name string + jsonInput []byte + wantNull bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"name":"yolo"}`), - // since name field is present in JSON, want defined to be true - wantDefined: true, + wantNull: false, }, { name: "simple object: set name to empty string value", jsonInput: []byte(`{"name":""}`), // since name field is present in JSON, want defined to be true - wantDefined: true, + wantNull: false, }, { name: "simple object: set name to null value", jsonInput: []byte(`{"name":null}`), // since name field is present in JSON, want defined to be true - wantDefined: true, + wantNull: true, }, /* Note that it is not possible to differentiate b/w `{"name":""}` and `{"name":null}` @@ -48,7 +47,7 @@ func TestSimpleString_IsDefined(t *testing.T) { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), // since name field is present in JSON, want defined to be false - wantDefined: false, + wantNull: false, }, } for _, tt := range tests { @@ -56,112 +55,46 @@ func TestSimpleString_IsDefined(t *testing.T) { var obj SimpleString err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()") - - }) - } -} - -type SimpleStringPointer struct { - // can decide if it was provided with `null` value in json - Name Optional[*string] `json:"name"` -} - -func TestSimpleStringPointer_IsDefined(t *testing.T) { - type testCase struct { - name string - jsonInput []byte - wantDefined bool - wantNull bool - } - tests := []testCase{ - { - name: "simple object: set name to some non null value", - jsonInput: []byte(`{"name":"yolo"}`), - // since name field is present in JSON, want defined to be true - wantDefined: true, - }, - - { - name: "simple object: set name to empty string value", - jsonInput: []byte(`{"name":""}`), - // since name field is present in JSON, want defined to be true - wantDefined: true, - }, - - { - name: "simple object: set name to null value", - jsonInput: []byte(`{"name":null}`), - // since name field is present in JSON, want defined to be true - wantDefined: true, - wantNull: true, - }, - /* - Note that it is possible to differentiate b/w `{"name":""}` and `{"name":null}` - as both will result in defined to be true but the value will always be zero - value for `{"name":""}` and nil for `{"name":null}`. - We could tell which one was null because of (pointer) Nullable[*string] - */ - - { - name: "simple object: do not provide name in json data", - jsonInput: []byte(`{}`), - // since name field is present in JSON, want defined to be false - wantDefined: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t1 *testing.T) { - var obj SimpleStringPointer - err := json.Unmarshal(tt.jsonInput, &obj) - assert.NoError(t, err) - assert.Equalf(t, tt.wantDefined, obj.Name.IsDefined(), "IsDefined()") - gotNull := false - if obj.Name.IsDefined() && obj.Name.Value == nil { - gotNull = true - } - assert.Equalf(t, tt.wantNull, gotNull, "Null Check") + assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + fmt.Println(obj.Name.Get()) }) } } type SimpleInt struct { // cannot decide if it was provided with `null` value in json - ReplicaCount Optional[int] `json:"replicaCount"` + ReplicaCount Nullable[int] `json:"replicaCount"` } func TestSimpleInt_IsDefined(t *testing.T) { type testCase struct { - name string - jsonInput []byte - wantDefined bool + name string + jsonInput []byte + wantNull bool } tests := []testCase{ { - name: "simple object: set name to some non null value", - jsonInput: []byte(`{"replicaCount":1}`), - wantDefined: true, + name: "simple object: set name to some non null value", + jsonInput: []byte(`{"replicaCount":1}`), + wantNull: false, }, { name: "simple object: set name to empty value", jsonInput: []byte(`{"replicaCount":0}`), - // since name field is present in JSON want defined to be true - wantDefined: true, + wantNull: false, }, { name: "simple object: set name to null value", jsonInput: []byte(`{"replicaCount":null}`), - // since name field is present in JSON want defined to be true - wantDefined: true, + wantNull: true, }, { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since name field is NOT present in JSON want defined to be false - wantDefined: false, + wantNull: false, }, } for _, tt := range tests { @@ -169,64 +102,53 @@ func TestSimpleInt_IsDefined(t *testing.T) { var obj SimpleInt err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()") + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") }) } } -type SimpleIntPointer struct { - // can decide if it was provided with `null` value in json - ReplicaCount Optional[*int] `json:"replicaCount"` +type SimplePointerInt struct { + // cannot decide if it was provided with `null` value in json + ReplicaCount Nullable[*int] `json:"replicaCount"` } -func TestSimpleIntPointer_IsDefined(t *testing.T) { +func TestSimplePointerInt_IsDefined(t *testing.T) { type testCase struct { - name string - jsonInput []byte - wantDefined bool - wantNull bool + name string + jsonInput []byte + wantNull bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"replicaCount":1}`), - // since replicaCount field is present in JSON want defined to be true - wantDefined: true, + wantNull: false, }, { name: "simple object: set name to empty value", jsonInput: []byte(`{"replicaCount":0}`), - // since replicaCount field is present in JSON want defined to be true - wantDefined: true, + wantNull: false, }, { name: "simple object: set name to null value", jsonInput: []byte(`{"replicaCount":null}`), - // since replicaCount field is present in JSON want defined to be true - wantDefined: true, - wantNull: true, + wantNull: true, }, { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since replicaCount field is NOT present in JSON want defined to be false - wantDefined: false, + wantNull: false, }, } for _, tt := range tests { t.Run(tt.name, func(t1 *testing.T) { - var obj SimpleIntPointer + var obj SimplePointerInt err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) - assert.Equalf(t, tt.wantDefined, obj.ReplicaCount.IsDefined(), "IsDefined()") - gotNull := false - if obj.ReplicaCount.IsDefined() && obj.ReplicaCount.Value == nil { - gotNull = true - } - assert.Equalf(t, tt.wantNull, gotNull, "Null Check") + assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") }) } } From 3e50f4c11b05e8a62a85bc4a94af63e6e2b098b2 Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Wed, 27 Dec 2023 09:17:50 +0530 Subject: [PATCH 05/11] add test cases Signed-off-by: Ashutosh Kumar --- types/nullable.go | 9 ++- types/nullable_test.go | 124 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 118 insertions(+), 15 deletions(-) diff --git a/types/nullable.go b/types/nullable.go index 0fbd5042..dd26af97 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -13,11 +13,13 @@ var nullBytes = []byte("null") // provided `null` in JSON or not type Nullable[T any] struct { Value T + Set bool Null bool } // UnmarshalJSON implements the Unmarshaler interface. func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + t.Set = true if bytes.Equal(data, nullBytes) { t.Null = true return nil @@ -32,7 +34,7 @@ func (t *Nullable[T]) UnmarshalJSON(data []byte) error { // MarshalJSON implements the Marshaler interface. func (t Nullable[T]) MarshalJSON() ([]byte, error) { if t.IsNull() { - return []byte("null"), nil + return nullBytes, nil } return json.Marshal(t.Value) } @@ -42,6 +44,11 @@ func (t *Nullable[T]) IsNull() bool { return t.Null } +// IsSet returns true if the value is provided in json +func (t *Nullable[T]) IsSet() bool { + return t.Set +} + func (t *Nullable[T]) Get() (value T, null bool) { return t.Value, t.IsNull() } diff --git a/types/nullable_test.go b/types/nullable_test.go index 82387a16..c6b246fd 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -12,42 +12,40 @@ type SimpleString struct { Name Nullable[string] `json:"name"` } -func TestSimpleString_IsDefined(t *testing.T) { +func TestSimpleString(t *testing.T) { type testCase struct { name string jsonInput []byte wantNull bool + wantSet bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"name":"yolo"}`), wantNull: false, + wantSet: true, }, { name: "simple object: set name to empty string value", jsonInput: []byte(`{"name":""}`), - // since name field is present in JSON, want defined to be true - wantNull: false, + wantNull: false, + wantSet: true, }, { name: "simple object: set name to null value", jsonInput: []byte(`{"name":null}`), - // since name field is present in JSON, want defined to be true - wantNull: true, + wantNull: true, + wantSet: true, }, - /* - Note that it is not possible to differentiate b/w `{"name":""}` and `{"name":null}` - as both will result in defined to be true but the value will always be the zero - value and hence cannot tell which one was null - */ + { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), - // since name field is present in JSON, want defined to be false - wantNull: false, + wantNull: false, + wantSet: false, }, } for _, tt := range tests { @@ -56,6 +54,7 @@ func TestSimpleString_IsDefined(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSet, obj.Name.IsSet(), "IsSet()") fmt.Println(obj.Name.Get()) }) } @@ -66,35 +65,40 @@ type SimpleInt struct { ReplicaCount Nullable[int] `json:"replicaCount"` } -func TestSimpleInt_IsDefined(t *testing.T) { +func TestSimpleInt(t *testing.T) { type testCase struct { name string jsonInput []byte wantNull bool + wantSet bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"replicaCount":1}`), wantNull: false, + wantSet: true, }, { name: "simple object: set name to empty value", jsonInput: []byte(`{"replicaCount":0}`), wantNull: false, + wantSet: true, }, { name: "simple object: set name to null value", jsonInput: []byte(`{"replicaCount":null}`), wantNull: true, + wantSet: true, }, { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), wantNull: false, + wantSet: false, }, } for _, tt := range tests { @@ -103,6 +107,7 @@ func TestSimpleInt_IsDefined(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSet, obj.ReplicaCount.IsSet(), "IsSet()") }) } } @@ -112,35 +117,40 @@ type SimplePointerInt struct { ReplicaCount Nullable[*int] `json:"replicaCount"` } -func TestSimplePointerInt_IsDefined(t *testing.T) { +func TestSimplePointerInt(t *testing.T) { type testCase struct { name string jsonInput []byte wantNull bool + wantSet bool } tests := []testCase{ { name: "simple object: set name to some non null value", jsonInput: []byte(`{"replicaCount":1}`), wantNull: false, + wantSet: true, }, { name: "simple object: set name to empty value", jsonInput: []byte(`{"replicaCount":0}`), wantNull: false, + wantSet: true, }, { name: "simple object: set name to null value", jsonInput: []byte(`{"replicaCount":null}`), wantNull: true, + wantSet: true, }, { name: "simple object: do not provide name in json data", jsonInput: []byte(`{}`), wantNull: false, + wantSet: false, }, } for _, tt := range tests { @@ -149,6 +159,92 @@ func TestSimplePointerInt_IsDefined(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") + assert.Equalf(t, tt.wantSet, obj.ReplicaCount.IsSet(), "IsSet()") + }) + } +} + +type TestComplex struct { + SimpleInt Nullable[SimpleInt] `json:"simple_int"` + SimpleString Nullable[SimpleString] `json:"simple_string"` + StringList Nullable[[]string] `json:"string_list"` +} + +func TestMixed(t *testing.T) { + type testCase struct { + name string + jsonInput []byte + assert func(obj TestComplex, t *testing.T) + } + tests := []testCase{ + { + name: "empty json input", + jsonInput: []byte(`{}`), + assert: func(obj TestComplex, t *testing.T) { + assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.IsSet(), "replica count should not be set") + assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.IsNull(), "replica count should not be null") + assert.Equalf(t, false, obj.SimpleString.Value.Name.IsSet(), "name should not be set") + assert.Equalf(t, false, obj.SimpleString.Value.Name.IsNull(), "name should not be null") + assert.Equalf(t, false, obj.StringList.IsSet(), "string list should not be set") + assert.Equalf(t, false, obj.StringList.IsNull(), "string list should not be null") + }, + }, + + { + name: "replica count having non null value", + jsonInput: []byte(`{"simple_int":{"replicaCount":1}}`), + assert: func(obj TestComplex, t *testing.T) { + assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.IsNull(), "replica count should NOT be null") + assert.Equalf(t, true, obj.SimpleInt.Value.ReplicaCount.IsSet(), "replica count should be set") + assert.Equalf(t, false, obj.SimpleString.Value.Name.IsSet(), "name should NOT be set") + assert.Equalf(t, false, obj.SimpleString.Value.Name.IsNull(), "name should NOT be null") + gotValue, isNull := obj.SimpleInt.Value.ReplicaCount.Get() + assert.Equalf(t, false, isNull, "replica count should NOT be null") + assert.Equalf(t, 1, gotValue, "replica count should be 1") + }, + }, + + { + name: "string list having null value", + jsonInput: []byte(`{"string_list": null}`), + assert: func(obj TestComplex, t *testing.T) { + assert.Equalf(t, true, obj.StringList.IsSet(), "string_list should be set") + assert.Equalf(t, true, obj.StringList.IsNull(), "string_list should be null") + }, + }, + + { + name: "string list having non null value", + jsonInput: []byte(`{"string_list": ["foo", "bar"]}`), + assert: func(obj TestComplex, t *testing.T) { + assert.Equalf(t, true, obj.StringList.IsSet(), "string_list should be set") + assert.Equalf(t, false, obj.StringList.IsNull(), "string_list should not be null") + gotStringList, isNull := obj.StringList.Get() + assert.Equalf(t, false, isNull, "string_list should not be null") + assert.Equalf(t, []string{"foo", "bar"}, gotStringList, "string_list should have the values as provided in the jSON") + + }, + }, + + { + name: "set string list having empty value", + jsonInput: []byte(`{"string_list":[]}`), + assert: func(obj TestComplex, t *testing.T) { + assert.Equalf(t, true, obj.StringList.IsSet(), "string_list should be set") + assert.Equalf(t, false, obj.StringList.IsNull(), "string_list should not be null") + gotStringList, isNull := obj.StringList.Get() + assert.Equalf(t, false, isNull, "string_list should not be null") + assert.Equalf(t, []string{}, gotStringList, "string_list should have the values as provided in the jSON") + + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t1 *testing.T) { + var obj TestComplex + err := json.Unmarshal(tt.jsonInput, &obj) + assert.NoError(t, err) + tt.assert(obj, t) }) } } From 2fafecc8c234f4cd9be5b8d7c027f6f15c94cfc7 Mon Sep 17 00:00:00 2001 From: Ashutosh Kumar Date: Wed, 27 Dec 2023 09:22:32 +0530 Subject: [PATCH 06/11] remove outdated comment Signed-off-by: Ashutosh Kumar --- types/nullable_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/types/nullable_test.go b/types/nullable_test.go index c6b246fd..79ef2c2f 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -8,7 +8,6 @@ import ( ) type SimpleString struct { - // cannot decide if it was provided with `null` value in json Name Nullable[string] `json:"name"` } @@ -61,7 +60,6 @@ func TestSimpleString(t *testing.T) { } type SimpleInt struct { - // cannot decide if it was provided with `null` value in json ReplicaCount Nullable[int] `json:"replicaCount"` } From 4db874b4a622d95a1792c3ea23ccec7fa1e21140 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Wed, 3 Jan 2024 09:25:42 +0000 Subject: [PATCH 07/11] sq --- types/nullable.go | 48 +++++++---- types/nullable_test.go | 186 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 203 insertions(+), 31 deletions(-) diff --git a/types/nullable.go b/types/nullable.go index dd26af97..611d0f0a 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -9,30 +9,49 @@ import ( // nullBytes is a JSON null literal var nullBytes = []byte("null") -// Nullable type which can help distinguish between if a value was explicitly +// Nullable allows defining that a // provided `null` in JSON or not type Nullable[T any] struct { - Value T - Set bool - Null bool + // Value contains the underlying value of the field. If `Set` is true, and `Null` is false, **??** + Value *T + // Set will be true if the field was sent. + Set bool } // UnmarshalJSON implements the Unmarshaler interface. func (t *Nullable[T]) UnmarshalJSON(data []byte) error { t.Set = true if bytes.Equal(data, nullBytes) { - t.Null = true + // t.Null = true return nil } - if err := json.Unmarshal(data, &t.Value); err != nil { + // fmt.Printf("data: %v\n", data) + // fmt.Printf("t.Value: %v\n", t.Value) + var tt T + if err := json.Unmarshal(data, &tt); err != nil { return fmt.Errorf("couldn't unmarshal JSON: %w", err) } - t.Null = false + // fmt.Printf("t.Value: %v\n", t.Value) + t.Value = &tt + // fmt.Printf("t.Value: %v\n", t.Value) + // fmt.Printf("t.Value: %v\n", *t.Value) + // t.Null = false return nil } // MarshalJSON implements the Marshaler interface. func (t Nullable[T]) MarshalJSON() ([]byte, error) { + // TODO + // TODO + // TODO + // if !t.Set { + // // return []byte(""), nil + // return nil, nil + // } + // TODO + // TODO + // TODO + if t.IsNull() { return nullBytes, nil } @@ -41,14 +60,13 @@ func (t Nullable[T]) MarshalJSON() ([]byte, error) { // IsNull returns true if the value is explicitly provided `null` in json func (t *Nullable[T]) IsNull() bool { - return t.Null -} - -// IsSet returns true if the value is provided in json -func (t *Nullable[T]) IsSet() bool { - return t.Set + return t.Value == nil } -func (t *Nullable[T]) Get() (value T, null bool) { - return t.Value, t.IsNull() +// Get retrieves the value of underlying nullable field, and indicates whether the value was set or not. +// If `set == false`, then `value` can be ignored +// If `set == true` and `value == nil`: the field was sent explicitly with the value `null` +// If `set == true` and `value != nil`: the field was sent with the contents at `*value` +func (t *Nullable[T]) Get() (value *T, set bool) { + return t.Value, t.Set } diff --git a/types/nullable_test.go b/types/nullable_test.go index 79ef2c2f..1bc2811d 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -3,10 +3,158 @@ package types import ( "encoding/json" "fmt" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func ExampleNullable_marshal() { + obj := struct { + ID Nullable[int] `json:"id"` + }{} + + // when it's not set + b, err := json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to nil + obj.ID.Value = nil + obj.ID.Set = true + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to the zero value + var v int + obj.ID.Value = &v + obj.ID.Set = true + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to a specific value + v = 12345 + obj.ID.Value = &v + obj.ID.Set = true + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // Output: + // JSON: {} + // --- + // JSON: {"id":null} + // --- + // JSON: {"id":0} + // --- + // JSON: {"id":12345} + // --- +} + +func ExampleNullable_unmarshal() { + obj := struct { + Name Nullable[string] `json:"name"` + }{} + + // when it's not set + err := json.Unmarshal([]byte(` + { + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Set: %v\n", obj.Name.Set) + fmt.Printf("obj.Name.Value: %v\n", obj.Name.Value) + fmt.Println("---") + + // when it's set explicitly to nil + err = json.Unmarshal([]byte(` + { + "name": null + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Set: %v\n", obj.Name.Set) + fmt.Printf("obj.Name.Value: %v\n", obj.Name.Value) + fmt.Println("---") + + // when it's set explicitly to the zero value + err = json.Unmarshal([]byte(` + { + "name": "" + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Set: %v\n", obj.Name.Set) + if obj.Name.Value == nil { + fmt.Println("Error: expected obj.Name.Value to have a value, but was ") + return + } + fmt.Printf("obj.Name.Value: %#v\n", *obj.Name.Value) + fmt.Println("---") + + // when it's set explicitly to a specific value + err = json.Unmarshal([]byte(` + { + "name": "foo" + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Set: %v\n", obj.Name.Set) + if obj.Name.Value == nil { + fmt.Println("Error: expected obj.Name.Value to have a value, but was ") + return + } + fmt.Printf("obj.Name.Value: %#v\n", *obj.Name.Value) + fmt.Println("---") + + // Output: + // obj.Name.Set: false + // obj.Name.Value: + // --- + // obj.Name.Set: true + // obj.Name.Value: + // --- + // obj.Name.Set: true + // obj.Name.Value: "" + // --- + // obj.Name.Set: true + // obj.Name.Value: "foo" + // --- +} + type SimpleString struct { Name Nullable[string] `json:"name"` } @@ -53,7 +201,7 @@ func TestSimpleString(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.Name.IsNull(), "IsNull()") - assert.Equalf(t, tt.wantSet, obj.Name.IsSet(), "IsSet()") + assert.Equalf(t, tt.wantSet, obj.Name.Set, "Set") fmt.Println(obj.Name.Get()) }) } @@ -105,7 +253,7 @@ func TestSimpleInt(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") - assert.Equalf(t, tt.wantSet, obj.ReplicaCount.IsSet(), "IsSet()") + assert.Equalf(t, tt.wantSet, obj.ReplicaCount.Set, "Set") }) } } @@ -157,7 +305,7 @@ func TestSimplePointerInt(t *testing.T) { err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) assert.Equalf(t, tt.wantNull, obj.ReplicaCount.IsNull(), "IsNull()") - assert.Equalf(t, tt.wantSet, obj.ReplicaCount.IsSet(), "IsSet()") + assert.Equalf(t, tt.wantSet, obj.ReplicaCount.Set, "Set") }) } } @@ -179,11 +327,14 @@ func TestMixed(t *testing.T) { name: "empty json input", jsonInput: []byte(`{}`), assert: func(obj TestComplex, t *testing.T) { - assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.IsSet(), "replica count should not be set") + require.NotNilf(t, obj.SimpleInt.Value, "NN") + require.NotNilf(t, obj.SimpleString.Value, "NN") + require.NotNilf(t, obj.StringList.Value, "NN") + assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.Set, "replica count should not be set") assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.IsNull(), "replica count should not be null") - assert.Equalf(t, false, obj.SimpleString.Value.Name.IsSet(), "name should not be set") + assert.Equalf(t, false, obj.SimpleString.Value.Name.Set, "name should not be set") assert.Equalf(t, false, obj.SimpleString.Value.Name.IsNull(), "name should not be null") - assert.Equalf(t, false, obj.StringList.IsSet(), "string list should not be set") + assert.Equalf(t, false, obj.StringList.Set, "string list should not be set") assert.Equalf(t, false, obj.StringList.IsNull(), "string list should not be null") }, }, @@ -192,13 +343,15 @@ func TestMixed(t *testing.T) { name: "replica count having non null value", jsonInput: []byte(`{"simple_int":{"replicaCount":1}}`), assert: func(obj TestComplex, t *testing.T) { + require.NotNilf(t, obj.SimpleInt.Value, "NN") + require.NotNilf(t, obj.SimpleString.Value, "NN") assert.Equalf(t, false, obj.SimpleInt.Value.ReplicaCount.IsNull(), "replica count should NOT be null") - assert.Equalf(t, true, obj.SimpleInt.Value.ReplicaCount.IsSet(), "replica count should be set") - assert.Equalf(t, false, obj.SimpleString.Value.Name.IsSet(), "name should NOT be set") + assert.Equalf(t, true, obj.SimpleInt.Value.ReplicaCount.Set, "replica count should be set") + assert.Equalf(t, false, obj.SimpleString.Value.Name.Set, "name should NOT be set") assert.Equalf(t, false, obj.SimpleString.Value.Name.IsNull(), "name should NOT be null") - gotValue, isNull := obj.SimpleInt.Value.ReplicaCount.Get() - assert.Equalf(t, false, isNull, "replica count should NOT be null") - assert.Equalf(t, 1, gotValue, "replica count should be 1") + gotValue, isSet := obj.SimpleInt.Value.ReplicaCount.Get() + assert.Equalf(t, true, isSet, "replica count should NOT be null") + assert.Equalf(t, 1, *gotValue, "replica count should be 1") }, }, @@ -206,7 +359,8 @@ func TestMixed(t *testing.T) { name: "string list having null value", jsonInput: []byte(`{"string_list": null}`), assert: func(obj TestComplex, t *testing.T) { - assert.Equalf(t, true, obj.StringList.IsSet(), "string_list should be set") + require.NotNilf(t, obj.StringList.Value, "NN") + assert.Equalf(t, true, obj.StringList.Set, "string_list should be set") assert.Equalf(t, true, obj.StringList.IsNull(), "string_list should be null") }, }, @@ -215,7 +369,7 @@ func TestMixed(t *testing.T) { name: "string list having non null value", jsonInput: []byte(`{"string_list": ["foo", "bar"]}`), assert: func(obj TestComplex, t *testing.T) { - assert.Equalf(t, true, obj.StringList.IsSet(), "string_list should be set") + assert.Equalf(t, true, obj.StringList.Set, "string_list should be set") assert.Equalf(t, false, obj.StringList.IsNull(), "string_list should not be null") gotStringList, isNull := obj.StringList.Get() assert.Equalf(t, false, isNull, "string_list should not be null") @@ -228,7 +382,7 @@ func TestMixed(t *testing.T) { name: "set string list having empty value", jsonInput: []byte(`{"string_list":[]}`), assert: func(obj TestComplex, t *testing.T) { - assert.Equalf(t, true, obj.StringList.IsSet(), "string_list should be set") + assert.Equalf(t, true, obj.StringList.Set, "string_list should be set") assert.Equalf(t, false, obj.StringList.IsNull(), "string_list should not be null") gotStringList, isNull := obj.StringList.Get() assert.Equalf(t, false, isNull, "string_list should not be null") @@ -238,7 +392,7 @@ func TestMixed(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t1 *testing.T) { + t.Run(tt.name, func(t *testing.T) { var obj TestComplex err := json.Unmarshal(tt.jsonInput, &obj) assert.NoError(t, err) From e7e109ca268fcc181fd35a7a42809f6b8b65a7e2 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 4 Jan 2024 12:06:06 +0000 Subject: [PATCH 08/11] sq --- types/nullable_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/nullable_test.go b/types/nullable_test.go index 1bc2811d..1ad07d94 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -11,7 +11,7 @@ import ( func ExampleNullable_marshal() { obj := struct { - ID Nullable[int] `json:"id"` + ID *Nullable[int] `json:"id,omitempty"` }{} // when it's not set @@ -24,6 +24,7 @@ func ExampleNullable_marshal() { fmt.Println("---") // when it's set explicitly to nil + obj.ID = &Nullable[int]{} obj.ID.Value = nil obj.ID.Set = true From b1098f3071dbf4a561425495848d9ee06883e4d5 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 4 Jan 2024 13:24:43 +0000 Subject: [PATCH 09/11] sq --- types/nullable.go | 14 ++++++++++++++ types/nullable_test.go | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/types/nullable.go b/types/nullable.go index 611d0f0a..73775c42 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -18,6 +18,16 @@ type Nullable[T any] struct { Set bool } +// Worst case we would have Unmarshal work correctly, and Marshal be broken +// https://stackoverflow.com/questions/70025330/how-to-allow-omitempty-only-unmarshal-and-not-when-marshal + +func (t *Nullable[T]) IsSet() bool { + if t == nil { + return false + } + return t.Set +} + // UnmarshalJSON implements the Unmarshaler interface. func (t *Nullable[T]) UnmarshalJSON(data []byte) error { t.Set = true @@ -60,6 +70,10 @@ func (t Nullable[T]) MarshalJSON() ([]byte, error) { // IsNull returns true if the value is explicitly provided `null` in json func (t *Nullable[T]) IsNull() bool { + if t == nil { + return true + } + return t.Value == nil } diff --git a/types/nullable_test.go b/types/nullable_test.go index 1ad07d94..7312d886 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -9,6 +9,15 @@ import ( "github.com/stretchr/testify/require" ) +func ExampleNullable_foo() { + obj := struct { + ID *Nullable[int] `json:"id,omitempty"` + }{} + fmt.Printf("obj.ID.IsNull(): %v\n", obj.ID.IsNull()) + fmt.Printf("obj.ID.IsSet(): %v\n", obj.ID.IsSet()) + // Output: +} + func ExampleNullable_marshal() { obj := struct { ID *Nullable[int] `json:"id,omitempty"` From c56794f657f3d1b7a52af50109668184dcdbdcbb Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 4 Jan 2024 14:24:26 +0000 Subject: [PATCH 10/11] sq --- types/nullable.go | 3 +- types/nullable_test.go | 206 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 2 deletions(-) diff --git a/types/nullable.go b/types/nullable.go index 73775c42..2d5dc750 100644 --- a/types/nullable.go +++ b/types/nullable.go @@ -30,6 +30,7 @@ func (t *Nullable[T]) IsSet() bool { // UnmarshalJSON implements the Unmarshaler interface. func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + fmt.Println(data) t.Set = true if bytes.Equal(data, nullBytes) { // t.Null = true @@ -71,7 +72,7 @@ func (t Nullable[T]) MarshalJSON() ([]byte, error) { // IsNull returns true if the value is explicitly provided `null` in json func (t *Nullable[T]) IsNull() bool { if t == nil { - return true + return false } return t.Value == nil diff --git a/types/nullable_test.go b/types/nullable_test.go index 7312d886..b5d5cc32 100644 --- a/types/nullable_test.go +++ b/types/nullable_test.go @@ -9,6 +9,58 @@ import ( "github.com/stretchr/testify/require" ) +/* ...... */ +// Adapted from https://www.calhoun.io/how-to-determine-if-a-json-key-has-been-set-to-null-or-not-provided/ +type Nullable_[T any] struct { + // Value contains the underlying value of the field. If `Set` is true, and `Null` is false, **??** + Value *T + // Set will be true if the field was sent + Set bool + // Valid will be true if the value is a valid type - either a value of T or as an explicit `null` + Valid bool +} + +func (t *Nullable_[T]) UnmarshalJSON(data []byte) error { + // If this method is called, there was a value explicitly sent, which was either or a value of `T` + t.Set = true + + // we received an explicit value of null + if string(data) == "null" { + // which is deemed valid, because we allow either or a value of `T` + t.Valid = true + t.Value = nil + return nil + } + + // we received a value of `T` + var temp T + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + t.Value = &temp + t.Valid = true + return nil +} + +/* */ +/* ...... */ + +func (t *Nullable_[T]) IsSet() bool { + if t == nil { + return false + } + + return t.Set +} + +func (t *Nullable_[T]) IsValid() bool { + if t == nil { + return false + } + + return t.Valid +} + func ExampleNullable_foo() { obj := struct { ID *Nullable[int] `json:"id,omitempty"` @@ -18,6 +70,158 @@ func ExampleNullable_foo() { // Output: } +// func (t *Nullable_[T]) IsSet() bool { +// if t == nil { +// return false +// } +// return t.Set +// } +// func (t *Nullable_[T]) IsValid() bool { +// if t == nil { +// return false +// } +// return t.Valid +// } + +// return t.Value == nil +// } + +func TestNullable___foo(t *testing.T) { + obj := struct { + ID Nullable_[int] `json:"id"` + }{} + + // when unset + obj.ID = Nullable_[int]{} + // assert.False(t, obj.ID.IsSet()) + // assert.False(t, obj.ID.IsNull()) + // assert.False(t, obj.ID.Valid) + + // when empty body + obj.ID = Nullable_[int]{} + err := json.Unmarshal([]byte("{}"), &obj) + require.NoError(t, err) + fmt.Printf("empty\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + + // assert.False(t, obj.ID.IsSet()) + // assert.False(t, obj.ID.IsNull()) + // fmt.Printf("obj: %v\n", obj) + assert.False(t, obj.ID.IsValid()) + + // when explicit null body + obj.ID = Nullable_[int]{} + err = json.Unmarshal([]byte(`{"id": null}`), &obj) + require.NoError(t, err) + fmt.Printf("null\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + + // fmt.Printf("obj.ID: %#v\n", obj.ID) + // assert.True(t, obj.ID.IsSet()) + // assert.True(t, obj.ID.IsNull()) + assert.False(t, obj.ID.IsValid()) + + // when explicit zero value + obj.ID = Nullable_[int]{} + err = json.Unmarshal([]byte(`{"id": 0}`), &obj) + require.NoError(t, err) + fmt.Printf("zero\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + if assert.NotNil(t, obj.ID.Value) { + fmt.Printf("obj.ID.Value: %v\n", *obj.ID.Value) + } + + // assert.True(t, obj.ID.IsSet()) + // assert.False(t, obj.ID.IsNull()) + if assert.NotNil(t, obj.ID.Value) { + assert.Equal(t, 0, *obj.ID.Value) + } + assert.True(t, obj.ID.IsValid()) + + // when explicit value + obj.ID = Nullable_[int]{} + err = json.Unmarshal([]byte(`{"id": 1230}`), &obj) + require.NoError(t, err) + fmt.Printf("val\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + if assert.NotNil(t, obj.ID.Value) { + fmt.Printf("obj.ID.Value: %v\n", *obj.ID.Value) + } + + // assert.True(t, obj.ID.IsSet()) + // assert.False(t, obj.ID.IsNull()) + if assert.NotNil(t, obj.ID.Value) { + assert.Equal(t, 1230, *obj.ID.Value) + } + assert.True(t, obj.ID.Valid) +} + +func TestNullable___food(t *testing.T) { + obj := struct { + ID *Nullable[int] `json:"id,omitempty"` + }{} + + // when unset + obj.ID = nil + assert.False(t, obj.ID.IsSet()) + assert.False(t, obj.ID.IsNull()) + + // when empty body + obj.ID = nil + err := json.Unmarshal([]byte("{}"), &obj) + require.NoError(t, err) + + assert.False(t, obj.ID.IsSet()) + assert.False(t, obj.ID.IsNull()) + + // when explicit null body + obj.ID = nil + err = json.Unmarshal([]byte(`{"id": null}`), &obj) + require.NoError(t, err) + + fmt.Printf("obj.ID: %#v\n", obj.ID) + assert.True(t, obj.ID.IsSet()) + assert.True(t, obj.ID.IsNull()) + + // when explicit zero value + obj.ID = nil + err = json.Unmarshal([]byte(`{"id": 0}`), &obj) + require.NoError(t, err) + + assert.True(t, obj.ID.IsSet()) + assert.False(t, obj.ID.IsNull()) + if assert.NotNil(t, obj.ID.Value) { + assert.Equal(t, 0, *obj.ID.Value) + } + + // when explicit value + obj.ID = nil + err = json.Unmarshal([]byte(`{"id": 1230}`), &obj) + require.NoError(t, err) + + assert.True(t, obj.ID.IsSet()) + assert.False(t, obj.ID.IsNull()) + if assert.NotNil(t, obj.ID.Value) { + assert.Equal(t, 1230, *obj.ID.Value) + } +} + +func TestNullable_UnmarshalJSON1(t *testing.T) { + jsonPayload := []byte(`{"replicaCount":null}`) + var obj SimpleInt + err := json.Unmarshal(jsonPayload, &obj) + require.NoError(t, err) + // This panics but expectation is -- it should print true + fmt.Println(obj.ReplicaCount.IsNull()) + assert.True(t, obj.ReplicaCount.IsSet()) + assert.True(t, obj.ReplicaCount.IsNull()) + + jsonPayload1 := []byte(`{}`) + var obj1 SimpleInt + err = json.Unmarshal(jsonPayload1, &obj1) + require.NoError(t, err) + // This panics but expectation is -- it should print false + fmt.Println(obj1.ReplicaCount.IsNull()) + assert.False(t, obj1.ReplicaCount.IsNull()) + assert.False(t, obj1.ReplicaCount.IsSet()) +} + func ExampleNullable_marshal() { obj := struct { ID *Nullable[int] `json:"id,omitempty"` @@ -218,7 +422,7 @@ func TestSimpleString(t *testing.T) { } type SimpleInt struct { - ReplicaCount Nullable[int] `json:"replicaCount"` + ReplicaCount *Nullable[int] `json:"replicaCount,omitempty"` } func TestSimpleInt(t *testing.T) { From bf83ce7b694769b881230975c73e600a94781d40 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Thu, 4 Jan 2024 14:39:40 +0000 Subject: [PATCH 11/11] sq --- types/nullable_new_test.go | 109 +++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 types/nullable_new_test.go diff --git a/types/nullable_new_test.go b/types/nullable_new_test.go new file mode 100644 index 00000000..5dbdec27 --- /dev/null +++ b/types/nullable_new_test.go @@ -0,0 +1,109 @@ +package types_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +/* ...... */ +// Adapted from https://www.calhoun.io/how-to-determine-if-a-json-key-has-been-set-to-null-or-not-provided/ +type Nullable[T any] struct { + // Value contains the underlying value of the field. If `Set` is true, and `Null` is false, **??** + Value *T + // Set will be true if the field was sent + Set bool + // Valid will be true if the value is a valid type - either a value of T or as an explicit `null` + Valid bool +} + +func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + // If this method is called, there was a value explicitly sent, which was either or a value of `T` + t.Set = true + + // we received an explicit value of null + if string(data) == "null" { + // which is deemed valid, because we allow either or a value of `T` + t.Valid = true + t.Value = nil + return nil + } + + // we received a value of `T` + var temp T + if err := json.Unmarshal(data, &temp); err != nil { + return fmt.Errorf("couldn't unmarshal JSON: %w", err) + } + t.Value = &temp + t.Valid = true + return nil +} + +/* */ +/* ...... */ + +func (t Nullable[T]) IsSet() bool { + return t.Set +} + +func (t Nullable[T]) IsValid() bool { + return t.Valid +} + +func TestNullable2(t *testing.T) { + obj := struct { + ID Nullable[int] `json:"id"` + }{} + + // when unset + obj.ID = Nullable[int]{} + assert.False(t, obj.ID.IsSet()) + assert.False(t, obj.ID.IsValid()) + assert.Nil(t, obj.ID.Value) + + // when empty body + obj.ID = Nullable[int]{} + err := json.Unmarshal([]byte("{}"), &obj) + require.NoError(t, err) + fmt.Printf("empty\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + + assert.False(t, obj.ID.IsSet()) + assert.False(t, obj.ID.IsValid()) + assert.Nil(t, obj.ID.Value) + + // when explicit null body + obj.ID = Nullable[int]{} + err = json.Unmarshal([]byte(`{"id": null}`), &obj) + require.NoError(t, err) + fmt.Printf("null\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + + assert.True(t, obj.ID.IsSet()) + assert.True(t, obj.ID.IsValid()) + assert.Nil(t, obj.ID.Value) + + // when explicit zero value + obj.ID = Nullable[int]{} + err = json.Unmarshal([]byte(`{"id": 0}`), &obj) + require.NoError(t, err) + fmt.Printf("zero\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + assert.True(t, obj.ID.IsSet()) + assert.True(t, obj.ID.IsValid()) + if assert.NotNil(t, obj.ID.Value) { + assert.Equal(t, 0, *obj.ID.Value) + } + + // when explicit value + obj.ID = Nullable[int]{} + err = json.Unmarshal([]byte(`{"id": 1230}`), &obj) + require.NoError(t, err) + fmt.Printf("val\t%v: %v %v\n", obj.ID, obj.ID.IsSet(), obj.ID.IsValid()) + assert.True(t, obj.ID.IsSet()) + assert.True(t, obj.ID.IsValid()) + if assert.NotNil(t, obj.ID.Value) { + assert.Equal(t, 1230, *obj.ID.Value) + } + assert.True(t, obj.ID.Valid) +}