Skip to content

Commit

Permalink
feat: add Optional type
Browse files Browse the repository at this point in the history
This commit adds a new Optional type in the 'nulls' package to hold nullable values. The Optional type offers functionality for JSON Marshalling and Unmarshalling. Currently, the 'Scan' and 'Value' methods are not supported and will return an error. Corresponding tests for the Optional type were also added for coverage.
  • Loading branch information
lefinal committed Jan 26, 2024
1 parent 83392c0 commit 251037e
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 251037e

Please sign in to comment.