Skip to content

Commit

Permalink
feat: add json nullable
Browse files Browse the repository at this point in the history
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
lefinal committed Jul 26, 2022
1 parent 3cbcc1d commit f68ff93
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
52 changes: 52 additions & 0 deletions json_nullable.go
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")
}
149 changes: 149 additions & 0 deletions json_nullable_test.go
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))
}

0 comments on commit f68ff93

Please sign in to comment.