diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 247c728..0622eaf 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -38,6 +38,6 @@ jobs: with: go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v4 with: version: latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index b66cec6..75c109b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out - +.idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 8476b67..c3e3f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.28.0] - 2024-02-13 +### Added +- Additionally supported types, cast to `sql.Valuer` supported types. + +### Changed +- Option scan to take advantage of new `sql.Null` and `reflect.TypeFor` for go1.22+. +- `BytesToString` & `StringToBytes` to use `unsafe.String` & `unsafe.Slice` for go1.21+. + +### Deprecated +- `mathext.Min` & `mathext.Max` in favour of std lib min & max. + +### Fixed +- Some documentation typos. + ## [5.27.0] - 2024-01-29 ### Changed - `sliceext.Retain` & `sliceext.Filter` to not shuffle data in the underlying slice array but create new slice referencing the data instead. In practice, it can cause unexpected behaviour and users expectations not met when the same data is also referenced elsewhere. If anyone still requires a `shuffle` implementation for efficiency I'd be happy to add a separate function for that as well. @@ -102,7 +116,9 @@ 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.26.0...HEAD +[Unreleased]: https://github.com/go-playground/pkg/compare/v5.28.0...HEAD +[5.28.0]: https://github.com/go-playground/pkg/compare/v5.27.0..v5.28.0 +[5.27.0]: https://github.com/go-playground/pkg/compare/v5.26.0..v5.27.0 [5.26.0]: https://github.com/go-playground/pkg/compare/v5.25.0..v5.26.0 [5.25.0]: https://github.com/go-playground/pkg/compare/v5.24.0..v5.25.0 [5.24.0]: https://github.com/go-playground/pkg/compare/v5.23.0..v5.24.0 diff --git a/README.md b/README.md index d0fd8e3..ec5fac2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pkg -![Project status](https://img.shields.io/badge/version-5.27.0-green.svg) +![Project status](https://img.shields.io/badge/version-5.28.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) diff --git a/math/max.go b/math/max.go index 5f931c6..012285f 100644 --- a/math/max.go +++ b/math/max.go @@ -1,11 +1,12 @@ -//go:build go1.18 -// +build go1.18 +//go:build go1.18 && !go1.21 +// +build go1.18,!go1.21 package mathext import ( - constraintsext "github.com/go-playground/pkg/v5/constraints" "math" + + constraintsext "github.com/go-playground/pkg/v5/constraints" ) // Max returns the larger value. diff --git a/math/max_go121.go b/math/max_go121.go new file mode 100644 index 0000000..0d78db9 --- /dev/null +++ b/math/max_go121.go @@ -0,0 +1,16 @@ +//go:build go1.21 + +package mathext + +import ( + constraintsext "github.com/go-playground/pkg/v5/constraints" +) + +// Max returns the larger value. +// +// NOTE: this function does not check for difference in floats of 0/zero vs -0/negative zero using Signbit. +// +// Deprecated: use the new std library `max` instead. +func Max[N constraintsext.Number](x, y N) N { + return max(x, y) +} diff --git a/math/min.go b/math/min.go index b5468d6..ec80eeb 100644 --- a/math/min.go +++ b/math/min.go @@ -1,11 +1,12 @@ -//go:build go1.18 -// +build go1.18 +//go:build go1.18 && !go1.21 +// +build go1.18,!go1.21 package mathext import ( - constraintsext "github.com/go-playground/pkg/v5/constraints" "math" + + constraintsext "github.com/go-playground/pkg/v5/constraints" ) // Min returns the smaller value. diff --git a/math/min_go121.go b/math/min_go121.go new file mode 100644 index 0000000..c0a8131 --- /dev/null +++ b/math/min_go121.go @@ -0,0 +1,16 @@ +//go:build go1.21 + +package mathext + +import ( + constraintsext "github.com/go-playground/pkg/v5/constraints" +) + +// Min returns the smaller value. +// +// NOTE: this function does not check for difference in floats of 0/zero vs -0/negative zero using Signbit. +// +// Deprecated: use the new std library `max` instead. +func Min[N constraintsext.Number](x, y N) N { + return min(x, y) +} diff --git a/net/http/helpers.go b/net/http/helpers.go index 1915d4e..745ff3c 100644 --- a/net/http/helpers.go +++ b/net/http/helpers.go @@ -59,7 +59,7 @@ func AcceptedLanguages(r *http.Request) (languages []string) { return } -// Attachment is a helper method for returning an attachement file +// Attachment is a helper method for returning an attachment file // to be downloaded, if you with to open inline see function Inline func Attachment(w http.ResponseWriter, r io.Reader, filename string) (err error) { w.Header().Set(ContentDisposition, "attachment;filename="+filename) @@ -79,7 +79,7 @@ func Inline(w http.ResponseWriter, r io.Reader, filename string) (err error) { return } -// ClientIP implements a best effort algorithm to return the real client IP, it parses +// ClientIP implements the best effort algorithm to return the real client IP, it parses // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. func ClientIP(r *http.Request) (clientIP string) { values := r.Header[XRealIP] @@ -103,7 +103,7 @@ func ClientIP(r *http.Request) (clientIP string) { return } -// JSONStream uses json.Encoder to stream the JSON reponse body. +// JSONStream uses json.Encoder to stream the JSON response body. // // This differs from the JSON helper which unmarshalls into memory first allowing the capture of JSON encoding errors. func JSONStream(w http.ResponseWriter, status int, i interface{}) error { diff --git a/net/http/retryable.go b/net/http/retryable.go index b653424..6162dde 100644 --- a/net/http/retryable.go +++ b/net/http/retryable.go @@ -6,11 +6,12 @@ package httpext import ( "context" "fmt" - bytesext "github.com/go-playground/pkg/v5/bytes" - errorsext "github.com/go-playground/pkg/v5/errors" - resultext "github.com/go-playground/pkg/v5/values/result" "net/http" "strconv" + + bytesext "github.com/go-playground/pkg/v5/bytes" + errorsext "github.com/go-playground/pkg/v5/errors" + . "github.com/go-playground/pkg/v5/values/result" ) var ( @@ -59,7 +60,7 @@ type BuildRequestFn func(ctx context.Context) (*http.Request, error) type IsRetryableStatusCodeFn func(code int) bool // DoRetryableResponse will execute the provided functions code and automatically retry before returning the *http.Response. -func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, buildFn BuildRequestFn) resultext.Result[*http.Response, error] { +func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, buildFn BuildRequestFn) Result[*http.Response, error] { if client == nil { client = http.DefaultClient } @@ -67,7 +68,7 @@ func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[erro for { req, err := buildFn(ctx) if err != nil { - return resultext.Err[*http.Response, error](err) + return Err[*http.Response, error](err) } resp, err := client.Do(req) @@ -75,23 +76,23 @@ func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[erro if retryReason, isRetryable := errorsext.IsRetryableHTTP(err); isRetryable { opt := onRetryFn(ctx, err, retryReason, attempt) if opt.IsSome() { - return resultext.Err[*http.Response, error](opt.Unwrap()) + return Err[*http.Response, error](opt.Unwrap()) } attempt++ continue } - return resultext.Err[*http.Response, error](err) + return Err[*http.Response, error](err) } if isRetryableStatusCode(resp.StatusCode) { opt := onRetryFn(ctx, ErrRetryableStatusCode{Response: resp}, strconv.Itoa(resp.StatusCode), attempt) if opt.IsSome() { - return resultext.Err[*http.Response, error](opt.Unwrap()) + return Err[*http.Response, error](opt.Unwrap()) } attempt++ continue } - return resultext.Ok[*http.Response, error](resp) + return Ok[*http.Response, error](resp) } } @@ -101,25 +102,25 @@ func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[erro // Gzip supported: // - JSON // - XML -func DoRetryable[T any](ctx context.Context, isRetryableFn errorsext.IsRetryableFn[error], onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, expectedResponseCode int, maxMemory bytesext.Bytes, buildFn BuildRequestFn) resultext.Result[T, error] { +func DoRetryable[T any](ctx context.Context, isRetryableFn errorsext.IsRetryableFn[error], onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, expectedResponseCode int, maxMemory bytesext.Bytes, buildFn BuildRequestFn) Result[T, error] { - return errorsext.DoRetryable(ctx, isRetryableFn, onRetryFn, func(ctx context.Context) resultext.Result[T, error] { + return errorsext.DoRetryable(ctx, isRetryableFn, onRetryFn, func(ctx context.Context) Result[T, error] { result := DoRetryableResponse(ctx, onRetryFn, isRetryableStatusCode, client, buildFn) if result.IsErr() { - return resultext.Err[T, error](result.Err()) + return Err[T, error](result.Err()) } resp := result.Unwrap() if resp.StatusCode != expectedResponseCode { - return resultext.Err[T, error](ErrUnexpectedResponse{Response: resp}) + return Err[T, error](ErrUnexpectedResponse{Response: resp}) } defer resp.Body.Close() data, err := DecodeResponse[T](resp, maxMemory) if err != nil { - return resultext.Err[T, error](err) + return Err[T, error](err) } - return resultext.Ok[T, error](data) + return Ok[T, error](data) }) } diff --git a/unsafe/conversions.go b/unsafe/conversions.go index 00ed803..40e5681 100644 --- a/unsafe/conversions.go +++ b/unsafe/conversions.go @@ -1,3 +1,6 @@ +//go:build !go1.21 +// +build !go1.21 + package unsafeext import ( diff --git a/unsafe/conversions_go121.go b/unsafe/conversions_go121.go new file mode 100644 index 0000000..965eb6b --- /dev/null +++ b/unsafe/conversions_go121.go @@ -0,0 +1,21 @@ +//go:build go1.21 + +package unsafeext + +import ( + "unsafe" +) + +// BytesToString converts an array of bytes into a string without allocating. +// The byte slice passed to this function is not to be used after this call as it's unsafe; you have been warned. +func BytesToString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} + +// StringToBytes converts an existing string into an []byte without allocating. +// The string passed to these functions is not to be used again after this call as it's unsafe; you have been warned. +func StringToBytes(s string) (b []byte) { + d := unsafe.StringData(s) + b = unsafe.Slice(d, len(s)) + return +} diff --git a/values/option/option_common.go b/values/option/option_common.go new file mode 100644 index 0000000..6270782 --- /dev/null +++ b/values/option/option_common.go @@ -0,0 +1,130 @@ +//go:build go1.18 +// +build go1.18 + +package optionext + +import ( + "encoding/json" +) + +// 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 +} + +// IsSome returns true if the option is not empty. +func (o Option[T]) IsSome() bool { + return o.isSome +} + +// IsNone returns true if the option is empty. +func (o Option[T]) IsNone() bool { + return !o.isSome +} + +// Unwrap returns the values if the option is not empty or panics. +func (o Option[T]) Unwrap() T { + if o.isSome { + return o.value + } + panic("Option.Unwrap: option is None") +} + +// UnwrapOr returns the contained `Some` value or provided default value. +// +// Arguments passed to `UnwrapOr` are eagerly evaluated; if you are passing the result of a function call, +// look to use `UnwrapOrElse`, which can be lazily evaluated. +func (o Option[T]) UnwrapOr(value T) T { + if o.isSome { + return o.value + } + return value +} + +// UnwrapOrElse returns the contained `Some` value or computes it from a provided function. +func (o Option[T]) UnwrapOrElse(fn func() T) T { + if o.isSome { + return o.value + } + return fn() +} + +// UnwrapOrDefault returns the contained `Some` value or the default value of the type T. +func (o Option[T]) UnwrapOrDefault() T { + return o.value +} + +// And calls the provided function with the contained value if the option is Some, returns the None value otherwise. +func (o Option[T]) And(fn func(T) T) Option[T] { + if o.isSome { + o.value = fn(o.value) + } + return o +} + +// AndThen calls the provided function with the contained value if the option is Some, returns the None value otherwise. +// +// This differs from `And` in that the provided function returns an Option[T] allowing changing of the Option value +// itself. +func (o Option[T]) AndThen(fn func(T) Option[T]) Option[T] { + if o.isSome { + return fn(o.value) + } + return o +} + +// Some creates a new Option with the given values. +func Some[T any](value T) Option[T] { + return Option[T]{value, true} +} + +// None creates an empty Option that represents no values. +func None[T any]() Option[T] { + return Option[T]{} +} + +// 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. +func (o *Option[T]) UnmarshalJSON(data []byte) error { + if len(data) == 4 && string(data[:4]) == "null" { + *o = None[T]() + return nil + } + var v T + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + *o = Some(v) + return nil +} diff --git a/values/option/option.go b/values/option/option_sql.go similarity index 61% rename from values/option/option.go rename to values/option/option_sql.go index d0ed8b3..e13e4a1 100644 --- a/values/option/option.go +++ b/values/option/option_sql.go @@ -1,5 +1,5 @@ -//go:build go1.18 -// +build go1.18 +//go:build go1.18 && !go1.22 +// +build go1.18,!go1.22 package optionext @@ -20,142 +20,20 @@ var ( timeType = reflect.TypeOf((*time.Time)(nil)).Elem() stringType = reflect.TypeOf((*string)(nil)).Elem() int64Type = reflect.TypeOf((*int64)(nil)).Elem() + uInt64Type = reflect.TypeOf((*uint64)(nil)).Elem() float64Type = reflect.TypeOf((*float64)(nil)).Elem() boolType = reflect.TypeOf((*bool)(nil)).Elem() ) -// 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 -} - -// IsSome returns true if the option is not empty. -func (o Option[T]) IsSome() bool { - return o.isSome -} - -// IsNone returns true if the option is empty. -func (o Option[T]) IsNone() bool { - return !o.isSome -} - -// Unwrap returns the values if the option is not empty or panics. -func (o Option[T]) Unwrap() T { - if o.isSome { - return o.value - } - panic("Option.Unwrap: option is None") -} - -// UnwrapOr returns the contained `Some` value or provided default value. -// -// Arguments passed to `UnwrapOr` are eagerly evaluated; if you are passing the result of a function call, -// look to use `UnwrapOrElse`, which can be lazily evaluated. -func (o Option[T]) UnwrapOr(value T) T { - if o.isSome { - return o.value - } - return value -} - -// UnwrapOrElse returns the contained `Some` value or computes it from a provided function. -func (o Option[T]) UnwrapOrElse(fn func() T) T { - if o.isSome { - return o.value - } - return fn() -} - -// UnwrapOrDefault returns the contained `Some` value or the default value of the type T. -func (o Option[T]) UnwrapOrDefault() T { - return o.value -} - -// And calls the provided function with the contained value if the option is Some, returns the None value otherwise. -func (o Option[T]) And(fn func(T) T) Option[T] { - if o.isSome { - o.value = fn(o.value) - } - return o -} - -// AndThen calls the provided function with the contained value if the option is Some, returns the None value otherwise. -// -// This differs from `And` in that the provided function returns an Option[T] allowing changing of the Option value -// itself. -func (o Option[T]) AndThen(fn func(T) Option[T]) Option[T] { - if o.isSome { - return fn(o.value) - } - return o -} - -// Some creates a new Option with the given values. -func Some[T any](value T) Option[T] { - return Option[T]{value, true} -} - -// None creates an empty Option that represents no values. -func None[T any]() Option[T] { - return Option[T]{} -} - -// 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. -func (o *Option[T]) UnmarshalJSON(data []byte) error { - if len(data) == 4 && string(data[:4]) == "null" { - *o = None[T]() - return nil - } - var v T - err := json.Unmarshal(data, &v) - if err != nil { - return err - } - *o = Some(v) - return nil -} - // Value implements the driver.Valuer interface. // // This honours the `driver.Valuer` interface if the value implements it. -// It also supports custom types of the std types and treats all else as []byte/ +// It also supports custom types of the std types and treats all else as []byte func (o Option[T]) Value() (driver.Value, error) { if o.IsNone() { return nil, nil } - value := o.Unwrap() - val := reflect.ValueOf(value) + val := reflect.ValueOf(o.value) if val.Type().Implements(valuerType) { return val.Interface().(driver.Valuer).Value() @@ -165,9 +43,15 @@ func (o Option[T]) Value() (driver.Value, error) { return val.Convert(stringType).Interface(), nil case reflect.Bool: return val.Convert(boolType).Interface(), nil - case reflect.Int64: + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + n := val.Convert(uInt64Type).Interface().(uint64) + if n > math.MaxInt64 { + return math.MaxInt64, nil + } + return int64(n), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return val.Convert(int64Type).Interface(), nil - case reflect.Float64: + case reflect.Float32, reflect.Float64: return val.Convert(float64Type).Interface(), nil case reflect.Slice, reflect.Array: if val.Type().ConvertibleTo(byteSliceType) { @@ -182,7 +66,7 @@ func (o Option[T]) Value() (driver.Value, error) { case reflect.Map: return json.Marshal(val.Interface()) default: - return val.Interface(), nil + return o.value, nil } } diff --git a/values/option/option_sql_go1.22.go b/values/option/option_sql_go1.22.go new file mode 100644 index 0000000..df5f11f --- /dev/null +++ b/values/option/option_sql_go1.22.go @@ -0,0 +1,181 @@ +//go:build go1.22 + +package optionext + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "fmt" + "math" + "reflect" + "time" +) + +var ( + scanType = reflect.TypeFor[sql.Scanner]() + valuerType = reflect.TypeFor[driver.Valuer]() + byteSliceType = reflect.TypeFor[[]byte]() + timeType = reflect.TypeFor[time.Time]() + stringType = reflect.TypeFor[string]() + int64Type = reflect.TypeFor[int64]() + uInt64Type = reflect.TypeFor[uint64]() + float64Type = reflect.TypeFor[float64]() + boolType = reflect.TypeFor[bool]() +) + +// Value implements the driver.Valuer interface. +// +// This honours the `driver.Valuer` interface if the value implements it. +// It also supports custom types of the std types and treats all else as []byte +func (o Option[T]) Value() (driver.Value, error) { + if o.IsNone() { + return nil, nil + } + val := reflect.ValueOf(o.value) + + if val.Type().Implements(valuerType) { + return val.Interface().(driver.Valuer).Value() + } + switch val.Kind() { + case reflect.String: + return val.Convert(stringType).Interface(), nil + case reflect.Bool: + return val.Convert(boolType).Interface(), nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + n := val.Convert(uInt64Type).Interface().(uint64) + if n > math.MaxInt64 { + return math.MaxInt64, nil + } + return int64(n), nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return val.Convert(int64Type).Interface(), nil + case reflect.Float32, reflect.Float64: + return val.Convert(float64Type).Interface(), nil + case reflect.Slice, reflect.Array: + if val.Type().ConvertibleTo(byteSliceType) { + return val.Convert(byteSliceType).Interface(), nil + } + return json.Marshal(val.Interface()) + case reflect.Struct: + if val.CanConvert(timeType) { + return val.Convert(timeType).Interface(), nil + } + return json.Marshal(val.Interface()) + case reflect.Map: + return json.Marshal(val.Interface()) + default: + return o.value, nil + } +} + +// Scan implements the sql.Scanner interface. +func (o *Option[T]) Scan(value any) error { + + if value == nil { + *o = None[T]() + return nil + } + + 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 + } + + val = val.Elem() + + switch val.Kind() { + case reflect.String, reflect.Bool, reflect.Uint8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float64: + var v sql.Null[T] + if err := v.Scan(value); err != nil { + return err + } + *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) + case reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + v := reflect.ValueOf(value) + if v.Type().ConvertibleTo(val.Type()) { + *o = Some(reflect.ValueOf(v.Convert(val.Type()).Interface()).Interface().(T)) + } else { + return fmt.Errorf("value %T not convertable to %T", value, o.value) + } + case reflect.Float32: + var v sql.Null[float64] + if err := v.Scan(value); err != nil { + return err + } + *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) + case reflect.Int: + var v sql.Null[int64] + if err := v.Scan(value); err != nil { + return err + } + if v.V > math.MaxInt || v.V < math.MinInt { + return fmt.Errorf("value %d out of range for int", v.V) + } + *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) + case reflect.Int8: + var v sql.Null[int64] + if err := v.Scan(value); err != nil { + return err + } + if v.V > math.MaxInt8 || v.V < math.MinInt8 { + return fmt.Errorf("value %d out of range for int8", v.V) + } + *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).Interface().(T)) + case reflect.Interface: + *o = Some(reflect.ValueOf(value).Convert(val.Type()).Interface().(T)) + case reflect.Struct: + if val.CanConvert(timeType) { + switch t := value.(type) { + case string: + tm, err := time.Parse(time.RFC3339Nano, t) + if err != nil { + return err + } + *o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T)) + + case []byte: + tm, err := time.Parse(time.RFC3339Nano, string(t)) + if err != nil { + return err + } + *o = Some(reflect.ValueOf(tm).Convert(val.Type()).Interface().(T)) + + default: + var v sql.Null[time.Time] + if err := v.Scan(value); err != nil { + return err + } + *o = Some(reflect.ValueOf(v.V).Convert(val.Type()).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 val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 { + *o = Some(reflect.ValueOf(v.Convert(val.Type()).Interface()).Interface().(T)) + } else { + 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 +}