Skip to content

Commit

Permalink
Merge pull request #11 from lefinal/feat-nullable-scan-into
Browse files Browse the repository at this point in the history
feat: add NullableInto type
  • Loading branch information
lefinal authored Dec 15, 2023
2 parents d57e4ac + 00aded4 commit 8590d9c
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 8590d9c

Please sign in to comment.