Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions internal/goutil/optional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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
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 an error if not present.
func (o Optional[T]) Get() (T, error) {
if !o.isSet {
var zero T
return zero, ErrNotPresent
}
return o.value, nil
}

// 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))
}
169 changes: 169 additions & 0 deletions internal/goutil/optional_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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())
value, err := opt.Get()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})

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")
value, err := opt.Get()
assert.NoError(t, err)
assert.Equal(t, "test", value)

emptyOpt := Empty[string]()
_, err = emptyOpt.Get()
assert.Error(t, err)
})

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())
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 })
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())
value, err := opt.Get()
assert.NoError(t, err)
assert.Equal(t, 42, value)

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())
ageValue, err := result.Age.Get()
assert.NoError(t, err)
assert.Equal(t, 30, ageValue)
assert.True(t, result.Address.IsPresent())
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) {
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")
})
}