Skip to content

Commit

Permalink
SQL plus improvements (#34)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
Dean Karn authored Jun 14, 2023
1 parent f910bd5 commit ab6d6f3
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 47 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [5.19.0] - 2023-06-14
### Added
- strconvext.ParseBool(...) which is a drop-in replacement for the std lin strconv.ParseBool(..) with a few more supported values.
- Expanded Option type SQL Scan support to handle Scanning to an Interface, Struct, Slice, Map and anything that implements the sql.Scanner interface.

## [5.18.0] - 2023-05-21
### Added
- typesext.Nothing & valuesext.Nothing for better clarity in generic params and values that represent struct{}. This will provide better code readability and intent.
Expand Down Expand Up @@ -50,7 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added `timext.NanoTime` for fast low level monotonic time with nanosecond precision.

[Unreleased]: https://github.com/go-playground/pkg/compare/v5.18.0...HEAD
[Unreleased]: https://github.com/go-playground/pkg/compare/v5.19.0...HEAD
[5.19.0]: https://github.com/go-playground/pkg/compare/v5.18.0..v5.19.0
[5.18.0]: https://github.com/go-playground/pkg/compare/v5.17.2..v5.18.0
[5.17.2]: https://github.com/go-playground/pkg/compare/v5.17.1..v5.17.2
[5.17.1]: https://github.com/go-playground/pkg/compare/v5.17.0...v5.17.1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pkg

![Project status](https://img.shields.io/badge/version-5.18.0-green.svg)
![Project status](https://img.shields.io/badge/version-5.19.0-green.svg)
[![Lint & Test](https://github.com/go-playground/pkg/actions/workflows/go.yml/badge.svg)](https://github.com/go-playground/pkg/actions/workflows/go.yml)
[![Coverage Status](https://coveralls.io/repos/github/go-playground/pkg/badge.svg?branch=master)](https://coveralls.io/github/go-playground/pkg?branch=master)
[![GoDoc](https://godoc.org/github.com/go-playground/pkg?status.svg)](https://pkg.go.dev/mod/github.com/go-playground/pkg/v5)
Expand Down
21 changes: 21 additions & 0 deletions strconv/bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package strconvext

import (
"strconv"
)

// ParseBool returns the boolean value represented by the string. It extends the std library parse bool with a few more
// valid options.
//
// 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.
func ParseBool(str string) (bool, error) {
switch str {
case "1", "t", "T", "true", "TRUE", "True", "on", "yes", "ok":
return true, nil
case "", "0", "f", "F", "false", "FALSE", "False", "off", "no":
return false, nil
}
// strconv.NumError mimicking exactly the strconv.ParseBool(..) error and type
// to ensure compatibility with std library.
return false, &strconv.NumError{Func: "ParseBool", Num: string([]byte(str)), Err: strconv.ErrSyntax}
}
130 changes: 85 additions & 45 deletions values/option/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,34 @@ import (
"time"
)

var (
scanType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
byteSliceType = reflect.TypeOf(([]byte)(nil))
)

// Option represents a values that represents a values existence.
//
// nil is usually used on Go however this has two problems:
// 1. Checking if the return values is nil is NOT enforced and can lead to panics.
// 2. Using nil is not good enough when nil itself is a valid value.
//
// This implements the sql.Scanner interface and can be used as a sql value for reading and writing. It supports:
// - String
// - Bool
// - Uint8
// - Float64
// - Int16
// - Int32
// - Int64
// - interface{}/any
// - time.Time
// - Struct - when type is convertable to []byte and assumes JSON.
// - Slice - when type is convertable to []byte and assumes JSON.
// - Map types - when type is convertable to []byte and assumes JSON.
//
// This also implements the `json.Marshaler` and `json.Unmarshaler` interfaces. The only caveat is a None value will result
// in a JSON `null` value. there is no way to hook into the std library to make `omitempty` not produce any value at
// this time.
type Option[T any] struct {
value T
isSome bool
Expand Down Expand Up @@ -50,15 +73,15 @@ func None[T any]() Option[T] {
return Option[T]{}
}

// MarshalJSON implements the json.Marshaler interface.
// MarshalJSON implements the `json.Marshaler` interface.
func (o Option[T]) MarshalJSON() ([]byte, error) {
if o.isSome {
return json.Marshal(o.value)
}
return []byte("null"), nil
}

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

// Scan implements the sql.Scanner interface.
func (o *Option[T]) Scan(value any) error {
val := reflect.ValueOf(o.value)

val := reflect.ValueOf(&o.value)

if val.Type().Implements(scanType) {
err := val.Interface().(sql.Scanner).Scan(value)
if err != nil {
return err
}
o.isSome = true
return nil
}

if value == nil {
*o = None[T]()
return nil
}
val = val.Elem()

switch val.Kind() {
case reflect.String:
var v sql.NullString
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.String).Interface().(T))
}
*o = Some(reflect.ValueOf(v.String).Interface().(T))
case reflect.Bool:
var v sql.NullBool
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.Bool).Interface().(T))
}
*o = Some(reflect.ValueOf(v.Bool).Interface().(T))
case reflect.Uint8:
var v sql.NullByte
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.Byte).Interface().(T))
}
*o = Some(reflect.ValueOf(v.Byte).Interface().(T))
case reflect.Float64:
var v sql.NullFloat64
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.Float64).Interface().(T))
}
*o = Some(reflect.ValueOf(v.Float64).Interface().(T))
case reflect.Int16:
var v sql.NullInt16
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.Int16).Interface().(T))
}
*o = Some(reflect.ValueOf(v.Int16).Interface().(T))
case reflect.Int32:
var v sql.NullInt32
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.Int32).Interface().(T))
}
*o = Some(reflect.ValueOf(v.Int32).Interface().(T))
case reflect.Int64:
var v sql.NullInt64
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
*o = Some(reflect.ValueOf(v.Int64).Interface().(T))
}
*o = Some(reflect.ValueOf(v.Int64).Interface().(T))
case reflect.Interface:
*o = Some(reflect.ValueOf(value).Interface().(T))
case reflect.Struct:
if val.Type() == reflect.TypeOf(time.Time{}) {
var v sql.NullTime
if err := v.Scan(value); err != nil {
return err
}
if !v.Valid {
*o = None[T]()
} else {
switch t := value.(type) {
case string:
tm, err := time.Parse(time.RFC3339Nano, t)
if err != nil {
return err
}
*o = Some(reflect.ValueOf(tm).Interface().(T))

case []byte:
tm, err := time.Parse(time.RFC3339Nano, string(t))
if err != nil {
return err
}
*o = Some(reflect.ValueOf(tm).Interface().(T))

default:
var v sql.NullTime
if err := v.Scan(value); err != nil {
return err
}
*o = Some(reflect.ValueOf(v.Time).Interface().(T))
}
return nil
}
fallthrough

default:
switch val.Kind() {
case reflect.Struct, reflect.Slice, reflect.Map:
v := reflect.ValueOf(value)

if v.Type().ConvertibleTo(byteSliceType) {
if err := json.Unmarshal(v.Convert(byteSliceType).Interface().([]byte), &o.value); err != nil {
return err
}
o.isSome = true
return nil
}
}
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", value, o.value)
}
return nil
Expand Down
106 changes: 106 additions & 0 deletions values/option/option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,112 @@ import (
. "github.com/go-playground/assert/v2"
)

