diff --git a/README.md b/README.md index 1b34748..06bc1fe 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ func main() { Method and description Returns + AsEmpty()
@@ -136,6 +137,7 @@ func main() { *Optional[T] + Clear()
@@ -144,6 +146,15 @@ func main() { *Optional[T] + + + + Default(v T)
+ returns the value if present, otherwise returns the provided default value + + T + + Filter(f func(v T) bool)
@@ -152,6 +163,7 @@ func main() { *Optional[T] + Get()
@@ -159,6 +171,7 @@ func main() { (T, error) + GetOk()
@@ -167,6 +180,7 @@ func main() { (T, bool) + IfElse(condition bool, other T)
@@ -175,6 +189,7 @@ func main() { T + IfPresent(f func(v T))
@@ -183,6 +198,7 @@ func main() { *Optional[T] + IfPresentOtherwise(f func(v T), other func())
@@ -191,6 +207,7 @@ func main() { *Optional[T] + IfSet(f func(v T), notPresent func())
@@ -201,6 +218,7 @@ func main() { *Optional[T] + IfSetOtherwise(f func(v T), notPresent func(), other func())
@@ -211,6 +229,7 @@ func main() { *Optional[T] + IsPresent()
@@ -218,6 +237,7 @@ func main() { bool + Map(f func(v T) any)
@@ -227,6 +247,7 @@ func main() { *Optional[any] + MarshalJSON()
@@ -236,6 +257,7 @@ func main() { ([]byte, error) + OrElse(other T)
@@ -243,6 +265,7 @@ func main() { T + OrElseError(err error)
@@ -250,6 +273,7 @@ func main() { error + OrElseGet(f func() T)
@@ -257,6 +281,7 @@ func main() { T + OrElsePanic(v any)
@@ -265,6 +290,7 @@ func main() { *Optional[T] + OrElseSet(v T)
@@ -272,6 +298,7 @@ func main() { *Optional[T] + Scan(value interface{})
@@ -279,6 +306,7 @@ func main() { error + UnSet()
@@ -287,6 +315,7 @@ func main() { *Optional[T] + UnmarshalJSON(data []byte)
@@ -297,6 +326,7 @@ func main() { error + WasSet()
@@ -306,6 +336,7 @@ func main() { bool + WasSetElse(other T)
@@ -313,6 +344,7 @@ func main() { T + WasSetElseError(err error)
@@ -321,6 +353,7 @@ func main() { error + WasSetElseGet(f func() T)
@@ -329,6 +362,7 @@ func main() { T + WasSetElsePanic(v any)
@@ -337,6 +371,7 @@ func main() { *Optional[T] + WasSetElseSet(v T)
@@ -398,7 +433,7 @@ func main() { EmptyInt8() *Optional[int8]
-7 returns an empty optional of type int8 + returns an empty optional of type int8 diff --git a/maps.go b/maps.go index 601edb1..eb44475 100644 --- a/maps.go +++ b/maps.go @@ -1,32 +1,94 @@ package gopt +// Extract extracts an optional value, of the specified type, from a map +// +// If the key is present (and the value is non-nil and of the specified type) then an optional with the value is returned, otherwise an empty optional is returned +func Extract[K comparable, T any](m map[K]any, key K) *Optional[T] { + result := Empty[T]() + if rv, ok := m[key]; ok && isPresent(rv) { + if v, ok := rv.(T); ok { + result = Of[T](v) + } + } + return result +} + +// ExtractJson extracts an optional value, of the specified type, from a map[string]interface{} +// +// If the key is present (and the value is non-nil and of the specified type) then an optional with the value is returned, otherwise an empty optional is returned +func ExtractJson[T any](m map[string]interface{}, key string) *Optional[T] { + result := Empty[T]() + if rv, ok := m[key]; ok && isPresent(rv) { + if v, ok := rv.(T); ok { + result = Of[T](v) + } + } + return result +} + +// Get obtains an optional from a map +// +// If the key is present (and the value is non-nil) then an optional with the value is returned, otherwise an empty optional is returned +func Get[K comparable, T any](m map[K]T, key K) *Optional[T] { + if v, ok := m[key]; ok && isPresent(v) { + return Of(v) + } + return Empty[T]() +} + // OptMap can be used to cast an existing map for optional/functional methods type OptMap[K comparable, V any] map[K]V // Get returns an optional of the value in the map // // If the value is not present (or is nil) then an empty optional is returned -func (m OptMap[K, V]) Get(k K) *Optional[V] { - return Map(m, k) +func (m OptMap[K, V]) Get(key K) *Optional[V] { + return Get(m, key) } // Default returns the value (if present and non-nil) otherwise returns the default value -func (m OptMap[K, V]) Default(k K, def V) V { - return Map(m, k).Default(def) +func (m OptMap[K, V]) Default(key K, def V) V { + return Get(m, key).Default(def) +} + +// IfPresent if the key is present (and the value is non-nil) calls the supplied function with the key and value +// +// otherwise does nothing +func (m OptMap[K, V]) IfPresent(key K, f func(key K, v V)) OptMap[K, V] { + if f != nil { + if v, ok := m[key]; ok && isPresent(v) { + f(key, v) + } + } + return m +} + +// IfPresentOtherwise if the key is present (and the value is non-nil) calls the supplied function with the key and value +// +// otherwise calls the other function with they key +func (m OptMap[K, V]) IfPresentOtherwise(key K, f func(key K, v V), other func(key K)) OptMap[K, V] { + if v, ok := m[key]; ok && isPresent(v) { + if f != nil { + f(key, v) + } + } else if other != nil { + other(key) + } + return m } // ComputeIfAbsent if the specified key is not present (or the value is nil) sets the value according to the specified function // // returns either the existing value or the newly set value -func (m OptMap[K, V]) ComputeIfAbsent(k K, f func(k K) V) V { - if v, ok := m[k]; ok && isPresent(v) { +func (m OptMap[K, V]) ComputeIfAbsent(key K, f func(key K) V) V { + if v, ok := m[key]; ok && isPresent(v) { return v } var rv V if f != nil { - rv = f(k) + rv = f(key) if isPresent(rv) { - m[k] = rv + m[key] = rv } } return rv @@ -35,14 +97,14 @@ func (m OptMap[K, V]) ComputeIfAbsent(k K, f func(k K) V) V { // ComputeIfPresent if the specified key is present (and the value is non-nil) attempts to compute a new mapping using the supplied function // // If the supplied function is called but returns a nil value, the key is deleted -func (m OptMap[K, V]) ComputeIfPresent(k K, f func(k K, v V) V) V { +func (m OptMap[K, V]) ComputeIfPresent(key K, f func(key K, v V) V) V { var rv V - if v, ok := m[k]; ok && isPresent(v) && f != nil { - rv = f(k, v) + if v, ok := m[key]; ok && isPresent(v) && f != nil { + rv = f(key, v) if isPresent(rv) { - m[k] = rv + m[key] = rv } else { - delete(m, k) + delete(m, key) } } return rv @@ -51,9 +113,9 @@ func (m OptMap[K, V]) ComputeIfPresent(k K, f func(k K, v V) V) V { // PutIfAbsent if the specified key if absent (not present or nil-value) it is set to the specified value // // returns true if the value was set -func (m OptMap[K, V]) PutIfAbsent(k K, v V) bool { - if ov, ok := m[k]; !ok || !isPresent(ov) { - m[k] = v +func (m OptMap[K, V]) PutIfAbsent(key K, v V) bool { + if ov, ok := m[key]; !ok || !isPresent(ov) { + m[key] = v return true } return false @@ -64,14 +126,14 @@ func (m OptMap[K, V]) PutIfAbsent(k K, v V) bool { // returns true if the value was replaced // // If the specified replacement value is nil and -func (m OptMap[K, V]) ReplaceIfPresent(k K, v V) bool { +func (m OptMap[K, V]) ReplaceIfPresent(key K, v V) bool { if isPresent(v) { - if ov, ok := m[k]; ok && isPresent(ov) { - m[k] = v + if ov, ok := m[key]; ok && isPresent(ov) { + m[key] = v return true } - } else if _, ok := m[k]; ok { - delete(m, k) + } else if _, ok := m[key]; ok { + delete(m, key) return true } return false diff --git a/maps_test.go b/maps_test.go index 2a20d31..6cfc4d1 100644 --- a/maps_test.go +++ b/maps_test.go @@ -3,8 +3,107 @@ package gopt import ( "github.com/stretchr/testify/require" "testing" + "time" ) +func TestExtract(t *testing.T) { + m := map[string]interface{}{ + "str": "Str", + "int": 16, + "time": time.Now(), + } + + s := Extract[string, string](m, "str") + require.True(t, s.IsPresent()) + require.Equal(t, "Str", s.OrElse("")) + + s = Extract[string, string](m, "int") + require.False(t, s.IsPresent()) + + i := Extract[string, int](m, "str") + require.False(t, i.IsPresent()) + + i = Extract[string, int](m, "int") + require.True(t, i.IsPresent()) + require.Equal(t, 16, i.OrElse(0)) + + dt := Extract[string, time.Time](m, "time") + require.True(t, dt.IsPresent()) + + dt = Extract[string, time.Time](m, "str") + require.False(t, dt.IsPresent()) + + m2 := map[int]interface{}{ + 1: "Str", + 2: 16, + } + s = Extract[int, string](m2, 1) + require.True(t, s.IsPresent()) + require.Equal(t, "Str", s.OrElse("")) + s = Extract[int, string](m2, 2) + require.False(t, s.IsPresent()) + i = Extract[int, int](m2, 2) + require.True(t, i.IsPresent()) + require.Equal(t, 16, i.OrElse(0)) + i = Extract[int, int](m2, 1) + require.False(t, i.IsPresent()) +} + +func TestExtractJson(t *testing.T) { + m := map[string]interface{}{ + "str": "Str", + "int": 16, + "time": time.Now(), + } + + s := ExtractJson[string](m, "str") + require.True(t, s.IsPresent()) + require.Equal(t, "Str", s.OrElse("")) + + s = ExtractJson[string](m, "int") + require.False(t, s.IsPresent()) + + i := ExtractJson[int](m, "str") + require.False(t, i.IsPresent()) + + i = ExtractJson[int](m, "int") + require.True(t, i.IsPresent()) + require.Equal(t, 16, i.OrElse(0)) + + dt := ExtractJson[time.Time](m, "time") + require.True(t, dt.IsPresent()) + + dt = ExtractJson[time.Time](m, "str") + require.False(t, dt.IsPresent()) +} + +func TestGet(t *testing.T) { + m := map[string]interface{}{ + "foo": "foo value", + "bar": 1, + } + o := Get(m, "foo") + require.True(t, o.IsPresent()) + require.Equal(t, "foo value", o.OrElse(nil)) + o = Get(m, "bar") + require.True(t, o.IsPresent()) + require.Equal(t, 1, o.OrElse(nil)) + o = Get(m, "baz") + require.False(t, o.IsPresent()) + require.Equal(t, "", o.OrElse("")) + + m2 := map[int]*myStruct{ + 1: {}, + 2: nil, + } + o2 := Get(m2, 1) + require.True(t, o2.IsPresent()) + o2 = Get(m2, 2) + require.False(t, o2.IsPresent()) + o2 = Get(m2, 3) + require.False(t, o2.IsPresent()) +} + func TestOptMap_Get(t *testing.T) { m := map[string]interface{}{ "foo": "foo value", @@ -19,7 +118,64 @@ func TestOptMap_Get(t *testing.T) { require.False(t, ov.IsPresent()) } -func TestOptMap_GetOrDefault(t *testing.T) { +func TestOptMap_IfPresent(t *testing.T) { + m := map[string]interface{}{ + "foo": "foo value", + "bar": nil, + } + om := OptMap[string, interface{}](m) + called := false + f := func(key string, v interface{}) { + called = true + } + om.IfPresent("foo", f) + require.True(t, called) + called = false + om.IfPresent("bar", f) + require.False(t, called) + om.IfPresent("baz", f) + require.False(t, called) + + om.IfPresent("bar", f).IfPresent("foo", f) + require.True(t, called) +} + +func TestOptMap_IfPresentOtherwise(t *testing.T) { + m := map[string]interface{}{ + "foo": "foo value", + "bar": nil, + } + om := OptMap[string, interface{}](m) + called := false + otherCalled := false + f := func(key string, v interface{}) { + called = true + } + other := func(key string) { + otherCalled = true + } + om.IfPresentOtherwise("foo", f, other) + require.True(t, called) + require.False(t, otherCalled) + called = false + otherCalled = false + om.IfPresentOtherwise("bar", f, other) + require.False(t, called) + require.True(t, otherCalled) + called = false + otherCalled = false + om.IfPresentOtherwise("baz", f, other) + require.False(t, called) + require.True(t, otherCalled) + + called = false + otherCalled = false + om.IfPresentOtherwise("bar", f, other).IfPresentOtherwise("foo", f, other) + require.True(t, called) + require.True(t, otherCalled) +} + +func TestOptMap_Default(t *testing.T) { m := map[string]interface{}{ "foo": "foo value", "bar": nil, diff --git a/optional.go b/optional.go index 93d1a27..c7a67d8 100644 --- a/optional.go +++ b/optional.go @@ -52,6 +52,30 @@ func Empty[T any]() *Optional[T] { } } +// MapTo maps the supplied optional to the specified type (if present and can can be converted) and returns an optional of the mapped value +func MapTo[T any](o *Optional[any]) *Optional[T] { + if o != nil && o.present { + if v, ok := o.value.(T); ok { + return Of[T](v) + } + } + return Empty[T]() +} + +// ConvertTo converts the supplied optional to the specified type (if present and can can be converted) and returns an optional of converted value +// +// If the present value cannot be converted, the supplied conversion function is called +func ConvertTo[F any, T any](o *Optional[F], f func(v F) *Optional[T]) *Optional[T] { + if o != nil && o.present { + if v, ok := any(o.value).(T); ok { + return Of[T](v) + } else if f != nil { + return f(o.value) + } + } + return Empty[T]() +} + func isPresent(v any) bool { vo := reflect.ValueOf(v) switch vk := vo.Kind(); vk { @@ -110,7 +134,7 @@ func (o *Optional[T]) Get() (T, error) { return o.value, nil } -// Default returns the value if present, otherwise returns the defaulted value +// Default returns the value if present, otherwise returns the provided default value func (o *Optional[T]) Default(v T) T { if !o.present { return v @@ -194,7 +218,7 @@ func (o *Optional[T]) IsPresent() bool { return o.present } -// Map if the value is present and the result of calling the supplied mapping function returns non-nil, returns +// Get 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 @@ -290,18 +314,24 @@ func (o *Optional[T]) Scan(value interface{}) error { return err } else if bd, ok := value.([]byte); ok { var uv T - if unErr := json.Unmarshal(bd, &uv); unErr == nil { - if isPresent(uv) { - o.present = true - o.value = uv - } else { - o.present = false - o.value = o.emptyValue() - } + if o.isString() { + o.present = true o.set = true + o.value = any(string(bd)).(T) } else { - o.clear(true) - return unErr + if unErr := json.Unmarshal(bd, &uv); unErr == nil { + if isPresent(uv) { + o.present = true + o.value = uv + } else { + o.present = false + o.value = o.emptyValue() + } + o.set = true + } else { + o.clear(true) + return unErr + } } } else { o.clear(true) @@ -415,6 +445,10 @@ func (o *Optional[T]) clear(set bool) { o.set = set } +func (o *Optional[T]) isString() bool { + return reflect.TypeOf(o.value).Kind() == reflect.String +} + func (o *Optional[T]) callScannable(value interface{}) (bool, error) { var nv reflect.Value if !isPresent(o.value) { @@ -525,11 +559,3 @@ func EmptyByte() *Optional[byte] { func EmptyRune() *Optional[rune] { return Empty[rune]() } - -// Map obtains an optional from a map - if the key is present (and the value is non-nil) then an optional with the value is returned, otherwise an empty optional is returned -func Map[K comparable, T any](m map[K]T, key K) *Optional[T] { - if v, ok := m[key]; ok && isPresent(v) { - return Of(v) - } - return Empty[T]() -} diff --git a/optional_test.go b/optional_test.go index 20e714f..edcf280 100644 --- a/optional_test.go +++ b/optional_test.go @@ -76,6 +76,38 @@ func TestOf(t *testing.T) { require.False(t, opt3.IsPresent()) } +func TestMapTo(t *testing.T) { + o1 := Of[interface{}]("foo") + o2 := MapTo[string](o1) + require.True(t, o2.IsPresent()) + require.Equal(t, "foo", o2.OrElse("")) + + o1 = Of[interface{}](1) + o2 = MapTo[string](o1) + require.False(t, o2.IsPresent()) +} + +func TestConvertTo(t *testing.T) { + o1 := Of[interface{}]("foo") + o2 := ConvertTo[interface{}, string](o1, nil) + require.True(t, o2.IsPresent()) + require.Equal(t, "foo", o2.OrElse("")) + + o1 = Of[interface{}](16) + o2 = ConvertTo[interface{}, string](o1, nil) + require.False(t, o2.IsPresent()) + f := func(v interface{}) *Optional[string] { + switch vt := v.(type) { + case int: + return Of(strconv.Itoa(vt)) + } + return EmptyString() + } + o2 = ConvertTo[interface{}, string](o1, f) + require.True(t, o2.IsPresent()) + require.Equal(t, "16", o2.OrElse("")) +} + func TestOptional_AsEmpty(t *testing.T) { o := Of("abc") require.True(t, o.IsPresent()) @@ -532,6 +564,15 @@ func TestOptional_Scan(t *testing.T) { require.NoError(t, err) require.False(t, o5.IsPresent()) require.True(t, o5.WasSet()) + + o6 := Empty[string]() + require.False(t, o6.IsPresent()) + require.False(t, o6.WasSet()) + err = o6.Scan([]uint8{'a', 'b', 'c'}) + require.NoError(t, err) + require.True(t, o6.IsPresent()) + require.True(t, o6.WasSet()) + require.Equal(t, "abc", o6.OrElse("")) } func TestOptional_UnSet(t *testing.T) { @@ -681,33 +722,6 @@ func TestOptional_Default(t *testing.T) { require.Equal(t, "bar", v4.Foo) } -func TestMap(t *testing.T) { - m := map[string]interface{}{ - "foo": "foo value", - "bar": 1, - } - o := Map(m, "foo") - require.True(t, o.IsPresent()) - require.Equal(t, "foo value", o.OrElse(nil)) - o = Map(m, "bar") - require.True(t, o.IsPresent()) - require.Equal(t, 1, o.OrElse(nil)) - o = Map(m, "baz") - require.False(t, o.IsPresent()) - require.Equal(t, "", o.OrElse("")) - - m2 := map[int]*myStruct{ - 1: {}, - 2: nil, - } - o2 := Map(m2, 1) - require.True(t, o2.IsPresent()) - o2 = Map(m2, 2) - require.False(t, o2.IsPresent()) - o2 = Map(m2, 3) - require.False(t, o2.IsPresent()) -} - type scannable struct { called bool err error