Skip to content

Commit a59c151

Browse files
authored
Merge pull request #13 from lefinal/feat-any-nullable
feat: add Optional type
2 parents 83392c0 + 251037e commit a59c151

File tree

2 files changed

+222
-0
lines changed

2 files changed

+222
-0
lines changed

optional.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package nulls
2+
3+
import (
4+
"database/sql/driver"
5+
"encoding/json"
6+
"fmt"
7+
)
8+
9+
// Optional holds a nullable value. This can be used instead of Nullable or
10+
// NullableInto if database support is not required.
11+
type Optional[T any] struct {
12+
// V is the actual value when Valid.
13+
V T
14+
// Valid describes whether the Nullable does not hold a NULL value.
15+
Valid bool
16+
}
17+
18+
// NewOptional creates a new valid Optional with the given value.
19+
func NewOptional[T any](v T) Optional[T] {
20+
return Optional[T]{
21+
V: v,
22+
Valid: true,
23+
}
24+
}
25+
26+
// MarshalJSON as value. If not vot valid, a NULL-value is returned.
27+
func (n Optional[T]) MarshalJSON() ([]byte, error) {
28+
if !n.Valid {
29+
return json.Marshal(nil)
30+
}
31+
return json.Marshal(n.V)
32+
}
33+
34+
// UnmarshalJSON as value or sets Valid or false if null.
35+
func (n *Optional[T]) UnmarshalJSON(data []byte) error {
36+
if string(data) == "null" {
37+
n.Valid = false
38+
return nil
39+
}
40+
n.Valid = true
41+
return json.Unmarshal(data, &n.V)
42+
}
43+
44+
// Scan returns an error as this is currently not supported on Optional. Use
45+
// Nullable or NullableInto instead.
46+
func (n *Optional[T]) Scan(_ any) error {
47+
return fmt.Errorf("cannot scan optional")
48+
}
49+
50+
// Value returns an error as this is currently not supported on Optional. Use
51+
// Nullable or NullableInto instead.
52+
func (n Optional[T]) Value() (driver.Value, error) {
53+
return nil, fmt.Errorf("cannot value optional")
54+
}

