Skip to content

Commit

Permalink
feat(core): json serialization/deserialization and refactoring
Browse files Browse the repository at this point in the history
Option values are now wrapped in a struct, replacing previous empty interface handling. Also, custom functions for handling equality have been introduced, utilizing 'github.com/google/go-cmp/cmp'. This change supports cleaner code and comparisons. In addition, option values can now be serialized/deserialized to/from JSON. This upgrade enhances the data interchangeability and compatibility of the package with JSON data formats.
  • Loading branch information
martianoff committed Jun 30, 2024
1 parent 848fc55 commit a106af3
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 52 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ carPlateOpt := option.FlatMap[Car, string](u.car, func(c Car) option.Option[stri
| String() | string representation |
`* - empty value will panic`

## Json serialization and deserialization

Build-in serialization and deserialization

## Custom equality

Custom equality powered by `github.com/google/go-cmp/cmp`

```
cmp.Equal(Some(11), Some(11)) // returns true
```

---
## Testing

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.8.4

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand Down
26 changes: 26 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package option

import (
"encoding/json"
"strings"
)

func (opt *Option[T]) UnmarshalJSON(data []byte) error {
if strings.ToLower(string(data)) == "null" {
*opt = Option[T]{None[T]()}
return nil
}
var v T
if err := json.Unmarshal(data, &v); err != nil {
return err
}
*opt = Option[T]{Some[T](v)}
return nil
}

func (opt Option[T]) MarshalJSON() ([]byte, error) {
if opt.Empty() {
return json.Marshal(nil)
}
return json.Marshal(opt.Get())
}
84 changes: 84 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package option

import (
"github.com/google/go-cmp/cmp"
"testing"
)

type T = int

func TestOption_UnmarshalJSON(t *testing.T) {
cases := []struct {
name string
jsonStr string
expected Option[T]
expectedErr bool
}{
{
name: "Test UnmarshalJSON with null value",
jsonStr: "null",
expected: None[T](),
expectedErr: false,
},
{
name: "Test UnmarshalJSON with normal value",
jsonStr: "123",
expected: Some[T](123),
expectedErr: false,
},
{
name: "Test UnmarshalJSON with invalid value",
jsonStr: "-",
expected: None[T](),
expectedErr: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var actual Option[T]
err := actual.UnmarshalJSON([]byte(tc.jsonStr))
if !tc.expectedErr {
if err != nil {
t.Errorf("UnmarshalJSON() error = %v", err)
}
if !cmp.Equal(actual, tc.expected) {
t.Errorf("UnmarshalJSON() = %v, want %v", actual, tc.expected)
}
} else if err == nil {
t.Error("UnmarshalJSON() expected error, but there is no error")
}
})
}
}

func TestOption_MarshalJSON(t *testing.T) {
cases := []struct {
name string
obj Option[T]
expected string
}{
{
name: "Test MarshalJSON with None value",
obj: None[T](),
expected: "null",
},
{
name: "Test MarshalJSON with Some value",
obj: Some[T](123),
expected: "123",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual, err := tc.obj.MarshalJSON()
if err != nil {
t.Errorf("MarshalJSON() error = %v", err)
}
if string(actual) != tc.expected {
t.Errorf("MarshalJSON() = %v, want %v", string(actual), tc.expected)
}
})
}
}
26 changes: 13 additions & 13 deletions none_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ func TestNone_Empty(t *testing.T) {
name string
want bool
}{
{name: "optNone[T] Empty() returns true", want: true},
{name: "None[T] Empty() returns true", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := optNone[int]{}
n := None[int]()
if got := n.Empty(); got != tt.want {
t.Errorf("Empty() = %v, want %v", got, tt.want)
}
Expand All @@ -28,11 +28,11 @@ func TestNone_Get(t *testing.T) {
name string
want int
}{
{name: "optNone[T] Get throws an exception"},
{name: "None[T] Get throws an exception"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := optNone[int]{}
n := None[int]()
assert.Panics(t, func() { n.Get() })
})
}
Expand All @@ -47,11 +47,11 @@ func TestNone_GetOrElse(t *testing.T) {
args args
want int
}{
{name: "optNone[T] GetOrElse() returns else value", args: args{v: 2}, want: 2},
{name: "None[T] GetOrElse() returns else value", args: args{v: 2}, want: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := optNone[int]{}
n := None[int]()
if got := n.GetOrElse(tt.args.v); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetOrElse() = %v, want %v", got, tt.want)
}
Expand All @@ -64,11 +64,11 @@ func TestNone_NonEmpty(t *testing.T) {
name string
want bool
}{
{name: "optNone[T] Empty() returns false", want: false},
{name: "None[T] Empty() returns false", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := optNone[int]{}
n := None[int]()
if got := n.NonEmpty(); got != tt.want {
t.Errorf("NonEmpty() = %v, want %v", got, tt.want)
}
Expand All @@ -85,12 +85,12 @@ func TestNone_OrElse(t *testing.T) {
args args
want Option[int]
}{
{name: "optNone[T] OrElse() returns optNone if else condition is optNone", args: args{opt: optNone[int]{}}, want: optNone[int]{}},
{name: "optNone[T] OrElse() returns optSome if else condition is optSome", args: args{opt: optSome[int]{2}}, want: optSome[int]{2}},
{name: "None[T] OrElse() returns None if else condition is None", args: args{opt: None[int]()}, want: None[int]()},
{name: "None[T] OrElse() returns Some if else condition is Some", args: args{opt: Some[int](2)}, want: Some[int](2)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := optNone[int]{}
n := None[int]()
if got := n.OrElse(tt.args.opt); !reflect.DeepEqual(got, tt.want) {
t.Errorf("OrElse() = %v, want %v", got, tt.want)
}
Expand All @@ -103,11 +103,11 @@ func TestNone_String(t *testing.T) {
name string
want string
}{
{name: "optNone[T] String() returns None", want: "None"},
{name: "None[T] String() returns None", want: "None"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := optNone[int]{}
n := None[int]()
if got := n.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
Expand Down
32 changes: 24 additions & 8 deletions option.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package option

import "reflect"
import (
"github.com/google/go-cmp/cmp"
"reflect"
)

type Option[T any] interface {
type optionalValue[T any] interface {
Get() T
GetOrElse(v T) T
OrElse(opt Option[T]) Option[T]
Expand All @@ -11,8 +14,12 @@ type Option[T any] interface {
String() string
}

type Option[T any] struct {
optionalValue[T]
}

func None[T any]() Option[T] {
return optNone[T]{}
return Option[T]{optNone[T]{}}
}

func Some[T any](o T) Option[T] {
Expand All @@ -26,23 +33,32 @@ func NewOption[T any](o T) Option[T] {
v.Kind() == reflect.Map ||
v.Kind() == reflect.Chan ||
v.Kind() == reflect.Func) && v.IsNil() {
return optNone[T]{}
return Option[T]{optNone[T]{}}
}
return optSome[T]{o}
return Option[T]{optSome[T]{o}}
}

func Map[T1, T2 any](opt Option[T1], mapper func(T1) T2) Option[T2] {
if opt.NonEmpty() {
return optSome[T2]{mapper(opt.Get())}
return Option[T2]{optSome[T2]{mapper(opt.Get())}}
} else {
return optNone[T2]{}
return Option[T2]{optNone[T2]{}}
}
}

func FlatMap[T1, T2 any](opt Option[T1], mapper func(T1) Option[T2]) Option[T2] {
if opt.NonEmpty() {
return mapper(opt.Get())
} else {
return optNone[T2]{}
return Option[T2]{optNone[T2]{}}
}
}

func (x Option[T]) Equal(y Option[T]) bool {
if x.Empty() && y.Empty() {
return true
} else if !x.Empty() && !y.Empty() {
return cmp.Equal(x.Get(), y.Get())
}
return false
}
Loading

0 comments on commit a106af3

Please sign in to comment.