Skip to content

Commit

Permalink
Merge pull request #13 from lefinal/feat-any-nullable
Browse files Browse the repository at this point in the history
feat: add Optional type
  • Loading branch information
lefinal authored Jan 26, 2024
2 parents 83392c0 + 251037e commit a59c151
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 0 deletions.
54 changes: 54 additions & 0 deletions optional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package nulls

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

// Optional holds a nullable value. This can be used instead of Nullable or
// NullableInto if database support is not required.
type Optional[T any] struct {
// V is the actual value when Valid.
V T
// Valid describes whether the Nullable does not hold a NULL value.
Valid bool
}

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

// MarshalJSON as value. If not vot valid, a NULL-value is returned.
func (n Optional[T]) MarshalJSON() ([]byte, error) {
if !n.Valid {
return json.Marshal(nil)
}
return json.Marshal(n.V)
}

// UnmarshalJSON as value or sets Valid or false if null.
func (n *Optional[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
n.Valid = false
return nil
}
n.Valid = true
return json.Unmarshal(data, &n.V)
}

// Scan returns an error as this is currently not supported on Optional. Use
// Nullable or NullableInto instead.
func (n *Optional[T]) Scan(_ any) error {
return fmt.Errorf("cannot scan optional")
}

// Value returns an error as this is currently not supported on Optional. Use
// Nullable or NullableInto instead.
func (n Optional[T]) Value() (driver.Value, error) {
return nil, fmt.Errorf("cannot value optional")
}
168 changes: 168 additions & 0 deletions optional_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package nulls

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

func TestOptional(t *testing.T) {
m := myStruct{A: "Hello World!"}
myOptional := NewOptional(m)
assert.True(t, myOptional.Valid, "should be valid")
assert.Equal(t, m, myOptional.V, "should have set correct value")
}

// TestNewOptional tests NewOptional.
func TestNewOptional(t *testing.T) {
n := NewOptional[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")
}

// OptionalValueMock implements OptionalValue.
type OptionalValueMock struct {
mock.Mock
}

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

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

func (n OptionalValueMock) Scan(src any) error {
return n.Called(src).Error(0)
}

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

// OptionalMarshalJSONSuite tests Optional.MarshalJSON.
type OptionalMarshalJSONSuite struct {
suite.Suite
}

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

func (suite *OptionalMarshalJSONSuite) TestMarshalFail() {
n := NewOptional(OptionalValueMock{})
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 *OptionalMarshalJSONSuite) TestOK() {
n := NewOptional(OptionalValueMock{})
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 TestOptional_MarshalJSON(t *testing.T) {
suite.Run(t, new(OptionalMarshalJSONSuite))
}

// OptionalUnmarshalJSONSuite tests Optional.UnmarshalJSON.
type OptionalUnmarshalJSONSuite struct {
suite.Suite
}

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

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

// OptionalScanSuite tests Optional.Scan.
type OptionalScanSuite struct {
suite.Suite
}

func (suite *OptionalScanSuite) TestNull() {
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
err := n.Scan(nil)
suite.Error(err, "should fail")
}

func (suite *OptionalScanSuite) TestScan() {
src := "Hello World!"
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
n.V.On("Scan", src).Return(nil).Maybe()
defer n.V.AssertExpectations(suite.T())
err := n.Scan(src)
suite.Error(err, "should fail")
}

func TestOptional_Scan(t *testing.T) {
suite.Run(t, new(OptionalScanSuite))
}

// OptionalValueSuite tests Optional.Value.
type OptionalValueSuite struct {
suite.Suite
}

func (suite *OptionalValueSuite) TestNull() {
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
_, err := n.Value()
suite.Error(err, "should fail")
}

func (suite *OptionalValueSuite) TestValue() {
n := NewOptional(OptionalValueMock{})
n.V.On("Value").Return("Hello", nil).Maybe()
defer n.V.AssertExpectations(suite.T())
_, err := n.Value()
suite.Error(err, "should fail")
}

func TestOptional_Value(t *testing.T) {
suite.Run(t, new(OptionalValueSuite))
}

0 comments on commit a59c151

Please sign in to comment.