Skip to content

Commit ab6d6f3

Browse files
author
Dean Karn
authored
SQL plus improvements (#34)
## PR - Added `strconv.ParseBool` which is like the std libraries `ParseBool` but with some additional cases added and should be a drop in replacement. - Updated/extended `Options` SQL decoding abilitys
1 parent f910bd5 commit ab6d6f3

File tree

5 files changed

+220
-47
lines changed

5 files changed

+220
-47
lines changed

CHANGELOG.md

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

77
## [Unreleased]
88

9+
## [5.19.0] - 2023-06-14
10+
### Added
11+
- strconvext.ParseBool(...) which is a drop-in replacement for the std lin strconv.ParseBool(..) with a few more supported values.
12+
- Expanded Option type SQL Scan support to handle Scanning to an Interface, Struct, Slice, Map and anything that implements the sql.Scanner interface.
13+
914
## [5.18.0] - 2023-05-21
1015
### Added
1116
- typesext.Nothing & valuesext.Nothing for better clarity in generic params and values that represent struct{}. This will provide better code readability and intent.
@@ -50,7 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5055
### Added
5156
- Added `timext.NanoTime` for fast low level monotonic time with nanosecond precision.
5257

53-
[Unreleased]: https://github.com/go-playground/pkg/compare/v5.18.0...HEAD
58+
[Unreleased]: https://github.com/go-playground/pkg/compare/v5.19.0...HEAD
59+
[5.19.0]: https://github.com/go-playground/pkg/compare/v5.18.0..v5.19.0
5460
[5.18.0]: https://github.com/go-playground/pkg/compare/v5.17.2..v5.18.0
5561
[5.17.2]: https://github.com/go-playground/pkg/compare/v5.17.1..v5.17.2
5662
[5.17.1]: https://github.com/go-playground/pkg/compare/v5.17.0...v5.17.1

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.18.0-green.svg)
3+
![Project status](https://img.shields.io/badge/version-5.19.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)

strconv/bool.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package strconvext
2+
3+
import (
4+
"strconv"
5+
)
6+
7+
// ParseBool returns the boolean value represented by the string. It extends the std library parse bool with a few more
8+
// valid options.
9+
//
10+
// It accepts 1, t, T, true, TRUE, True, on, yes, ok as true values and 0, f, F, false, FALSE, False, off, no as false.
11+
func ParseBool(str string) (bool, error) {
12+
switch str {
13+
case "1", "t", "T", "true", "TRUE", "True", "on", "yes", "ok":
14+
return true, nil
15+
case "", "0", "f", "F", "false", "FALSE", "False", "off", "no":
16+
return false, nil
17+
}
18+
// strconv.NumError mimicking exactly the strconv.ParseBool(..) error and type
19+
// to ensure compatibility with std library.
20+
return false, &strconv.NumError{Func: "ParseBool", Num: string([]byte(str)), Err: strconv.ErrSyntax}
21+
}

values/option/option.go

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,34 @@ import (
1212
"time"
1313
)
1414

15+
var (
16+
scanType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
17+
byteSliceType = reflect.TypeOf(([]byte)(nil))
18+
)
19+
1520
// Option represents a values that represents a values existence.
1621
//
1722
// nil is usually used on Go however this has two problems:
1823
// 1. Checking if the return values is nil is NOT enforced and can lead to panics.
1924
// 2. Using nil is not good enough when nil itself is a valid value.
25+
//
26+
// This implements the sql.Scanner interface and can be used as a sql value for reading and writing. It supports:
27+
// - String
28+
// - Bool
29+
// - Uint8
30+
// - Float64
31+
// - Int16
32+
// - Int32
33+
// - Int64
34+
// - interface{}/any
35+
// - time.Time
36+
// - Struct - when type is convertable to []byte and assumes JSON.
37+
// - Slice - when type is convertable to []byte and assumes JSON.
38+
// - Map types - when type is convertable to []byte and assumes JSON.
39+
//
40+
// This also implements the `json.Marshaler` and `json.Unmarshaler` interfaces. The only caveat is a None value will result
41+
// in a JSON `null` value. there is no way to hook into the std library to make `omitempty` not produce any value at
42+
// this time.
2043
type Option[T any] struct {
2144
value T
2245
isSome bool
@@ -50,15 +73,15 @@ func None[T any]() Option[T] {
5073
return Option[T]{}
5174
}
5275

53-
// MarshalJSON implements the json.Marshaler interface.
76+
// MarshalJSON implements the `json.Marshaler` interface.
5477
func (o Option[T]) MarshalJSON() ([]byte, error) {
5578
if o.isSome {
5679
return json.Marshal(o.value)
5780
}
5881
return []byte("null"), nil
5982
}
6083

61-
// UnmarshalJSON implements the json.Unmarshaler interface.
84+
// UnmarshalJSON implements the `json.Unmarshaler` interface.
6285
func (o *Option[T]) UnmarshalJSON(data []byte) error {
6386
if len(data) == 4 && string(data[:4]) == "null" {
6487
*o = None[T]()
@@ -83,93 +106,110 @@ func (o Option[T]) Value() (driver.Value, error) {
83106

84107
// Scan implements the sql.Scanner interface.
85108
func (o *Option[T]) Scan(value any) error {
86-
val := reflect.ValueOf(o.value)
109+
110+
val := reflect.ValueOf(&o.value)
111+
112+
if val.Type().Implements(scanType) {
113+
err := val.Interface().(sql.Scanner).Scan(value)
114+
if err != nil {
115+
return err
116+
}
117+
o.isSome = true
118+
return nil
119+
}
120+
121+
if value == nil {
122+
*o = None[T]()
123+
return nil
124+
}
125+
val = val.Elem()
126+
87127
switch val.Kind() {
88128
case reflect.String:
89129
var v sql.NullString
90130
if err := v.Scan(value); err != nil {
91131
return err
92132
}
93-
if !v.Valid {
94-
*o = None[T]()
95-
} else {
96-
*o = Some(reflect.ValueOf(v.String).Interface().(T))
97-
}
133+
*o = Some(reflect.ValueOf(v.String).Interface().(T))
98134
case reflect.Bool:
99135
var v sql.NullBool
100136
if err := v.Scan(value); err != nil {
101137
return err
102138
}
103-
if !v.Valid {
104-
*o = None[T]()
105-
} else {
106-
*o = Some(reflect.ValueOf(v.Bool).Interface().(T))
107-
}
139+
*o = Some(reflect.ValueOf(v.Bool).Interface().(T))
108140
case reflect.Uint8:
109141
var v sql.NullByte
110142
if err := v.Scan(value); err != nil {
111143
return err
112144
}
113-
if !v.Valid {
114-
*o = None[T]()
115-
} else {
116-
*o = Some(reflect.ValueOf(v.Byte).Interface().(T))
117-
}
145+
*o = Some(reflect.ValueOf(v.Byte).Interface().(T))
118146
case reflect.Float64:
119147
var v sql.NullFloat64
120148
if err := v.Scan(value); err != nil {
121149
return err
122150
}
123-
if !v.Valid {
124-
*o = None[T]()
125-
} else {
126-
*o = Some(reflect.ValueOf(v.Float64).Interface().(T))
127-
}
151+
*o = Some(reflect.ValueOf(v.Float64).Interface().(T))
128152
case reflect.Int16:
129153
var v sql.NullInt16
130154
if err := v.Scan(value); err != nil {
131155
return err
132156
}
133-
if !v.Valid {
134-
*o = None[T]()
135-
} else {
136-
*o = Some(reflect.ValueOf(v.Int16).Interface().(T))
137-
}
157+
*o = Some(reflect.ValueOf(v.Int16).Interface().(T))
138158
case reflect.Int32:
139159
var v sql.NullInt32
140160
if err := v.Scan(value); err != nil {
141161
return err
142162
}
143-
if !v.Valid {
144-
*o = None[T]()
145-
} else {
146-
*o = Some(reflect.ValueOf(v.Int32).Interface().(T))
147-
}
163+
*o = Some(reflect.ValueOf(v.Int32).Interface().(T))
148164
case reflect.Int64:
149165
var v sql.NullInt64
150166
if err := v.Scan(value); err != nil {
151167
return err
152168
}
153-
if !v.Valid {
154-
*o = None[T]()
155-
} else {
156-
*o = Some(reflect.ValueOf(v.Int64).Interface().(T))
157-
}
169+
*o = Some(reflect.ValueOf(v.Int64).Interface().(T))
170+
case reflect.Interface:
171+
*o = Some(reflect.ValueOf(value).Interface().(T))
158172
case reflect.Struct:
159173
if val.Type() == reflect.TypeOf(time.Time{}) {
160-
var v sql.NullTime
161-
if err := v.Scan(value); err != nil {
162-
return err
163-
}
164-
if !v.Valid {
165-
*o = None[T]()
166-
} else {
174+
switch t := value.(type) {
175+
case string:
176+
tm, err := time.Parse(time.RFC3339Nano, t)
177+
if err != nil {
178+
return err
179+
}
180+
*o = Some(reflect.ValueOf(tm).Interface().(T))
181+
182+
case []byte:
183+
tm, err := time.Parse(time.RFC3339Nano, string(t))
184+
if err != nil {
185+
return err
186+
}
187+
*o = Some(reflect.ValueOf(tm).Interface().(T))
188+
189+
default:
190+
var v sql.NullTime
191+
if err := v.Scan(value); err != nil {
192+
return err
193+
}
167194
*o = Some(reflect.ValueOf(v.Time).Interface().(T))
168195
}
169196
return nil
170197
}
171198
fallthrough
199+
172200
default:
201+
switch val.Kind() {
202+
case reflect.Struct, reflect.Slice, reflect.Map:
203+
v := reflect.ValueOf(value)
204+
205+
if v.Type().ConvertibleTo(byteSliceType) {
206+
if err := json.Unmarshal(v.Convert(byteSliceType).Interface().([]byte), &o.value); err != nil {
207+
return err
208+
}
209+
o.isSome = true
210+
return nil
211+
}
212+
}
173213
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", value, o.value)
174214
}
175215
return nil

values/option/option_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,112 @@ import (
1111
. "github.com/go-playground/assert/v2"
1212
)
1313

14+
type customScanner struct {
15+
S string
16+
}
17+
18+
func (c *customScanner) Scan(src interface{}) error {
19+
c.S = src.(string)
20+
return nil
21+
}
22+
23+
func TestSQL(t *testing.T) {
24+
value := int64(123)
25+
var optionI64 Option[int64]
26+
var optionI32 Option[int32]
27+
var optionI16 Option[int16]
28+
var optionString Option[string]
29+
var optionBool Option[bool]
30+
var optionF64 Option[float64]
31+
var optionByte Option[byte]
32+
var optionTime Option[time.Time]
33+
var optionInterface Option[any]
34+
35+
err := optionInterface.Scan(1)
36+
Equal(t, err, nil)
37+
Equal(t, optionInterface, Some(any(1)))
38+
39+
err = optionInterface.Scan("blah")
40+
Equal(t, err, nil)
41+
Equal(t, optionInterface, Some(any("blah")))
42+
43+
err = optionI64.Scan(value)
44+
Equal(t, err, nil)
45+
Equal(t, optionI64, Some(value))
46+
47+
err = optionI32.Scan(value)
48+
Equal(t, err, nil)
49+
Equal(t, optionI32, Some(int32(value)))
50+
51+
err = optionI16.Scan(value)
52+
Equal(t, err, nil)
53+
Equal(t, optionI16, Some(int16(value)))
54+
55+
err = optionBool.Scan(1)
56+
Equal(t, err, nil)
57+
Equal(t, optionBool, Some(true))
58+
59+
err = optionString.Scan(value)
60+
Equal(t, err, nil)
61+
Equal(t, optionString, Some("123"))
62+
63+
err = optionF64.Scan(2.0)
64+
Equal(t, err, nil)
65+
Equal(t, optionF64, Some(2.0))
66+
67+
err = optionByte.Scan(uint8('1'))
68+
Equal(t, err, nil)
69+
Equal(t, optionByte, Some(uint8('1')))
70+
71+
err = optionTime.Scan("2023-06-13T06:34:32Z")
72+
Equal(t, err, nil)
73+
Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)))
74+
75+
err = optionTime.Scan([]byte("2023-06-13T06:34:32Z"))
76+
Equal(t, err, nil)
77+
Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)))
78+
79+
err = optionTime.Scan(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC))
80+
Equal(t, err, nil)
81+
Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)))
82+
83+
// Test nil
84+
var nullableOption Option[int64]
85+
err = nullableOption.Scan(nil)
86+
Equal(t, err, nil)
87+
Equal(t, nullableOption, None[int64]())
88+
89+
// custom scanner
90+
var custom Option[customScanner]
91+
err = custom.Scan("GOT HERE")
92+
Equal(t, err, nil)
93+
Equal(t, custom, Some(customScanner{S: "GOT HERE"}))
94+
95+
// test unmarshal to struct
96+
type myStruct struct {
97+
Name string `json:"name"`
98+
}
99+
100+
var optionMyStruct Option[myStruct]
101+
err = optionMyStruct.Scan([]byte(`{"name":"test"}`))
102+
Equal(t, err, nil)
103+
Equal(t, optionMyStruct, Some(myStruct{Name: "test"}))
104+
105+
err = optionMyStruct.Scan(json.RawMessage(`{"name":"test2"}`))
106+
Equal(t, err, nil)
107+
Equal(t, optionMyStruct, Some(myStruct{Name: "test2"}))
108+
109+
var optionArrayOfMyStruct Option[[]myStruct]
110+
err = optionArrayOfMyStruct.Scan([]byte(`[{"name":"test"}]`))
111+
Equal(t, err, nil)
112+
Equal(t, optionArrayOfMyStruct, Some([]myStruct{{Name: "test"}}))
113+
114+
var optionMap Option[map[string]any]
115+
err = optionMap.Scan([]byte(`{"name":"test"}`))
116+
Equal(t, err, nil)
117+
Equal(t, optionMap, Some(map[string]any{"name": "test"}))
118+
}
119+
14120
func TestNilOption(t *testing.T) {
15121
value := Some[any](nil)
16122
Equal(t, false, value.IsNone())

0 commit comments

Comments
 (0)