From dd5f8ebb7af8aa849902bafdc1ac2d85c63ff731 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Wed, 4 Sep 2024 17:08:08 -0700 Subject: [PATCH 1/2] [goutil] Java style Optional construct --- internal/goutil/optional.go | 89 ++++++++++++++++++ internal/goutil/optional_test.go | 156 +++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 internal/goutil/optional.go create mode 100644 internal/goutil/optional_test.go diff --git a/internal/goutil/optional.go b/internal/goutil/optional.go new file mode 100644 index 00000000000..5e005159f20 --- /dev/null +++ b/internal/goutil/optional.go @@ -0,0 +1,89 @@ +package goutil + +import ( + "encoding/json" + "fmt" +) + +// Optional represents a value that may or may not be present. +type Optional[T any] struct { + value T + isSet bool +} + +// NewOptional creates a new Optional with the given value. +func NewOptional[T any](value T) Optional[T] { + return Optional[T]{ + value: value, + isSet: true, + } +} + +// Empty returns an empty Optional. +func Empty[T any]() Optional[T] { + return Optional[T]{} +} + +// IsPresent returns true if the Optional contains a value. +func (o Optional[T]) IsPresent() bool { + return o.isSet +} + +// Get returns the value if present, or panics if not present. +func (o Optional[T]) Get() T { + if !o.isSet { + panic("Optional value is not present") + } + return o.value +} + +// OrElse returns the value if present, or the given default value if not present. +func (o Optional[T]) OrElse(defaultValue T) T { + if o.isSet { + return o.value + } + return defaultValue +} + +// IfPresent calls the given function with the value if present. +func (o Optional[T]) IfPresent(f func(T)) { + if o.isSet { + f(o.value) + } +} + +// Map applies the given function to the value if present and returns a new Optional. +func (o Optional[T]) Map(f func(T) T) Optional[T] { + if !o.isSet { + return Empty[T]() + } + return NewOptional(f(o.value)) +} + +// String returns a string representation of the Optional. +func (o Optional[T]) String() string { + if !o.isSet { + return "Optional.Empty" + } + return fmt.Sprintf("Optional[%v]", o.value) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (o *Optional[T]) UnmarshalJSON(data []byte) error { + // Check if it's null first + if string(data) == "null" { + *o = Empty[T]() + return nil + } + + // If not null, try to unmarshal into the value type T + var value T + err := json.Unmarshal(data, &value) + if err == nil { + *o = NewOptional(value) + return nil + } + + // If it's neither a valid T nor null, return an error + return fmt.Errorf("cannot unmarshal %s into Optional[T]", string(data)) +} diff --git a/internal/goutil/optional_test.go b/internal/goutil/optional_test.go new file mode 100644 index 00000000000..0a1390ccc7d --- /dev/null +++ b/internal/goutil/optional_test.go @@ -0,0 +1,156 @@ +package goutil + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptional(t *testing.T) { + t.Run("NewOptional", func(t *testing.T) { + opt := NewOptional(42) + assert.True(t, opt.IsPresent()) + assert.Equal(t, 42, opt.Get()) + }) + + t.Run("Empty", func(t *testing.T) { + opt := Empty[int]() + assert.False(t, opt.IsPresent()) + }) + + t.Run("Get", func(t *testing.T) { + opt := NewOptional("test") + assert.Equal(t, "test", opt.Get()) + + emptyOpt := Empty[string]() + assert.Panics(t, func() { emptyOpt.Get() }) + }) + + t.Run("OrElse", func(t *testing.T) { + opt := NewOptional(10) + assert.Equal(t, 10, opt.OrElse(20)) + + emptyOpt := Empty[int]() + assert.Equal(t, 20, emptyOpt.OrElse(20)) + }) + + t.Run("IfPresent", func(t *testing.T) { + opt := NewOptional(5) + called := false + opt.IfPresent(func(v int) { + called = true + assert.Equal(t, 5, v) + }) + assert.True(t, called) + + emptyOpt := Empty[int]() + emptyOpt.IfPresent(func(v int) { + t.Fail() // This should not be called + }) + }) + + t.Run("Map", func(t *testing.T) { + opt := NewOptional(3) + mapped := opt.Map(func(v int) int { return v * 2 }) + assert.True(t, mapped.IsPresent()) + assert.Equal(t, 6, mapped.Get()) + + emptyOpt := Empty[int]() + mappedEmpty := emptyOpt.Map(func(v int) int { return v * 2 }) + assert.False(t, mappedEmpty.IsPresent()) + }) + + t.Run("String", func(t *testing.T) { + opt := NewOptional("hello") + assert.Equal(t, "Optional[hello]", opt.String()) + + emptyOpt := Empty[string]() + assert.Equal(t, "Optional.Empty", emptyOpt.String()) + }) + + t.Run("UnmarshalJSON", func(t *testing.T) { + var opt Optional[int] + + err := json.Unmarshal([]byte("42"), &opt) + assert.NoError(t, err) + assert.True(t, opt.IsPresent()) + assert.Equal(t, 42, opt.Get()) + + err = json.Unmarshal([]byte("null"), &opt) + assert.NoError(t, err) + assert.False(t, opt.IsPresent()) + + err = json.Unmarshal([]byte(`"invalid"`), &opt) + assert.Error(t, err) + }) +} + +func TestOptionalUnmarshalJSONInStruct(t *testing.T) { + type TestStruct struct { + Name string `json:"name"` + Age Optional[int] `json:"age"` + Address Optional[string] `json:"address"` + } + + t.Run("Present values", func(t *testing.T) { + jsonData := `{ + "name": "John Doe", + "age": 30, + "address": "123 Main St" + }` + + var result TestStruct + err := json.Unmarshal([]byte(jsonData), &result) + + assert.NoError(t, err) + assert.Equal(t, "John Doe", result.Name) + assert.True(t, result.Age.IsPresent()) + assert.Equal(t, 30, result.Age.Get()) + assert.True(t, result.Address.IsPresent()) + assert.Equal(t, "123 Main St", result.Address.Get()) + }) + + t.Run("Missing optional values", func(t *testing.T) { + jsonData := `{ + "name": "Jane Doe" + }` + + var result TestStruct + err := json.Unmarshal([]byte(jsonData), &result) + + assert.NoError(t, err) + assert.Equal(t, "Jane Doe", result.Name) + assert.False(t, result.Age.IsPresent()) + assert.False(t, result.Address.IsPresent()) + }) + + t.Run("Null optional values", func(t *testing.T) { + jsonData := `{ + "name": "Bob Smith", + "age": null, + "address": "null" + }` + + var result TestStruct + err := json.Unmarshal([]byte(jsonData), &result) + + assert.NoError(t, err) + assert.Equal(t, "Bob Smith", result.Name) + assert.False(t, result.Age.IsPresent()) + assert.True(t, result.Address.IsPresent()) + }) + + t.Run("Invalid type for optional value", func(t *testing.T) { + jsonData := `{ + "name": "Alice Johnson", + "age": "thirty" + }` + + var result TestStruct + err := json.Unmarshal([]byte(jsonData), &result) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot unmarshal") + }) +} From 808f3a5aa14c31e2de9113acadc62368e8def811 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Wed, 4 Sep 2024 17:13:54 -0700 Subject: [PATCH 2/2] Return error --- internal/goutil/optional.go | 12 ++++++++---- internal/goutil/optional_test.go | 27 ++++++++++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/internal/goutil/optional.go b/internal/goutil/optional.go index 5e005159f20..60a26b4dd1d 100644 --- a/internal/goutil/optional.go +++ b/internal/goutil/optional.go @@ -2,9 +2,12 @@ package goutil import ( "encoding/json" + "errors" "fmt" ) +var ErrNotPresent = errors.New("Optional value is not present") + // Optional represents a value that may or may not be present. type Optional[T any] struct { value T @@ -29,12 +32,13 @@ func (o Optional[T]) IsPresent() bool { return o.isSet } -// Get returns the value if present, or panics if not present. -func (o Optional[T]) Get() T { +// Get returns the value if present, or an error if not present. +func (o Optional[T]) Get() (T, error) { if !o.isSet { - panic("Optional value is not present") + var zero T + return zero, ErrNotPresent } - return o.value + return o.value, nil } // OrElse returns the value if present, or the given default value if not present. diff --git a/internal/goutil/optional_test.go b/internal/goutil/optional_test.go index 0a1390ccc7d..299511373ee 100644 --- a/internal/goutil/optional_test.go +++ b/internal/goutil/optional_test.go @@ -11,7 +11,9 @@ func TestOptional(t *testing.T) { t.Run("NewOptional", func(t *testing.T) { opt := NewOptional(42) assert.True(t, opt.IsPresent()) - assert.Equal(t, 42, opt.Get()) + value, err := opt.Get() + assert.NoError(t, err) + assert.Equal(t, 42, value) }) t.Run("Empty", func(t *testing.T) { @@ -21,10 +23,13 @@ func TestOptional(t *testing.T) { t.Run("Get", func(t *testing.T) { opt := NewOptional("test") - assert.Equal(t, "test", opt.Get()) + value, err := opt.Get() + assert.NoError(t, err) + assert.Equal(t, "test", value) emptyOpt := Empty[string]() - assert.Panics(t, func() { emptyOpt.Get() }) + _, err = emptyOpt.Get() + assert.Error(t, err) }) t.Run("OrElse", func(t *testing.T) { @@ -54,7 +59,9 @@ func TestOptional(t *testing.T) { opt := NewOptional(3) mapped := opt.Map(func(v int) int { return v * 2 }) assert.True(t, mapped.IsPresent()) - assert.Equal(t, 6, mapped.Get()) + value, err := mapped.Get() + assert.NoError(t, err) + assert.Equal(t, 6, value) emptyOpt := Empty[int]() mappedEmpty := emptyOpt.Map(func(v int) int { return v * 2 }) @@ -75,7 +82,9 @@ func TestOptional(t *testing.T) { err := json.Unmarshal([]byte("42"), &opt) assert.NoError(t, err) assert.True(t, opt.IsPresent()) - assert.Equal(t, 42, opt.Get()) + value, err := opt.Get() + assert.NoError(t, err) + assert.Equal(t, 42, value) err = json.Unmarshal([]byte("null"), &opt) assert.NoError(t, err) @@ -106,9 +115,13 @@ func TestOptionalUnmarshalJSONInStruct(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "John Doe", result.Name) assert.True(t, result.Age.IsPresent()) - assert.Equal(t, 30, result.Age.Get()) + ageValue, err := result.Age.Get() + assert.NoError(t, err) + assert.Equal(t, 30, ageValue) assert.True(t, result.Address.IsPresent()) - assert.Equal(t, "123 Main St", result.Address.Get()) + addressValue, err := result.Address.Get() + assert.NoError(t, err) + assert.Equal(t, "123 Main St", addressValue) }) t.Run("Missing optional values", func(t *testing.T) {