-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a JSON-nullable that can be used like nullable, but omits the need for implementing SQL interfaces. This means, that it can only be used for (un-)marshalling.
- Loading branch information
Showing
3 changed files
with
202 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package nulls | ||
|
||
import ( | ||
"database/sql/driver" | ||
"encoding/json" | ||
"errors" | ||
) | ||
|
||
// JSONNullable holds a nullable value. Keep in mind, that T must be | ||
// (un)marshallable. However, it cannot be used as sql.Scanner or driver.Valuer. | ||
type JSONNullable[T any] struct { | ||
// V is the actual value when Valid. | ||
V T | ||
// Valid describes whether the JSONNullable does not hold a NULL value. | ||
Valid bool | ||
} | ||
|
||
// NewJSONNullable creates a new valid JSONNullable with the given value. | ||
func NewJSONNullable[T any](v T) JSONNullable[T] { | ||
return JSONNullable[T]{ | ||
V: v, | ||
Valid: true, | ||
} | ||
} | ||
|
||
// MarshalJSON as value. If not vot valid, a NULL-value is returned. | ||
func (n JSONNullable[T]) MarshalJSON() ([]byte, error) { | ||
if !n.Valid { | ||
return json.Marshal(nil) | ||
} | ||
return json.Marshal(n.V) | ||
} | ||
|
||
// UnmarshalJSON as value ro sets Valid o false if null. | ||
func (n *JSONNullable[T]) UnmarshalJSON(data []byte) error { | ||
if string(data) == "null" { | ||
n.Valid = false | ||
return nil | ||
} | ||
n.Valid = true | ||
return json.Unmarshal(data, &n.V) | ||
} | ||
|
||
// Scan to value or not valid if nil. | ||
func (n *JSONNullable[T]) Scan(src any) error { | ||
return errors.New("unsupported operation") | ||
} | ||
|
||
// Value returns the value for satisfying the driver.Valuer interface. | ||
func (n JSONNullable[T]) Value() (driver.Value, error) { | ||
return nil, errors.New("unsupported operation") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package nulls | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
"github.com/stretchr/testify/suite" | ||
"testing" | ||
) | ||
|
||
type aStruct struct { | ||
An int `json:"an"` | ||
} | ||
|
||
// TestNewJSONNullable tests NewJSONNullable. | ||
func TestNewJSONNullable(t *testing.T) { | ||
n := NewJSONNullable[aStruct](aStruct{An: 12}) | ||
assert.True(t, n.Valid, "should be valid") | ||
assert.Equal(t, 12, n.V.An, "should have set correct value") | ||
} | ||
|
||
// jSONNullableValueMock implements JSONNullableValue. | ||
type jSONNullableValueMock struct { | ||
mock.Mock | ||
} | ||
|
||
func (n *jSONNullableValueMock) MarshalJSON() ([]byte, error) { | ||
args := n.Called() | ||
var b []byte | ||
b, _ = args.Get(0).([]byte) | ||
return b, args.Error(1) | ||
} | ||
|
||
func (n *jSONNullableValueMock) UnmarshalJSON(data []byte) error { | ||
return n.Called(data).Error(0) | ||
} | ||
|
||
// JSONNullableMarshalJSONSuite tests JSONNullable.MarshalJSON. | ||
type JSONNullableMarshalJSONSuite struct { | ||
suite.Suite | ||
} | ||
|
||
func (suite *JSONNullableMarshalJSONSuite) TestNotValid() { | ||
n := JSONNullable[*jSONNullableValueMock]{V: &jSONNullableValueMock{}} | ||
raw, err := json.Marshal(n) | ||
suite.Require().NoError(err, "should not fail") | ||
suite.Equal(jsonNull, raw, "should return correct value") | ||
} | ||
|
||
func (suite *JSONNullableMarshalJSONSuite) TestMarshalFail() { | ||
n := NewJSONNullable(&jSONNullableValueMock{}) | ||
n.V.On("MarshalJSON").Return(nil, errors.New("sad life")) | ||
defer n.V.AssertExpectations(suite.T()) | ||
_, err := json.Marshal(n) | ||
suite.Require().Error(err, "should fail") | ||
} | ||
|
||
func (suite *JSONNullableMarshalJSONSuite) TestOK() { | ||
n := NewJSONNullable(&jSONNullableValueMock{}) | ||
expectRaw := marshalMust("meow") | ||
n.V.On("MarshalJSON").Return(expectRaw, nil) | ||
defer n.V.AssertExpectations(suite.T()) | ||
raw, err := json.Marshal(n) | ||
suite.Require().NoError(err, "should not fail") | ||
suite.Equal(expectRaw, raw, "should return correct value") | ||
} | ||
|
||
func TestJSONNullable_MarshalJSON(t *testing.T) { | ||
suite.Run(t, new(JSONNullableMarshalJSONSuite)) | ||
} | ||
|
||
// JSONNullableUnmarshalJSONSuite tests JSONNullable.UnmarshalJSON. | ||
type JSONNullableUnmarshalJSONSuite struct { | ||
suite.Suite | ||
} | ||
|
||
func (suite *JSONNullableUnmarshalJSONSuite) TestNull() { | ||
var n JSONNullable[*jSONNullableValueMock] | ||
err := json.Unmarshal(jsonNull, &n) | ||
suite.Require().NoError(err, "should not fail") | ||
suite.False(n.Valid, "should not be valid") | ||
} | ||
|
||
func (suite *JSONNullableUnmarshalJSONSuite) TestUnmarshalFail() { | ||
raw := marshalMust("meow") | ||
n := JSONNullable[*jSONNullableValueMock]{V: &jSONNullableValueMock{}} | ||
n.V.On("UnmarshalJSON", raw).Return(errors.New("sad life")) | ||
defer n.V.AssertExpectations(suite.T()) | ||
err := json.Unmarshal(raw, &n) | ||
suite.Require().Error(err, "should fail") | ||
} | ||
|
||
func (suite *JSONNullableUnmarshalJSONSuite) TestOK() { | ||
raw := marshalMust(`{"an": 12}`) | ||
n := JSONNullable[*jSONNullableValueMock]{V: &jSONNullableValueMock{}} | ||
n.V.On("UnmarshalJSON", raw).Return(nil) | ||
defer n.V.AssertExpectations(suite.T()) | ||
err := json.Unmarshal(raw, &n) | ||
suite.Require().NoError(err, "should not fail") | ||
suite.True(n.Valid, "should be valid") | ||
} | ||
|
||
func TestJSONNullable_UnmarshalJSON(t *testing.T) { | ||
suite.Run(t, new(JSONNullableUnmarshalJSONSuite)) | ||
} | ||
|
||
// JSONNullableScanSuite tests JSONNullable.Scan. | ||
type JSONNullableScanSuite struct { | ||
suite.Suite | ||
} | ||
|
||
func (suite *JSONNullableScanSuite) TestNull() { | ||
n := JSONNullable[*jSONNullableValueMock]{V: &jSONNullableValueMock{}} | ||
err := n.Scan(nil) | ||
suite.Error(err, "should fail") | ||
} | ||
|
||
func (suite *JSONNullableScanSuite) TestOK() { | ||
src := "Hello World!" | ||
n := JSONNullable[*jSONNullableValueMock]{V: &jSONNullableValueMock{}} | ||
err := n.Scan(src) | ||
suite.Error(err, "should fail") | ||
} | ||
|
||
func TestJSONNullable_Scan(t *testing.T) { | ||
suite.Run(t, new(JSONNullableScanSuite)) | ||
} | ||
|
||
// JSONNullableValueSuite tests JSONNullable.Value. | ||
type JSONNullableValueSuite struct { | ||
suite.Suite | ||
} | ||
|
||
func (suite *JSONNullableValueSuite) TestNull() { | ||
n := JSONNullable[*jSONNullableValueMock]{V: &jSONNullableValueMock{}} | ||
_, err := n.Value() | ||
suite.Error(err, "should fail") | ||
} | ||
|
||
func (suite *JSONNullableValueSuite) TestOK() { | ||
n := NewJSONNullable(&jSONNullableValueMock{}) | ||
_, err := n.Value() | ||
suite.Error(err, "should fail") | ||
} | ||
|
||
func TestJSONNullable_Value(t *testing.T) { | ||
suite.Run(t, new(JSONNullableValueSuite)) | ||
} |