Skip to content

Commit

Permalink
feat: add NullableInto type
Browse files Browse the repository at this point in the history
Introduced the NullableInto type in nulls package to handle nullable values without required pointer treatment. This enhances flexibility for json and sql value handling. Accompanied this new feature with comprehensive unit tests.
  • Loading branch information
lefinal committed Dec 15, 2023
1 parent d57e4ac commit 00aded4
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 0 deletions.
68 changes: 68 additions & 0 deletions nullable_into.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package nulls

import (
"database/sql/driver"
"encoding/json"
)

// NullableIntoValue are the requirements for values used in NullableInto as they
// need to implement at least driver.Valuer, and a ScanInto method.
type NullableIntoValue[T any] interface {
ScanInto(src any, dst *T) error
driver.Valuer
}

// NullableInto holds a nullable value that satisfies NullableIntoValue. This can
// be used instead of Nullable if the value should not be treated as pointer. It
// then provides the NullableIntoValue.ScanInto method that scans into a passed
// reference.
type NullableInto[T NullableIntoValue[T]] struct {
// V is the actual value when Valid.
V T
// Valid describes whether the Nullable does not hold a NULL value.
Valid bool
}

// NewNullableInto creates a new valid NullableInto with the given value.
func NewNullableInto[T NullableIntoValue[T]](v T) NullableInto[T] {
return NullableInto[T]{
V: v,
Valid: true,
}
}

// MarshalJSON as value. If not vot valid, a NULL-value is returned.
func (n NullableInto[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 *NullableInto[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 *NullableInto[T]) Scan(src any) error {
if src == nil {
n.Valid = false
return nil
}
n.Valid = true
return n.V.ScanInto(src, &n.V)
}

// Value returns the value for satisfying the driver.Valuer interface.
func (n NullableInto[T]) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.V.Value()
}
214 changes: 214 additions & 0 deletions nullable_into_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package nulls

import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"testing"
)

type myStruct struct {
A string
}

func (m myStruct) ScanInto(src any, dst *myStruct) error {
switch src := src.(type) {
case string:
dst.A = src
return nil
default:
return fmt.Errorf("unsupported src type: %T", src)
}
}

func (m myStruct) Value() (driver.Value, error) {
return m.A, nil
}

func TestNullableInto2(t *testing.T) {
m := myStruct{A: "Hello World!"}
myNullable := NewNullableInto(m)
assert.True(t, myNullable.Valid, "should be valid")
assert.Equal(t, m, myNullable.V, "should have set correct value")
err := myNullable.Scan("Ola!")
require.NoError(t, err, "scan should not fail")
assert.True(t, myNullable.Valid, "should still be valid")
assert.Equal(t, "Ola!", myNullable.V.A, "should have scanned correctly")
}

// TestNewNullableInto tests NewNullableInto.
func TestNewNullableInto(t *testing.T) {
n := NewNullableInto[myStruct](myStruct{A: "Hello World!"})
assert.True(t, n.Valid, "should be valid")
assert.Equal(t, "Hello World!", n.V.A, "should have set correct value")
}

// NullableIntoValueMock implements NullableIntoValue.
type NullableIntoValueMock struct {
mock.Mock
}

func (n NullableIntoValueMock) MarshalJSON() ([]byte, error) {
args := n.Called()
var b []byte
b, _ = args.Get(0).([]byte)
return b, args.Error(1)
}

func (n *NullableIntoValueMock) UnmarshalJSON(data []byte) error {
return n.Called(data).Error(0)
}

func (n NullableIntoValueMock) ScanInto(src any, dst *NullableIntoValueMock) error {
return n.Called(src, dst).Error(0)
}

func (n NullableIntoValueMock) Value() (driver.Value, error) {
args := n.Called()
var v driver.Value
v, _ = args.Get(0).(driver.Value)
return v, args.Error(1)
}

// NullableIntoMarshalJSONSuite tests NullableInto.MarshalJSON.
type NullableIntoMarshalJSONSuite struct {
suite.Suite
}

