From 691ceb067590005ebeb482557601753e24270dfe Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Sun, 9 Jul 2023 10:21:56 +0200 Subject: [PATCH] fix: add json serialization support for option Signed-off-by: Dr. Carsten Leue --- either/core.go | 36 ++++++++++++------------------ either/either_test.go | 6 +++++ option/core.go | 51 ++++++++++++++++++------------------------- option/option_test.go | 45 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 52 deletions(-) diff --git a/either/core.go b/either/core.go index b278812..1b30411 100644 --- a/either/core.go +++ b/either/core.go @@ -2,29 +2,21 @@ package either import "fmt" -type EitherTag string - -const ( - leftTag = "Left" - rightTag = "Right" +// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases +type ( + Either[E, A any] struct { + isLeft bool + left E + right A + } ) -// Either defines a data structure that logically holds either an E or an A. The tag discriminates the cases -type Either[E, A any] struct { - tag EitherTag `default:"Left"` - left E - right A -} - // String prints some debug info for the object func (s Either[E, A]) String() string { - switch s.tag { - case leftTag: - return fmt.Sprintf("%s[%T, %T](%v)", s.tag, s.left, s.right, s.left) - case rightTag: - return fmt.Sprintf("%s[%T, %T](%v)", s.tag, s.left, s.right, s.right) + if s.isLeft { + return fmt.Sprintf("Left[%T, %T](%v)", s.left, s.right, s.left) } - return "Invalid" + return fmt.Sprintf("Right[%T, %T](%v)", s.left, s.right, s.right) } // Format prints some debug info for the object @@ -38,19 +30,19 @@ func (s Either[E, A]) Format(f fmt.State, c rune) { } func IsLeft[E, A any](val Either[E, A]) bool { - return val.tag == leftTag + return val.isLeft } func IsRight[E, A any](val Either[E, A]) bool { - return val.tag == rightTag + return !val.isLeft } func Left[E, A any](value E) Either[E, A] { - return Either[E, A]{tag: leftTag, left: value} + return Either[E, A]{isLeft: true, left: value} } func Right[E, A any](value A) Either[E, A] { - return Either[E, A]{tag: rightTag, right: value} + return Either[E, A]{isLeft: false, right: value} } func MonadFold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a A) B) B { diff --git a/either/either_test.go b/either/either_test.go index dc416ed..4714df9 100644 --- a/either/either_test.go +++ b/either/either_test.go @@ -11,6 +11,12 @@ import ( "github.com/stretchr/testify/assert" ) +func TestDefault(t *testing.T) { + var e Either[error, string] + + assert.Equal(t, Of[error](""), e) +} + func TestIsLeft(t *testing.T) { err := errors.New("Some error") withError := Left[error, string](err) diff --git a/option/core.go b/option/core.go index 66c3ff8..6a4ead2 100644 --- a/option/core.go +++ b/option/core.go @@ -6,32 +6,22 @@ import ( "fmt" ) -type OptionTag string - -const ( - noneTag = "None" - someTag = "Some" -) - var ( jsonNull = []byte("null") ) // Option defines a data structure that logically holds a value or not type Option[A any] struct { - tag OptionTag `default:"None"` - value A + isSome bool + some A } // String prints some debug info for the object func (s Option[A]) String() string { - switch s.tag { - case noneTag: - return fmt.Sprintf("%s[%T]", s.tag, s.value) - case someTag: - return fmt.Sprintf("%s[%T](%v)", s.tag, s.value, s.value) + if s.isSome { + return fmt.Sprintf("Some[%T](%v)", s.some, s.some) } - return "Invalid" + return fmt.Sprintf("None[%T]", s.some) } // Format prints some debug info for the object @@ -45,28 +35,29 @@ func (s Option[A]) Format(f fmt.State, c rune) { } func (s Option[A]) MarshalJSON() ([]byte, error) { - if s.tag == noneTag { - return jsonNull, nil + if IsSome(s) { + return json.Marshal(s.some) } - return json.Marshal(s.value) + return jsonNull, nil } -func (s Option[A]) UnmarshalJSON(data []byte) error { +func (s *Option[A]) UnmarshalJSON(data []byte) error { // decode the value if bytes.Equal(data, jsonNull) { - s.tag = noneTag + s.isSome = false + s.some = *new(A) return nil } - s.tag = someTag - return json.Unmarshal(data, &s.value) + s.isSome = true + return json.Unmarshal(data, &s.some) } func IsNone[T any](val Option[T]) bool { - return val.tag == noneTag + return !val.isSome } func Some[T any](value T) Option[T] { - return Option[T]{tag: someTag, value: value} + return Option[T]{isSome: true, some: value} } func Of[T any](value T) Option[T] { @@ -74,20 +65,20 @@ func Of[T any](value T) Option[T] { } func None[T any]() Option[T] { - return Option[T]{tag: noneTag} + return Option[T]{isSome: false} } func IsSome[T any](val Option[T]) bool { - return val.tag == someTag + return val.isSome } func MonadFold[A, B any](ma Option[A], onNone func() B, onSome func(A) B) B { - if IsNone(ma) { - return onNone() + if IsSome(ma) { + return onSome(ma.some) } - return onSome(ma.value) + return onNone() } func Unwrap[A any](ma Option[A]) (A, bool) { - return ma.value, ma.tag == someTag + return ma.some, ma.isSome } diff --git a/option/option_test.go b/option/option_test.go index b1ec678..4345bb1 100644 --- a/option/option_test.go +++ b/option/option_test.go @@ -1,14 +1,59 @@ package option import ( + "encoding/json" "fmt" "testing" F "github.com/ibm/fp-go/function" "github.com/ibm/fp-go/internal/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +type ( + SampleData struct { + Value string + OptValue Option[string] + } +) + +func TestJson(t *testing.T) { + + sample := SampleData{ + Value: "value", + OptValue: Of("optValue"), + } + + data, err := json.Marshal(&sample) + require.NoError(t, err) + + var deser SampleData + err = json.Unmarshal(data, &deser) + require.NoError(t, err) + + assert.Equal(t, sample, deser) + + sample = SampleData{ + Value: "value", + OptValue: None[string](), + } + + data, err = json.Marshal(&sample) + require.NoError(t, err) + + err = json.Unmarshal(data, &deser) + require.NoError(t, err) + + assert.Equal(t, sample, deser) +} + +func TestDefault(t *testing.T) { + var e Option[string] + + assert.Equal(t, None[string](), e) +} + func TestReduce(t *testing.T) { assert.Equal(t, 2, F.Pipe1(None[int](), Reduce(utils.Sum, 2)))