Skip to content

Commit dd5f8eb

Browse files
committed
[goutil] Java style Optional construct
1 parent 4677feb commit dd5f8eb

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

internal/goutil/optional.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package goutil
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
// Optional represents a value that may or may not be present.
9+
type Optional[T any] struct {
10+
value T
11+
isSet bool
12+
}
13+
14+
// NewOptional creates a new Optional with the given value.
15+
func NewOptional[T any](value T) Optional[T] {
16+
return Optional[T]{
17+
value: value,
18+
isSet: true,
19+
}
20+
}
21+
22+
// Empty returns an empty Optional.
23+
func Empty[T any]() Optional[T] {
24+
return Optional[T]{}
25+
}
26+
27+
// IsPresent returns true if the Optional contains a value.
28+
func (o Optional[T]) IsPresent() bool {
29+
return o.isSet
30+
}
31+
32+
// Get returns the value if present, or panics if not present.
33+
func (o Optional[T]) Get() T {
34+
if !o.isSet {
35+
panic("Optional value is not present")
36+
}
37+
return o.value
38+
}
39+
40+
// OrElse returns the value if present, or the given default value if not present.
41+
func (o Optional[T]) OrElse(defaultValue T) T {
42+
if o.isSet {
43+
return o.value
44+
}
45+
return defaultValue
46+
}
47+
48+
// IfPresent calls the given function with the value if present.
49+
func (o Optional[T]) IfPresent(f func(T)) {
50+
if o.isSet {
51+
f(o.value)
52+
}
53+
}
54+
55+
// Map applies the given function to the value if present and returns a new Optional.
56+
func (o Optional[T]) Map(f func(T) T) Optional[T] {
57+
if !o.isSet {
58+
return Empty[T]()
59+
}
60+
return NewOptional(f(o.value))
61+
}
62+
63+
// String returns a string representation of the Optional.
64+
func (o Optional[T]) String() string {
65+
if !o.isSet {
66+
return "Optional.Empty"
67+
}
68+
return fmt.Sprintf("Optional[%v]", o.value)
69+
}
70+
71+
// UnmarshalJSON implements the json.Unmarshaler interface.
72+
func (o *Optional[T]) UnmarshalJSON(data []byte) error {
73+
// Check if it's null first
74+
if string(data) == "null" {
75+
*o = Empty[T]()
76+
return nil
77+
}
78+
79+
// If not null, try to unmarshal into the value type T
80+
var value T
81+
err := json.Unmarshal(data, &value)
82+
if err == nil {
83+
*o = NewOptional(value)
84+
return nil
85+
}
86+
87+
// If it's neither a valid T nor null, return an error
88+
return fmt.Errorf("cannot unmarshal %s into Optional[T]", string(data))
89+
}

