Skip to content

Commit 8acc0b9

Browse files
author
Dean Karn
authored
Expand Option.Value cases (#35)
1 parent ab6d6f3 commit 8acc0b9

File tree

4 files changed

+220
-18
lines changed

4 files changed

+220
-18
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [5.20.0] - 2023-06-17
10+
### Added
11+
- Expanded Option type SQL Value support to handle value custom types and honour the `driver.Valuer` interface.
12+
13+
### Changed
14+
- Option sql.Scanner to support custom types.
15+
916
## [5.19.0] - 2023-06-14
1017
### Added
1118
- strconvext.ParseBool(...) which is a drop-in replacement for the std lin strconv.ParseBool(..) with a few more supported values.
@@ -55,7 +62,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5562
### Added
5663
- Added `timext.NanoTime` for fast low level monotonic time with nanosecond precision.
5764

58-
[Unreleased]: https://github.com/go-playground/pkg/compare/v5.19.0...HEAD
65+
[Unreleased]: https://github.com/go-playground/pkg/compare/v5.20.0...HEAD
66+
[5.20.0]: https://github.com/go-playground/pkg/compare/v5.19.0..v5.20.0
5967
[5.19.0]: https://github.com/go-playground/pkg/compare/v5.18.0..v5.19.0
6068
[5.18.0]: https://github.com/go-playground/pkg/compare/v5.17.2..v5.18.0
6169
[5.17.2]: https://github.com/go-playground/pkg/compare/v5.17.1..v5.17.2

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# pkg
22

3-
![Project status](https://img.shields.io/badge/version-5.19.0-green.svg)
3+
![Project status](https://img.shields.io/badge/version-5.20.0-green.svg)
44
[![Lint & Test](https://github.com/go-playground/pkg/actions/workflows/go.yml/badge.svg)](https://github.com/go-playground/pkg/actions/workflows/go.yml)
55
[![Coverage Status](https://coveralls.io/repos/github/go-playground/pkg/badge.svg?branch=master)](https://coveralls.io/github/go-playground/pkg?branch=master)
66
[![GoDoc](https://godoc.org/github.com/go-playground/pkg?status.svg)](https://pkg.go.dev/mod/github.com/go-playground/pkg/v5)

values/option/option.go

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import (
1515
var (
1616
scanType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
1717
byteSliceType = reflect.TypeOf(([]byte)(nil))
18+
valuerType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
19+
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
20+
stringType = reflect.TypeOf((*string)(nil)).Elem()
21+
int64Type = reflect.TypeOf((*int64)(nil)).Elem()
22+
float64Type = reflect.TypeOf((*float64)(nil)).Elem()
23+
boolType = reflect.TypeOf((*bool)(nil)).Elem()
1824
)
1925

2026
// Option represents a values that represents a values existence.
@@ -97,11 +103,43 @@ func (o *Option[T]) UnmarshalJSON(data []byte) error {
97103
}
98104

99105
// Value implements the driver.Valuer interface.
106+
//
107+
// This honours the `driver.Valuer` interface if the value implements it.
108+
// It also supports custom types of the std types and treats all else as []byte/
100109
func (o Option[T]) Value() (driver.Value, error) {
101-
if o.isSome {
102-
return o.Unwrap(), nil
110+
if o.IsNone() {
111+
return nil, nil
112+
}
113+
value := o.Unwrap()
114+
val := reflect.ValueOf(value)
115+
116+
if val.Type().Implements(valuerType) {
117+
return val.Interface().(driver.Valuer).Value()
118+
}
119+
switch val.Kind() {
120+
case reflect.String:
121+
return val.Convert(stringType).Interface(), nil
122+
case reflect.Bool:
123+
return val.Convert(boolType).Interface(), nil
124+
case reflect.Int64:
125+
return val.Convert(int64Type).Interface(), nil
126+
case reflect.Float64:
127+
return val.Convert(float64Type).Interface(), nil
128+
case reflect.Slice, reflect.Array:
129+
if val.Type().ConvertibleTo(byteSliceType) {
130+
return val.Convert(byteSliceType).Interface(), nil
131+
}
132+
return json.Marshal(val.Interface())
133+
case reflect.Struct:
134+
if val.CanConvert(timeType) {
135+
return val.Convert(timeType).Interface(), nil
136+
}
137+
return json.Marshal(val.Interface())
138+
case reflect.Map:
139+
return json.Marshal(val.Interface())
140+
default:
141+
return val.Interface(), nil
103142
}
104-
return nil, nil
105143
}
106144

107145
// Scan implements the sql.Scanner interface.
@@ -130,68 +168,68 @@ func (o *Option[T]) Scan(value any) error {
130168
if err := v.Scan(value); err != nil {
131169
return err
132170
}
133-
*o = Some(reflect.ValueOf(v.String).Interface().(T))
171+
*o = Some(reflect.ValueOf(v.String).Convert(val.Type()).Interface().(T))
134172
case reflect.Bool:
135173
var v sql.NullBool
136174
if err := v.Scan(value); err != nil {
137175
return err
138176
}
139-
*o = Some(reflect.ValueOf(v.Bool).Interface().(T))
177+
*o = Some(reflect.ValueOf(v.Bool).Convert(val.Type()).Interface().(T))
140178
case reflect.Uint8:
141179
var v sql.NullByte
142180
if err := v.Scan(value); err != nil {
143181
return err
144182
}
145-
*o = Some(reflect.ValueOf(v.Byte).Interface().(T))
183+
*o = Some(reflect.ValueOf(v.Byte).Convert(val.Type()).Interface().(T))
146184
case reflect.Float64:
147185
var v sql.NullFloat64
148186
if err := v.Scan(value); err != nil {
149187
return err
150188
}
151-
*o = Some(reflect.ValueOf(v.Float64).Interface().(T))
189+
*o = Some(reflect.ValueOf(v.Float64).Convert(val.Type()).Interface().(T))
152190
case reflect.Int16:
153191
var v sql.NullInt16
154192
if err := v.Scan(value); err != nil {
155193
return err
156194
}
157-
*o = Some(reflect.ValueOf(v.Int16).Interface().(T))
195+
*o = Some(reflect.ValueOf(v.Int16).Convert(val.Type()).Interface().(T))
158196
case reflect.Int32:
159197
var v sql.NullInt32
160198
if err := v.Scan(value); err != nil {
161199
return err
162200
}
163-
*o = Some(reflect.ValueOf(v.Int32).Interface().(T))
201+
*o = Some(reflect.ValueOf(v.Int32).Convert(val.Type()).Interface().(T))
164202
case reflect.Int64:
165203
var v sql.NullInt64
166204
if err := v.Scan(value); err != nil {
167205
return err
168206
}
169-
*o = Some(reflect.ValueOf(v.Int64).Interface().(T))
207+
*o = Some(reflect.ValueOf(v.Int64).Convert(val.Type()).Interface().(T))
170208
case reflect.Interface:
171-
*o = Some(reflect.ValueOf(value).Interface().(T))
209+
*o = Some(reflect.ValueOf(value).Convert(val.Type()).Interface().(T))
172210
case reflect.Struct:
173-
if val.Type() == reflect.TypeOf(time.Time{}) {
211+
if val.CanConvert(timeType) {
174212
switch t := value.(type) {
175213
case string:
176214
tm, err := time.Parse(time.RFC3339Nano, t)
177215
if err != nil {
178216
return err
179217
}
180-
*o = Some(reflect.ValueOf(tm).Interface().(T))
218+
*o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T))
181219

182220
case []byte:
183221
tm, err := time.Parse(time.RFC3339Nano, string(t))
184222
if err != nil {
185223
return err
186224
}
187-
*o = Some(reflect.ValueOf(tm).Interface().(T))
225+
*o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T))
188226

189227
default:
190228
var v sql.NullTime
191229
if err := v.Scan(value); err != nil {
192230
return err
193231
}
194-
*o = Some(reflect.ValueOf(v.Time).Interface().(T))
232+
*o = Some(reflect.ValueOf(v.Time).Convert(val.Type()).Interface().(T))
195233
}
196234
return nil
197235
}

values/option/option_test.go

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,163 @@
44
package optionext
55

66
import (
7+
"database/sql/driver"
78
"encoding/json"
9+
"reflect"
810
"testing"
911
"time"
1012

1113
. "github.com/go-playground/assert/v2"
1214
)
1315

16+
type valueTest struct {
17+
}
18+
19+
func (valueTest) Value() (driver.Value, error) {
20+
return "value", nil
21+
}
22+
23+
type customStringType string
24+
25+
type testStructType struct {
26+
Name string
27+
}
28+
29+
func TestSQLDriverValue(t *testing.T) {
30+
31+
var v valueTest
32+
Equal(t, reflect.TypeOf(v).Implements(valuerType), true)
33+
34+
// none
35+
nOpt := None[string]()
36+
nVal, err := nOpt.Value()
37+
Equal(t, err, nil)
38+
Equal(t, nVal, nil)
39+
40+
// string + convert custom string type
41+
sOpt := Some("myString")
42+
sVal, err := sOpt.Value()
43+
Equal(t, err, nil)
44+
45+
_, ok := sVal.(string)
46+
Equal(t, ok, true)
47+
Equal(t, sVal, "myString")
48+
49+
sCustOpt := Some(customStringType("string"))
50+
sCustVal, err := sCustOpt.Value()
51+
Equal(t, err, nil)
52+
Equal(t, sCustVal, "string")
53+
54+
_, ok = sCustVal.(string)
55+
Equal(t, ok, true)
56+
57+
// bool
58+
bOpt := Some(true)
59+
bVal, err := bOpt.Value()
60+
Equal(t, err, nil)
61+
62+
_, ok = bVal.(bool)
63+
Equal(t, ok, true)
64+
Equal(t, bVal, true)
65+
66+
// int64
67+
iOpt := Some(int64(2))
68+
iVal, err := iOpt.Value()
69+
Equal(t, err, nil)
70+
71+
_, ok = iVal.(int64)
72+
Equal(t, ok, true)
73+
Equal(t, iVal, int64(2))
74+
75+
// float64
76+
fOpt := Some(1.1)
77+
fVal, err := fOpt.Value()
78+
Equal(t, err, nil)
79+
80+
_, ok = fVal.(float64)
81+
Equal(t, ok, true)
82+
Equal(t, fVal, 1.1)
83+
84+
// time.Time
85+
dt := time.Now().UTC()
86+
dtOpt := Some(dt)
87+
dtVal, err := dtOpt.Value()
88+
Equal(t, err, nil)
89+
90+
_, ok = dtVal.(time.Time)
91+
Equal(t, ok, true)
92+
Equal(t, dtVal, dt)
93+
94+
// Slice []byte
95+
b := []byte("myBytes")
96+
bytesOpt := Some(b)
97+
bytesVal, err := bytesOpt.Value()
98+
Equal(t, err, nil)
99+
100+
_, ok = bytesVal.([]byte)
101+
Equal(t, ok, true)
102+
Equal(t, bytesVal, b)
103+
104+
// Slice []uint8
105+
b2 := []uint8("myBytes")
106+
bytes2Opt := Some(b2)
107+
bytes2Val, err := bytes2Opt.Value()
108+
Equal(t, err, nil)
109+
110+
_, ok = bytes2Val.([]byte)
111+
Equal(t, ok, true)
112+
Equal(t, bytes2Val, b2)
113+
114+
// Array []byte
115+
a := []byte{'1', '2', '3'}
116+
arrayOpt := Some(a)
117+
arrayVal, err := arrayOpt.Value()
118+
Equal(t, err, nil)
119+
120+
_, ok = arrayVal.([]byte)
121+
Equal(t, ok, true)
122+
Equal(t, arrayVal, a)
123+
124+
// Slice []byte
125+
data := []testStructType{{Name: "test"}}
126+
b, err = json.Marshal(data)
127+
Equal(t, err, nil)
128+
129+
dataOpt := Some(data)
130+
dataVal, err := dataOpt.Value()
131+
Equal(t, err, nil)
132+
133+
_, ok = dataVal.([]byte)
134+
Equal(t, ok, true)
135+
Equal(t, dataVal, b)
136+
137+
// Map
138+
data2 := map[string]int{"test": 1}
139+
b, err = json.Marshal(data2)
140+
Equal(t, err, nil)
141+
142+
data2Opt := Some(data2)
143+
data2Val, err := data2Opt.Value()
144+
Equal(t, err, nil)
145+
146+
_, ok = data2Val.([]byte)
147+
Equal(t, ok, true)
148+
Equal(t, data2Val, b)
149+
150+
// Struct
151+
data3 := testStructType{Name: "test"}
152+
b, err = json.Marshal(data3)
153+
Equal(t, err, nil)
154+
155+
data3Opt := Some(data3)
156+
data3Val, err := data3Opt.Value()
157+
Equal(t, err, nil)
158+
159+
_, ok = data3Val.([]byte)
160+
Equal(t, ok, true)
161+
Equal(t, data3Val, b)
162+
}
163+
14164
type customScanner struct {
15165
S string
16166
}
@@ -20,7 +170,7 @@ func (c *customScanner) Scan(src interface{}) error {
20170
return nil
21171
}
22172

23-
func TestSQL(t *testing.T) {
173+
func TestSQLScanner(t *testing.T) {
24174
value := int64(123)
25175
var optionI64 Option[int64]
26176
var optionI32 Option[int32]
@@ -115,6 +265,12 @@ func TestSQL(t *testing.T) {
115265
err = optionMap.Scan([]byte(`{"name":"test"}`))
116266
Equal(t, err, nil)
117267
Equal(t, optionMap, Some(map[string]any{"name": "test"}))
268+
269+
// test custom types
270+
var ct Option[customStringType]
271+
err = ct.Scan("test")
272+
Equal(t, err, nil)
273+
Equal(t, ct, Some(customStringType("test")))
118274
}
119275

120276
func TestNilOption(t *testing.T) {

0 commit comments

Comments
 (0)