func (suite *NullableIntoMarshalJSONSuite) TestNotValid() {
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
raw, err := json.Marshal(n)
suite.Require().NoError(err, "should not fail")
suite.Equal(jsonNull, raw, "should return correct value")
}

func (suite *NullableIntoMarshalJSONSuite) TestMarshalFail() {
n := NewNullableInto(NullableIntoValueMock{})
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 *NullableIntoMarshalJSONSuite) TestOK() {
n := NewNullableInto(NullableIntoValueMock{})
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 TestNullableInto_MarshalJSON(t *testing.T) {
suite.Run(t, new(NullableIntoMarshalJSONSuite))
}

// NullableIntoUnmarshalJSONSuite tests NullableInto.UnmarshalJSON.
type NullableIntoUnmarshalJSONSuite struct {
suite.Suite
}

func (suite *NullableIntoUnmarshalJSONSuite) TestNull() {
var n NullableInto[NullableIntoValueMock]
err := json.Unmarshal(jsonNull, &n)
suite.Require().NoError(err, "should not fail")
suite.False(n.Valid, "should not be valid")
}

func (suite *NullableIntoUnmarshalJSONSuite) TestUnmarshalFail() {
raw := marshalMust("meow")
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
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 *NullableIntoUnmarshalJSONSuite) TestOK() {
raw := marshalMust("meow")
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
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 TestNullableInto_UnmarshalJSON(t *testing.T) {
suite.Run(t, new(NullableIntoUnmarshalJSONSuite))
}

// NullableIntoScanSuite tests NullableInto.Scan.
type NullableIntoScanSuite struct {
suite.Suite
}

func (suite *NullableIntoScanSuite) TestNull() {
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
err := n.Scan(nil)
suite.Require().NoError(err, "should not fail")
suite.False(n.Valid, "should not be valid")
}

func (suite *NullableIntoScanSuite) TestScanFail() {
src := "Hello World!"
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
n.V.On("ScanInto", src, &n.V).Return(errors.New("sad life"))
defer n.V.AssertExpectations(suite.T())
err := n.Scan(src)
suite.Require().Error(err, "should fail")
}

func (suite *NullableIntoScanSuite) TestOK() {
src := "Hello World!"
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
n.V.On("ScanInto", src, &n.V).Return(nil)
defer n.V.AssertExpectations(suite.T())
err := n.Scan(src)
suite.Require().NoError(err, "should not fail")
suite.True(n.Valid, "should be valid")
}

func TestNullableInto_ScanInto(t *testing.T) {
suite.Run(t, new(NullableIntoScanSuite))
}

// NullableIntoValueSuite tests NullableInto.Value.
type NullableIntoValueSuite struct {
suite.Suite
}

func (suite *NullableIntoValueSuite) TestNull() {
n := NullableInto[NullableIntoValueMock]{V: NullableIntoValueMock{}}
raw, err := n.Value()
suite.Require().NoError(err, "should not fail")
suite.Nil(raw, "should return correct value")
}

func (suite *NullableIntoValueSuite) TestValueFail() {
n := NewNullableInto(NullableIntoValueMock{})
n.V.On("Value").Return(nil, errors.New("sad life"))
defer n.V.AssertExpectations(suite.T())
_, err := n.Value()
suite.Require().Error(err, "should fail")
}

func (suite *NullableIntoValueSuite) TestOK() {
expectRaw := []byte("Hello World!")
n := NewNullableInto(NullableIntoValueMock{})
n.V.On("Value").Return(expectRaw, nil)
defer n.V.AssertExpectations(suite.T())
raw, err := n.Value()
suite.Require().NoError(err, "should not fail")
suite.Equal(expectRaw, raw, "should return correct value")
}

func TestNullableInto_Value(t *testing.T) {
suite.Run(t, new(NullableIntoValueSuite))
}

0 comments on commit 00aded4

Please sign in to comment.