type customScanner struct {
S string
}

func (c *customScanner) Scan(src interface{}) error {
c.S = src.(string)
return nil
}

func TestSQL(t *testing.T) {
value := int64(123)
var optionI64 Option[int64]
var optionI32 Option[int32]
var optionI16 Option[int16]
var optionString Option[string]
var optionBool Option[bool]
var optionF64 Option[float64]
var optionByte Option[byte]
var optionTime Option[time.Time]
var optionInterface Option[any]

err := optionInterface.Scan(1)
Equal(t, err, nil)
Equal(t, optionInterface, Some(any(1)))

err = optionInterface.Scan("blah")
Equal(t, err, nil)
Equal(t, optionInterface, Some(any("blah")))

err = optionI64.Scan(value)
Equal(t, err, nil)
Equal(t, optionI64, Some(value))

err = optionI32.Scan(value)
Equal(t, err, nil)
Equal(t, optionI32, Some(int32(value)))

err = optionI16.Scan(value)
Equal(t, err, nil)
Equal(t, optionI16, Some(int16(value)))

err = optionBool.Scan(1)
Equal(t, err, nil)
Equal(t, optionBool, Some(true))

err = optionString.Scan(value)
Equal(t, err, nil)
Equal(t, optionString, Some("123"))

err = optionF64.Scan(2.0)
Equal(t, err, nil)
Equal(t, optionF64, Some(2.0))

err = optionByte.Scan(uint8('1'))
Equal(t, err, nil)
Equal(t, optionByte, Some(uint8('1')))

err = optionTime.Scan("2023-06-13T06:34:32Z")
Equal(t, err, nil)
Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)))

err = optionTime.Scan([]byte("2023-06-13T06:34:32Z"))
Equal(t, err, nil)
Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)))

err = optionTime.Scan(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC))
Equal(t, err, nil)
Equal(t, optionTime, Some(time.Date(2023, 6, 13, 6, 34, 32, 0, time.UTC)))

// Test nil
var nullableOption Option[int64]
err = nullableOption.Scan(nil)
Equal(t, err, nil)
Equal(t, nullableOption, None[int64]())

// custom scanner
var custom Option[customScanner]
err = custom.Scan("GOT HERE")
Equal(t, err, nil)
Equal(t, custom, Some(customScanner{S: "GOT HERE"}))

// test unmarshal to struct
type myStruct struct {
Name string `json:"name"`
}

var optionMyStruct Option[myStruct]
err = optionMyStruct.Scan([]byte(`{"name":"test"}`))
Equal(t, err, nil)
Equal(t, optionMyStruct, Some(myStruct{Name: "test"}))

err = optionMyStruct.Scan(json.RawMessage(`{"name":"test2"}`))
Equal(t, err, nil)
Equal(t, optionMyStruct, Some(myStruct{Name: "test2"}))

var optionArrayOfMyStruct Option[[]myStruct]
err = optionArrayOfMyStruct.Scan([]byte(`[{"name":"test"}]`))
Equal(t, err, nil)
Equal(t, optionArrayOfMyStruct, Some([]myStruct{{Name: "test"}}))

var optionMap Option[map[string]any]
err = optionMap.Scan([]byte(`{"name":"test"}`))
Equal(t, err, nil)
Equal(t, optionMap, Some(map[string]any{"name": "test"}))
}

func TestNilOption(t *testing.T) {
value := Some[any](nil)
Equal(t, false, value.IsNone())
Expand Down

0 comments on commit ab6d6f3

Please sign in to comment.