diff --git a/README.md b/README.md index f1af8c7..4024fe3 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,200 @@ [![codecov](https://codecov.io/gh/go-andiamo/gopt/branch/main/graph/badge.svg?token=igjnZdgh0e)](https://codecov.io/gh/go-andiamo/gopt) [![Go Report Card](https://goreportcard.com/badge/github.com/go-andiamo/gopt)](https://goreportcard.com/report/github.com/go-andiamo/gopt) -A very simple Optional implementation in Golang +A very light Optional implementation in Golang + +## Installation +To install Gopt, use go get: + + go get github.com/go-andiamo/gopt + +To update Gopt to the latest version, run: + + go get -u github.com/go-andiamo/gopt + +## Examples +```go +package main + +import ( + . "github.com/go-andiamo/gopt" +) + +func main() { + optFlt := Of[float64](1.23) + println(optFlt.IsPresent()) + println(optFlt.OrElse(-1)) + + opt2 := Empty[float64]() + println(opt2.IsPresent()) + println(opt2.OrElse(-1)) + + opt2.OrElseSet(10) + println(opt2.IsPresent()) + println(opt2.OrElse(-1)) +} +``` + +## Methods + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Method and descriptionReturns
+ Get()
+ returns the value and an error if the value is not present +
(T, error)
+ AsEmpty()
+ returns a new empty optional of the same type +
Optional[T]
+ IsPresent()
+ returns true if the value is present, otherwise false +
bool
+ IfPresent(f func(v T))
+ if the value is present, calls the supplied function with the value, otherwise does nothing
+ returns the original optional +
Optional[T]
+ IfPresentOtherwise(f func(v T), other func())
+ if the value is present, calls the supplied function with the value, otherwise calls the other function
+ returns the original optional +
Optional[T]
+ OrElse(other T)
+ returns the value if present, otherwise returns other +
T
+ OrElseGet(f func() T)
+ returns the value if present, otherwise returns the result of calling the supplied function +
T
+ OrElseSet(v T)
+ if the value is not present it is set to the supplied value +
Optional[T]
+ OrElseError(err error)
+ returns the supplied error if the value is not present, otherwise returns nil +
error
+ OrElsePanic(v any)
+ if the value is not present, panics with the supplied value, otherwise does nothing +
nothing
+ DoWith(f func(v T))
+ if the value is present, calls the supplied function with the value
+ returns the original optional +
Optional[T]
+ Filter(f func(v T) bool)
+ if the value is present and calling the supplied filter function returns true, returns a new optional describing the value
+ Otherwise returns an empty optional +
Optional[T]
+ Map(f func(v T) any)
+ if the value is present and the result of calling the supplied mapping function returns non-nil, returns + an optional describing that returned value
+ Otherwise returns an empty optional +
Optional[any]
+ MarshalJSON()
+ implements JSON marshal
+ if the value is present, returns the marshalled data for the value
+ Otherwise, returns the marshalled data for null +
([]byte, error)
+ UnmarshalJSON(data []byte)
+ implements JSON unmarshal
+ if the supplied data is null representation, sets the present to false
+ Otherwise, unmarshal the data as the value and sets the optional to present (unless the result of + unmarshalling the value returns an error - in which case the present is set to false) +
error
+ Scan(value interface{})
+ implements sql.Scan +
error
+ +## Constructors + + + + + + + + + + + + + + + + +
Constructor function and description
+ Of[T any](value T) Optional[T]
+ Creates a new optional with the supplied value +
+ OfNillable[T any](value T) Optional[T]
+ Creates a new optional with the supplied value
+ If the supplied value is nil, an empty (not present) optional is returned +
+ OfNillableString(value string) Optional[string]
+ Creates a new string optional with the supplied value
+ If the supplied value is an empty string, an empty (not-present) optional is returned +
+ Empty[T any]() Optional[T]
+ Creates a new empty (not-present) optional of the specified type +
diff --git a/go.mod b/go.mod index 404fe30..e19ee8f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require github.com/stretchr/testify v1.8.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2ec90f7..027b89d 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,13 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -10,8 +17,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/optional.go b/optional.go index 50ba74c..48c6ec0 100644 --- a/optional.go +++ b/optional.go @@ -1,6 +1,7 @@ package gopt import ( + "database/sql" "encoding/json" "errors" "reflect" @@ -8,6 +9,47 @@ import ( type String *string +var ( + _EmptyString = Optional[string]{} + _EmptyInterface = Optional[interface{}]{} + _EmptyInt = Optional[int]{} + _EmptyInt8 = Optional[int8]{} + _EmptyInt16 = Optional[int16]{} + _EmptyInt32 = Optional[int32]{} + _EmptyInt64 = Optional[int64]{} + _EmptyUint = Optional[uint]{} + _EmptyUint8 = Optional[uint8]{} + _EmptyUint16 = Optional[uint16]{} + _EmptyUint32 = Optional[uint32]{} + _EmptyUint64 = Optional[uint64]{} + _EmptyBool = Optional[bool]{} + _EmptyFloat32 = Optional[float32]{} + _EmptyFloat64 = Optional[float64]{} + _EmptyByte = Optional[byte]{} + _EmptyRune = Optional[rune]{} +) + +var ( + EmptyString = _EmptyString + EmptyInterface = _EmptyInterface + EmptyInt = _EmptyInt + EmptyInt8 = _EmptyInt8 + EmptyInt16 = _EmptyInt16 + EmptyInt32 = _EmptyInt32 + EmptyInt64 = _EmptyInt64 + EmptyUint = _EmptyUint + EmptyUint8 = _EmptyUint8 + EmptyUint16 = _EmptyUint16 + EmptyUint32 = _EmptyUint32 + EmptyUint64 = _EmptyUint64 + EmptyBool = _EmptyBool + EmptyFloat32 = _EmptyFloat32 + EmptyFloat64 = _EmptyFloat64 + EmptyByte = _EmptyByte + EmptyRune = _EmptyRune +) + +// Of creates a new optional with the supplied value func Of[T any](value T) Optional[T] { return Optional[T]{ present: isPresent(value), @@ -15,6 +57,9 @@ func Of[T any](value T) Optional[T] { } } +// OfNillable creates a new optional with the supplied value +// +// If the supplied value is nil, an empty (not present) optional is returned func OfNillable[T any](value T) Optional[T] { if isPresent(value) { return Optional[T]{ @@ -27,6 +72,17 @@ func OfNillable[T any](value T) Optional[T] { } } +// OfNillableString creates a new string optional with the supplied value +// +// If the supplied value is an empty string, an empty (not-present) optional is returned +func OfNillableString(value string) Optional[string] { + return Optional[string]{ + present: value != "", + value: value, + } +} + +// Empty creates a new empty (not-present) optional of the specified type func Empty[T any]() Optional[T] { return Optional[T]{ present: false, @@ -35,12 +91,14 @@ func Empty[T any]() Optional[T] { func isPresent(v any) bool { vo := reflect.ValueOf(v) - vk := vo.Kind() - if vk == reflect.Ptr { + switch vk := vo.Kind(); vk { + case reflect.Ptr: vk = vo.Elem().Kind() if vk == reflect.Invalid { return false } + case reflect.Map, reflect.Slice, reflect.Interface: + return !vo.IsNil() } return v != nil } @@ -50,6 +108,7 @@ type Optional[T any] struct { value T } +// Get returns the value and an error if the value is not present func (o Optional[T]) Get() (T, error) { if !o.present { return o.value, errors.New("not present") @@ -57,22 +116,37 @@ func (o Optional[T]) Get() (T, error) { return o.value, nil } -func (o Optional[T]) Empty() Optional[T] { +// AsEmpty returns a new empty optional of the same type +func (o Optional[T]) AsEmpty() Optional[T] { return Optional[T]{ present: false, } } +// IsPresent returns true if the value is present, otherwise false func (o Optional[T]) IsPresent() bool { return o.present } -func (o Optional[T]) IfPresent(f func(v T)) { +// IfPresent if the value is present, calls the supplied function with the value, otherwise does nothing +func (o Optional[T]) IfPresent(f func(v T)) Optional[T] { + if o.present { + f(o.value) + } + return o +} + +// IfPresentOtherwise if the value is present, calls the supplied function with the value, otherwise calls the other function +func (o Optional[T]) IfPresentOtherwise(f func(v T), other func()) Optional[T] { if o.present { f(o.value) + } else { + other() } + return o } +// OrElse returns the value if present, otherwise returns other func (o Optional[T]) OrElse(other T) T { if o.present { return o.value @@ -80,6 +154,7 @@ func (o Optional[T]) OrElse(other T) T { return other } +// OrElseGet returns the value if present, otherwise returns the result of calling the supplied function func (o Optional[T]) OrElseGet(f func() T) T { if o.present { return o.value @@ -87,13 +162,43 @@ func (o Optional[T]) OrElseGet(f func() T) T { return f() } -func (o Optional[T]) OrElseError(f func() error) error { +// OrElseSet if the value is not present it is set to the supplied value +func (o *Optional[T]) OrElseSet(v T) Optional[T] { + if !o.present && isPresent(v) { + o.present = true + o.value = v + } + return *o +} + +// OrElseError returns the supplied error if the value is not present, otherwise returns nil +func (o Optional[T]) OrElseError(err error) error { if !o.present { - return f() + return err } return nil } +// OrElsePanic if the value is not present, panics with the supplied value, otherwise does nothing +func (o Optional[T]) OrElsePanic(v any) { + if !o.present { + panic(v) + } +} + +// DoWith if the value is present, calls the supplied function with the value +// +// Returns the original optional +func (o Optional[T]) DoWith(f func(v T)) Optional[T] { + if o.present { + f(o.value) + } + return o +} + +// Filter if the value is present and calling the supplied filter function returns true, returns a new optional describing the value +// +// Otherwise returns an empty optional func (o Optional[T]) Filter(f func(v T) bool) Optional[T] { if o.present && f(o.value) { return Optional[T]{ @@ -106,10 +211,14 @@ func (o Optional[T]) Filter(f func(v T) bool) Optional[T] { } } +// Map if the value is present and the result of calling the supplied mapping function returns non-nil, returns +// an optional describing that returned value +// +// Otherwise returns an empty optional func (o Optional[T]) Map(f func(v T) any) Optional[any] { if o.present { v := f(o.value) - if v != nil { + if isPresent(v) { return Of(v) } } @@ -118,6 +227,11 @@ func (o Optional[T]) Map(f func(v T) any) Optional[any] { } } +// MarshalJSON implements JSON marshal +// +// If the value is present, returns the marshalled data for the value +// +// Otherwise, returns the marshalled data for null func (o Optional[T]) MarshalJSON() ([]byte, error) { if !o.present { return []byte("null"), nil @@ -125,6 +239,12 @@ func (o Optional[T]) MarshalJSON() ([]byte, error) { return json.Marshal(o.value) } +// UnmarshalJSON implements JSON unmarshal +// +// if the supplied data is null representation, sets the present to false +// +// Otherwise, unmarshal the data as the value and sets the optional to present (unless the result of +// unmarshalling the value returns an error - in which case the present is set to false) func (o *Optional[T]) UnmarshalJSON(data []byte) error { if len(data) == 4 && data[0] == 'n' && data[1] == 'u' && data[2] == 'l' && data[3] == 'l' { o.present = false @@ -140,3 +260,49 @@ func (o *Optional[T]) UnmarshalJSON(data []byte) error { } return err } + +// Scan implements sql.Scan +func (o *Optional[T]) Scan(value interface{}) error { + if value == nil { + o.present = false + } else if av, ok := value.(T); ok { + o.present = true + o.value = av + } else if ok, err := o.callScannable(value); ok { + return err + } else if bd, ok := value.([]byte); ok { + var uv T + if err := json.Unmarshal(bd, &uv); err == nil { + o.present = true + o.value = uv + } else { + o.present = false + } + } else { + o.present = false + } + return nil +} + +func (o *Optional[T]) callScannable(value interface{}) (bool, error) { + var nv reflect.Value + if !isPresent(o.value) { + rt := reflect.TypeOf(o.value) + if rt.Kind() == reflect.Pointer { + rt = rt.Elem() + } + nv = reflect.New(rt) + } else { + nv = reflect.ValueOf(o.value) + } + anv := nv.Interface() + if sanv, ok := anv.(sql.Scanner); ok { + err := sanv.Scan(value) + if err == nil { + o.value = anv.(T) + o.present = true + } + return true, err + } + return false, nil +} diff --git a/optional_test.go b/optional_test.go index 0e94eea..7e686ee 100644 --- a/optional_test.go +++ b/optional_test.go @@ -22,7 +22,7 @@ func TestOfNillable(t *testing.T) { x := &myStruct{ Foo: "", } - opt2 := Of(x).Empty() + opt2 := Of(x).AsEmpty() require.False(t, opt2.IsPresent()) _, err = opt2.Get() require.Error(t, err) @@ -33,7 +33,7 @@ func TestOfNillable(t *testing.T) { }) require.Equal(t, "bar", v.Foo) - opt3 := Of("").Empty() + opt3 := Of("").AsEmpty() require.False(t, opt3.IsPresent()) _, err = opt3.Get() require.Error(t, err) @@ -42,6 +42,17 @@ func TestOfNillable(t *testing.T) { require.False(t, opt4.IsPresent()) } +func TestOfNillableString(t *testing.T) { + opt := OfNillableString("") + require.False(t, opt.IsPresent()) + opt = OfNillableString("foo") + require.True(t, opt.IsPresent()) + opt = Of("aaa") + require.True(t, opt.IsPresent()) + opt = Of("") + require.True(t, opt.IsPresent()) +} + func TestOf(t *testing.T) { opt := Of("aaa") require.True(t, opt.IsPresent()) @@ -91,6 +102,32 @@ func TestOptional_IfPresent(t *testing.T) { require.Equal(t, "aaa", collected) } +func TestOptional_IfPresentOtherwise(t *testing.T) { + calledPresent := false + collected := "" + calledOther := false + f := func(v string) { + calledPresent = true + collected = v + } + oth := func() { + calledOther = true + } + + o := Of("aaa") + o.IfPresentOtherwise(f, oth) + require.True(t, calledPresent) + require.Equal(t, "aaa", collected) + require.False(t, calledOther) + + calledPresent = false + calledOther = false + o = o.AsEmpty() + o.IfPresentOtherwise(f, oth) + require.False(t, calledPresent) + require.True(t, calledOther) +} + func TestOptional_OrElse(t *testing.T) { o := Empty[string]() v := o.OrElse("bbb") @@ -119,25 +156,60 @@ func TestOptional_OrElseGet(t *testing.T) { require.False(t, called) } +func TestOptional_OrElseSet(t *testing.T) { + o := Empty[map[string]interface{}]() + require.False(t, o.IsPresent()) + + o2 := o.OrElseSet(map[string]interface{}{}) + require.Equal(t, o, o2) + require.True(t, o2.IsPresent()) + require.True(t, o.IsPresent()) + + o = Empty[map[string]interface{}]() + require.False(t, o.IsPresent()) + o2 = o.OrElseSet(nil) + require.Equal(t, o, o2) + require.False(t, o2.IsPresent()) + require.False(t, o.IsPresent()) +} + func TestOptional_OrElseError(t *testing.T) { o := Empty[string]() - called := false - f := func() error { - called = true - return errors.New("not there") - } - err := o.OrElseError(f) - require.True(t, called) + err := o.OrElseError(errors.New("not there")) require.Error(t, err) require.Equal(t, "not there", err.Error()) - called = false o = Of("abc") - err = o.OrElseError(f) - require.False(t, called) + err = o.OrElseError(errors.New("not there")) require.NoError(t, err) } +func TestOptional_OrElsePanic(t *testing.T) { + o := Of("str") + o.OrElsePanic("whoops") + o = Empty[string]() + require.Panics(t, func() { + o.OrElsePanic("whoops") + }) +} + +func TestOptional_DoWith(t *testing.T) { + o := Empty[string]() + called := false + f := func(v string) { + called = true + } + o2 := o.DoWith(f) + require.False(t, called) + require.Equal(t, o, o2) + + called = false + o = Of("aaa") + o2 = o.DoWith(f) + require.True(t, called) + require.Equal(t, o, o2) +} + func TestOptional_Filter(t *testing.T) { o := Empty[string]() called := false @@ -234,3 +306,81 @@ func TestOptional_MarshalUnmarshalJSON(t *testing.T) { err = json.Unmarshal([]byte(str), myA3) require.Error(t, err) } + +func TestOptional_Scan(t *testing.T) { + var o Optional[string] + err := o.Scan("str") + require.NoError(t, err) + require.True(t, o.IsPresent()) + require.Equal(t, "str", o.OrElse("other")) + err = o.Scan(nil) + require.NoError(t, err) + require.False(t, o.IsPresent()) + + o2 := OfNillable(map[string]interface{}{}).AsEmpty() + require.False(t, o2.IsPresent()) + err = o2.Scan([]byte(`{"foo":"bar"}`)) + require.NoError(t, err) + require.True(t, o2.IsPresent()) + ov2, err := o2.Get() + require.NoError(t, err) + require.Equal(t, 1, len(ov2)) + + o3 := OfNillable[map[string]interface{}](nil) + require.False(t, o3.IsPresent()) + err = o3.Scan(nil) + require.NoError(t, err) + require.False(t, o3.IsPresent()) + err = o3.Scan("") + require.NoError(t, err) + require.False(t, o3.IsPresent()) + err = o3.Scan([]byte(`["foo","bar"]`)) + require.NoError(t, err) + require.False(t, o3.IsPresent()) + + o4 := OfNillable[*scannable](nil) + require.False(t, o4.present) + err = o4.Scan("abc") + require.NoError(t, err) + o4v := o4.OrElse(nil) + require.NotNil(t, o4v) + require.True(t, o4v.called) + require.Equal(t, "abc", o4v.value) + + o4 = Of(&scannable{err: errors.New("fooey")}) + require.True(t, o4.present) + err = o4.Scan("abc") + require.Error(t, err) +} + +type scannable struct { + called bool + err error + value any +} + +func (s *scannable) Scan(src any) error { + s.called = true + s.value = src + return s.err +} + +func TestEmpties(t *testing.T) { + require.False(t, EmptyString.IsPresent()) + require.False(t, EmptyInterface.IsPresent()) + require.False(t, EmptyInt.IsPresent()) + require.False(t, EmptyInt8.IsPresent()) + require.False(t, EmptyInt16.IsPresent()) + require.False(t, EmptyInt32.IsPresent()) + require.False(t, EmptyInt64.IsPresent()) + require.False(t, EmptyUint.IsPresent()) + require.False(t, EmptyUint8.IsPresent()) + require.False(t, EmptyUint16.IsPresent()) + require.False(t, EmptyUint32.IsPresent()) + require.False(t, EmptyUint64.IsPresent()) + require.False(t, EmptyBool.IsPresent()) + require.False(t, EmptyFloat32.IsPresent()) + require.False(t, EmptyFloat64.IsPresent()) + require.False(t, EmptyByte.IsPresent()) + require.False(t, EmptyRune.IsPresent()) +}