internal/goutil/optional_test.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package goutil
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestOptional(t *testing.T) {
11+
t.Run("NewOptional", func(t *testing.T) {
12+
opt := NewOptional(42)
13+
assert.True(t, opt.IsPresent())
14+
assert.Equal(t, 42, opt.Get())
15+
})
16+
17+
t.Run("Empty", func(t *testing.T) {
18+
opt := Empty[int]()
19+
assert.False(t, opt.IsPresent())
20+
})
21+
22+
t.Run("Get", func(t *testing.T) {
23+
opt := NewOptional("test")
24+
assert.Equal(t, "test", opt.Get())
25+
26+
emptyOpt := Empty[string]()
27+
assert.Panics(t, func() { emptyOpt.Get() })
28+
})
29+
30+
t.Run("OrElse", func(t *testing.T) {
31+
opt := NewOptional(10)
32+
assert.Equal(t, 10, opt.OrElse(20))
33+
34+
emptyOpt := Empty[int]()
35+
assert.Equal(t, 20, emptyOpt.OrElse(20))
36+
})
37+
38+
t.Run("IfPresent", func(t *testing.T) {
39+
opt := NewOptional(5)
40+
called := false
41+
opt.IfPresent(func(v int) {
42+
called = true
43+
assert.Equal(t, 5, v)
44+
})
45+
assert.True(t, called)
46+
47+
emptyOpt := Empty[int]()
48+
emptyOpt.IfPresent(func(v int) {
49+
t.Fail() // This should not be called
50+
})
51+
})
52+
53+
t.Run("Map", func(t *testing.T) {
54+
opt := NewOptional(3)
55+
mapped := opt.Map(func(v int) int { return v * 2 })
56+
assert.True(t, mapped.IsPresent())
57+
assert.Equal(t, 6, mapped.Get())
58+
59+
emptyOpt := Empty[int]()
60+
mappedEmpty := emptyOpt.Map(func(v int) int { return v * 2 })
61+
assert.False(t, mappedEmpty.IsPresent())
62+
})
63+
64+
t.Run("String", func(t *testing.T) {
65+
opt := NewOptional("hello")
66+
assert.Equal(t, "Optional[hello]", opt.String())
67+
68+
emptyOpt := Empty[string]()
69+
assert.Equal(t, "Optional.Empty", emptyOpt.String())
70+
})
71+
72+
t.Run("UnmarshalJSON", func(t *testing.T) {
73+
var opt Optional[int]
74+
75+
err := json.Unmarshal([]byte("42"), &opt)
76+
assert.NoError(t, err)
77+
assert.True(t, opt.IsPresent())
78+
assert.Equal(t, 42, opt.Get())
79+
80+
err = json.Unmarshal([]byte("null"), &opt)
81+
assert.NoError(t, err)
82+
assert.False(t, opt.IsPresent())
83+
84+
err = json.Unmarshal([]byte(`"invalid"`), &opt)
85+
assert.Error(t, err)
86+
})
87+
}
88+
89+
func TestOptionalUnmarshalJSONInStruct(t *testing.T) {
90+
type TestStruct struct {
91+
Name string `json:"name"`
92+
Age Optional[int] `json:"age"`
93+
Address Optional[string] `json:"address"`
94+
}
95+
96+
t.Run("Present values", func(t *testing.T) {
97+
jsonData := `{
98+
"name": "John Doe",
99+
"age": 30,
100+
"address": "123 Main St"
101+
}`
102+
103+
var result TestStruct
104+
err := json.Unmarshal([]byte(jsonData), &result)
105+
106+
assert.NoError(t, err)
107+
assert.Equal(t, "John Doe", result.Name)
108+
assert.True(t, result.Age.IsPresent())
109+
assert.Equal(t, 30, result.Age.Get())
110+
assert.True(t, result.Address.IsPresent())
111+
assert.Equal(t, "123 Main St", result.Address.Get())
112+
})
113+
114+
t.Run("Missing optional values", func(t *testing.T) {
115+
jsonData := `{
116+
"name": "Jane Doe"
117+
}`
118+
119+
var result TestStruct
120+
err := json.Unmarshal([]byte(jsonData), &result)
121+
122+
assert.NoError(t, err)
123+
assert.Equal(t, "Jane Doe", result.Name)
124+
assert.False(t, result.Age.IsPresent())
125+
assert.False(t, result.Address.IsPresent())
126+
})
127+
128+
t.Run("Null optional values", func(t *testing.T) {
129+
jsonData := `{
130+
"name": "Bob Smith",
131+
"age": null,
132+
"address": "null"
133+
}`
134+
135+
var result TestStruct
136+
err := json.Unmarshal([]byte(jsonData), &result)
137+
138+
assert.NoError(t, err)
139+
assert.Equal(t, "Bob Smith", result.Name)
140+
assert.False(t, result.Age.IsPresent())
141+
assert.True(t, result.Address.IsPresent())
142+
})
143+
144+
t.Run("Invalid type for optional value", func(t *testing.T) {
145+
jsonData := `{
146+
"name": "Alice Johnson",
147+
"age": "thirty"
148+
}`
149+
150+
var result TestStruct
151+
err := json.Unmarshal([]byte(jsonData), &result)
152+
153+
assert.Error(t, err)
154+
assert.Contains(t, err.Error(), "cannot unmarshal")
155+
})
156+
}

0 commit comments

Comments
 (0)