From 16302c7c817613c9cf27b5f1b7267484b573ab0c Mon Sep 17 00:00:00 2001 From: Luiz Ferraz Date: Fri, 8 Jul 2022 00:03:27 -0300 Subject: [PATCH] Implement options package --- options/transforms.go | 53 +++++++++ options/transforms_example_test.go | 117 ++++++++++++++++++++ options/transforms_test.go | 166 +++++++++++++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 options/transforms.go create mode 100644 options/transforms_example_test.go create mode 100644 options/transforms_test.go diff --git a/options/transforms.go b/options/transforms.go new file mode 100644 index 0000000..7a41b5b --- /dev/null +++ b/options/transforms.go @@ -0,0 +1,53 @@ +// Package options provides cross type transformations for `mo.Option`. +// +// The functions provided by this package are not methods of `mo.Option` due to the lack of method type parameters +// on methods. This is part of the design decision of the Go's generics as explained here: +// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods +// +// Providing these methods as a separate package also matches Go's primitives and standard library: +// - The `string` type don't have methods, but there we have the `strings` package. +// - The `[]byte` type don't have methods, but there we have the `bytes` package. +// - The `io.Reader` defines a single method, and all manipulations of a reader is done on packages `io` and `ioutil`. +package options + +import ( + "github.com/samber/mo" +) + +// Map returns a new `mo.Option` wrapping the result of applying `f` to the value of opt, if present, and None otherwise. +func Map[I any, O any](opt mo.Option[I], f func(I) O) mo.Option[O] { + if val, ok := opt.Get(); ok { + return mo.Some(f(val)) + } + + return mo.None[O]() +} + +// FlatMap returns the result of applying `f` to the value of opt, if present, and None otherwise. +func FlatMap[I any, O any](opt mo.Option[I], f func(I) mo.Option[O]) mo.Option[O] { + if val, ok := opt.Get(); ok { + return f(val) + } + + return mo.None[O]() +} + +// Match returns a new `mo.Option` from the result of applying `onValue` to the value of opt, if present, +// or from the result of calling `onNone` if absent. +func Match[I any, O any](opt mo.Option[I], onValue func(I) (O, bool), onNone func() (O, bool)) mo.Option[O] { + if val, ok := opt.Get(); ok { + return mo.TupleToOption(onValue(val)) + } + + return mo.TupleToOption(onNone()) +} + +// FlatMatch returns the result of applying `onValue` to the value of opt, if present, +// or the result of `onNone` if absent. +func FlatMatch[I any, O any](opt mo.Option[I], onValue func(I) O, onNone func() O) O { + if val, ok := opt.Get(); ok { + return onValue(val) + } + + return onNone() +} diff --git a/options/transforms_example_test.go b/options/transforms_example_test.go new file mode 100644 index 0000000..cec45ec --- /dev/null +++ b/options/transforms_example_test.go @@ -0,0 +1,117 @@ +package options_test + +import ( + "fmt" + "strings" + + "github.com/samber/mo" + "github.com/samber/mo/options" +) + +func ExampleMap_some() { + some := mo.Some("hello world") + result := options.Map(some, func(v string) int { return len(v) }) + + fmt.Printf("%t -> %d", result.IsPresent(), result.OrEmpty()) + // Output: true -> 11 +} + +func ExampleMap_none() { + none := mo.None[string]() + result := options.Map(none, func(v string) int { return len(v) }) + + fmt.Printf("%t -> %d", result.IsPresent(), result.OrEmpty()) + // Output: false -> 0 +} + +func ExampleFlatMap_some() { + some := mo.Some("hello world") + result := options.FlatMap(some, func(v string) mo.Option[[]string] { + if len(v) > 0 { + return mo.Some(strings.Fields(v)) + } + + return mo.None[[]string]() + }) + + fmt.Printf("%t -> %q", result.IsPresent(), result.OrEmpty()) + // Output: true -> ["hello" "world"] +} + +func ExampleFlatMap_none() { + none := mo.None[string]() + result := options.FlatMap(none, func(v string) mo.Option[[]string] { + if len(v) > 0 { + return mo.Some(strings.Fields(v)) + } + + return mo.None[[]string]() + }) + + fmt.Printf("%t -> %q", result.IsPresent(), result.OrEmpty()) + // Output: false -> [] +} + +func ExampleMatch_some() { + some := mo.Some(42) + result := options.Match( + some, + func(i int) (string, bool) { + return fmt.Sprintf("The number is %d", i), true + }, + func() (string, bool) { + return "none", false + }, + ) + + fmt.Printf("%t -> %q", result.IsPresent(), result.OrEmpty()) + // Output: true -> "The number is 42" +} + +func ExampleMatch_none() { + none := mo.None[int]() + result := options.Match( + none, + func(i int) (string, bool) { + return fmt.Sprintf("The number is %d", i), false + }, + func() (string, bool) { + return "No value", true + }, + ) + + fmt.Printf("%t -> %q", result.IsPresent(), result.OrEmpty()) + // Output: true -> "No value" +} + +func ExampleFlatMatch_some() { + some := mo.Some(42) + result := options.FlatMatch( + some, + func(i int) string { + return fmt.Sprintf("The number is %d", i) + }, + func() string { + return "none" + }, + ) + + fmt.Printf("%q", result) + // Output: "The number is 42" +} + +func ExampleFlatMatch_none() { + none := mo.None[int]() + result := options.FlatMatch( + none, + func(i int) string { + return fmt.Sprintf("The number is %d", i) + }, + func() string { + return "No value" + }, + ) + + fmt.Printf("%q", result) + // Output: "No value" +} diff --git a/options/transforms_test.go b/options/transforms_test.go new file mode 100644 index 0000000..1c43be5 --- /dev/null +++ b/options/transforms_test.go @@ -0,0 +1,166 @@ +package options_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/samber/mo" + "github.com/samber/mo/options" +) + +func nonEmptyFields(s string) mo.Option[[]string] { + fields := strings.Fields(s) + if len(fields) > 0 { + return mo.Some(fields) + } + return mo.None[[]string]() +} + +func TestMap(t *testing.T) { + t.Run("Some", func(t *testing.T) { + original := mo.Some("hello world") + + mapped := options.Map(original, strings.Fields) + + assert.Equal(t, mo.Some([]string{"hello", "world"}), mapped) + }) + t.Run("None", func(t *testing.T) { + original := mo.None[string]() + + mapped := options.Map(original, strings.Fields) + + assert.Equal(t, mo.None[[]string](), mapped) + }) +} + +func TestFlatMap(t *testing.T) { + t.Run("Some-to-Some", func(t *testing.T) { + original := mo.Some("hello world") + + mapped := options.FlatMap(original, nonEmptyFields) + + assert.Equal(t, mo.Some([]string{"hello", "world"}), mapped) + }) + t.Run("Some-to-None", func(t *testing.T) { + original := mo.Some("") + + mapped := options.FlatMap(original, nonEmptyFields) + + assert.Equal(t, mo.None[[]string](), mapped) + }) + t.Run("None", func(t *testing.T) { + original := mo.None[string]() + + mapped := options.FlatMap(original, nonEmptyFields) + + assert.Equal(t, mo.None[[]string](), mapped) + }) +} + +func TestMatch(t *testing.T) { + t.Run("Some-to-Present", func(t *testing.T) { + original := mo.Some("hello world") + + result := options.Match( + original, + func(val string) (int, bool) { + assert.Equal(t, "hello world", val) + return 1234, true + }, + func() (int, bool) { + require.Fail(t, "should not be called") + return 0, false + }, + ) + + assert.Equal(t, mo.Some(1234), result) + }) + t.Run("Some-to-Absent", func(t *testing.T) { + original := mo.Some("hello world") + + result := options.Match( + original, + func(val string) (int, bool) { + assert.Equal(t, "hello world", val) + return 0, false + }, + func() (int, bool) { + require.Fail(t, "should not be called") + return 0, false + }, + ) + + assert.Equal(t, mo.None[int](), result) + }) + t.Run("None-to-Present", func(t *testing.T) { + original := mo.None[string]() + + result := options.Match( + original, + func(val string) (int, bool) { + require.Fail(t, "should not be called") + return 0, false + }, + func() (int, bool) { + return 1234, true + }, + ) + + assert.Equal(t, mo.Some(1234), result) + }) + t.Run("None-to-Absent", func(t *testing.T) { + original := mo.None[string]() + + result := options.Match( + original, + func(val string) (int, bool) { + require.Fail(t, "should not be called") + return 0, false + }, + func() (int, bool) { + return 0, false + }, + ) + + assert.Equal(t, mo.None[int](), result) + }) +} + +func TestFlatMatch(t *testing.T) { + t.Run("Some", func(t *testing.T) { + original := mo.Some("hello world") + + result := options.FlatMatch( + original, + func(val string) int { + assert.Equal(t, "hello world", val) + return 1234 + }, + func() int { + require.Fail(t, "should not be called") + return 0 + }, + ) + + assert.Equal(t, 1234, result) + }) + t.Run("None", func(t *testing.T) { + original := mo.None[string]() + + result := options.FlatMatch( + original, + func(val string) int { + require.Fail(t, "should not be called") + return 0 + }, + func() int { + return 1234 + }, + ) + + assert.Equal(t, 1234, result) + }) +}