diff --git a/README.md b/README.md index fb1ed67..f783d31 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ go get github.com/lefinal/nulls Any datatype implementing the required interface can be used as `Nullable` offering the same functionality as predefined ones. +If no SQL-support is required, you can also use `JSONNullable`. # Usage diff --git a/json_nullable.go b/json_nullable.go new file mode 100644 index 0000000..2a14aaf --- /dev/null +++ b/json_nullable.go @@ -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") +} diff --git a/json_nullable_test.go b/json_nullable_test.go new file mode 100644 index 0000000..842c4d0 --- /dev/null +++ b/json_nullable_test.go @@ -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)) +}