optional_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package nulls
2+
3+
import (
4+
"database/sql/driver"
5+
"encoding/json"
6+
"errors"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/mock"
9+
"github.com/stretchr/testify/suite"
10+
"testing"
11+
)
12+
13+
func TestOptional(t *testing.T) {
14+
m := myStruct{A: "Hello World!"}
15+
myOptional := NewOptional(m)
16+
assert.True(t, myOptional.Valid, "should be valid")
17+
assert.Equal(t, m, myOptional.V, "should have set correct value")
18+
}
19+
20+
// TestNewOptional tests NewOptional.
21+
func TestNewOptional(t *testing.T) {
22+
n := NewOptional[myStruct](myStruct{A: "Hello World!"})
23+
assert.True(t, n.Valid, "should be valid")
24+
assert.Equal(t, "Hello World!", n.V.A, "should have set correct value")
25+
}
26+
27+
// OptionalValueMock implements OptionalValue.
28+
type OptionalValueMock struct {
29+
mock.Mock
30+
}
31+
32+
func (n OptionalValueMock) MarshalJSON() ([]byte, error) {
33+
args := n.Called()
34+
var b []byte
35+
b, _ = args.Get(0).([]byte)
36+
return b, args.Error(1)
37+
}
38+
39+
func (n *OptionalValueMock) UnmarshalJSON(data []byte) error {
40+
return n.Called(data).Error(0)
41+
}
42+
43+
func (n OptionalValueMock) Scan(src any) error {
44+
return n.Called(src).Error(0)
45+
}
46+
47+
func (n OptionalValueMock) Value() (driver.Value, error) {
48+
args := n.Called()
49+
var v driver.Value
50+
v, _ = args.Get(0).(driver.Value)
51+
return v, args.Error(1)
52+
}
53+
54+
// OptionalMarshalJSONSuite tests Optional.MarshalJSON.
55+
type OptionalMarshalJSONSuite struct {
56+
suite.Suite
57+
}
58+
59+
func (suite *OptionalMarshalJSONSuite) TestNotValid() {
60+
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
61+
raw, err := json.Marshal(n)
62+
suite.Require().NoError(err, "should not fail")
63+
suite.Equal(jsonNull, raw, "should return correct value")
64+
}
65+
66+
func (suite *OptionalMarshalJSONSuite) TestMarshalFail() {
67+
n := NewOptional(OptionalValueMock{})
68+
n.V.On("MarshalJSON").Return(nil, errors.New("sad life"))
69+
defer n.V.AssertExpectations(suite.T())
70+
_, err := json.Marshal(n)
71+
suite.Require().Error(err, "should fail")
72+
}
73+
74+
func (suite *OptionalMarshalJSONSuite) TestOK() {
75+
n := NewOptional(OptionalValueMock{})
76+
expectRaw := marshalMust("meow")
77+
n.V.On("MarshalJSON").Return(expectRaw, nil)
78+
defer n.V.AssertExpectations(suite.T())
79+
raw, err := json.Marshal(n)
80+
suite.Require().NoError(err, "should not fail")
81+
suite.Equal(expectRaw, raw, "should return correct value")
82+
}
83+
84+
func TestOptional_MarshalJSON(t *testing.T) {
85+
suite.Run(t, new(OptionalMarshalJSONSuite))
86+
}
87+
88+
// OptionalUnmarshalJSONSuite tests Optional.UnmarshalJSON.
89+
type OptionalUnmarshalJSONSuite struct {
90+
suite.Suite
91+
}
92+
93+
func (suite *OptionalUnmarshalJSONSuite) TestNull() {
94+
var n Optional[OptionalValueMock]
95+
err := json.Unmarshal(jsonNull, &n)
96+
suite.Require().NoError(err, "should not fail")
97+
suite.False(n.Valid, "should not be valid")
98+
}
99+
100+
func (suite *OptionalUnmarshalJSONSuite) TestUnmarshalFail() {
101+
raw := marshalMust("meow")
102+
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
103+
n.V.On("UnmarshalJSON", raw).Return(errors.New("sad life"))
104+
defer n.V.AssertExpectations(suite.T())
105+
err := json.Unmarshal(raw, &n)
106+
suite.Require().Error(err, "should fail")
107+
}
108+
109+
func (suite *OptionalUnmarshalJSONSuite) TestOK() {
110+
raw := marshalMust("meow")
111+
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
112+
n.V.On("UnmarshalJSON", raw).Return(nil)
113+
defer n.V.AssertExpectations(suite.T())
114+
err := json.Unmarshal(raw, &n)
115+
suite.Require().NoError(err, "should not fail")
116+
suite.True(n.Valid, "should be valid")
117+
}
118+
119+
func TestOptional_UnmarshalJSON(t *testing.T) {
120+
suite.Run(t, new(OptionalUnmarshalJSONSuite))
121+
}
122+
123+
// OptionalScanSuite tests Optional.Scan.
124+
type OptionalScanSuite struct {
125+
suite.Suite
126+
}
127+
128+
func (suite *OptionalScanSuite) TestNull() {
129+
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
130+
err := n.Scan(nil)
131+
suite.Error(err, "should fail")
132+
}
133+
134+
func (suite *OptionalScanSuite) TestScan() {
135+
src := "Hello World!"
136+
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
137+
n.V.On("Scan", src).Return(nil).Maybe()
138+
defer n.V.AssertExpectations(suite.T())
139+
err := n.Scan(src)
140+
suite.Error(err, "should fail")
141+
}
142+
143+
func TestOptional_Scan(t *testing.T) {
144+
suite.Run(t, new(OptionalScanSuite))
145+
}
146+
147+
// OptionalValueSuite tests Optional.Value.
148+
type OptionalValueSuite struct {
149+
suite.Suite
150+
}
151+
152+
func (suite *OptionalValueSuite) TestNull() {
153+
n := Optional[OptionalValueMock]{V: OptionalValueMock{}}
154+
_, err := n.Value()
155+
suite.Error(err, "should fail")
156+
}
157+
158+
func (suite *OptionalValueSuite) TestValue() {
159+
n := NewOptional(OptionalValueMock{})
160+
n.V.On("Value").Return("Hello", nil).Maybe()
161+
defer n.V.AssertExpectations(suite.T())
162+
_, err := n.Value()
163+
suite.Error(err, "should fail")
164+
}
165+
166+
func TestOptional_Value(t *testing.T) {
167+
suite.Run(t, new(OptionalValueSuite))
168+
}

0 commit comments

Comments
 (0)