This commit is contained in:
2023-06-07 16:58:17 +02:00
parent c7df9d2264
commit 45d4fd7101
12 changed files with 365 additions and 255 deletions

136
reflectext/casting.go Normal file
View File

@@ -0,0 +1,136 @@
package reflectext
import (
"reflect"
)
var reflectBasicTypes = map[reflect.Kind]reflect.Type{
reflect.Bool: reflect.TypeOf(false),
reflect.Int: reflect.TypeOf(int(0)),
reflect.Int8: reflect.TypeOf(int8(0)),
reflect.Int16: reflect.TypeOf(int16(0)),
reflect.Int32: reflect.TypeOf(int32(0)),
reflect.Int64: reflect.TypeOf(int64(0)),
reflect.Uint: reflect.TypeOf(uint(0)),
reflect.Uint8: reflect.TypeOf(uint8(0)),
reflect.Uint16: reflect.TypeOf(uint16(0)),
reflect.Uint32: reflect.TypeOf(uint32(0)),
reflect.Uint64: reflect.TypeOf(uint64(0)),
reflect.Uintptr: reflect.TypeOf(uintptr(0)),
reflect.Float32: reflect.TypeOf(float32(0)),
reflect.Float64: reflect.TypeOf(float64(0)),
reflect.Complex64: reflect.TypeOf(complex64(0)),
reflect.Complex128: reflect.TypeOf(complex128(0)),
reflect.String: reflect.TypeOf(""),
}
// Underlying returns the underlying type of t (without type alias)
//
// https://github.com/golang/go/issues/39574#issuecomment-655664772
func Underlying(t reflect.Type) (ret reflect.Type) {
if t.Name() == "" {
// t is an unnamed type. the underlying type is t itself
return t
}
kind := t.Kind()
if ret = reflectBasicTypes[kind]; ret != nil {
return ret
}
switch kind {
case reflect.Array:
ret = reflect.ArrayOf(t.Len(), t.Elem())
case reflect.Chan:
ret = reflect.ChanOf(t.ChanDir(), t.Elem())
case reflect.Map:
ret = reflect.MapOf(t.Key(), t.Elem())
case reflect.Func:
nIn := t.NumIn()
nOut := t.NumOut()
in := make([]reflect.Type, nIn)
out := make([]reflect.Type, nOut)
for i := 0; i < nIn; i++ {
in[i] = t.In(i)
}
for i := 0; i < nOut; i++ {
out[i] = t.Out(i)
}
ret = reflect.FuncOf(in, out, t.IsVariadic())
case reflect.Interface:
// not supported
case reflect.Ptr:
ret = reflect.PtrTo(t.Elem())
case reflect.Slice:
ret = reflect.SliceOf(t.Elem())
case reflect.Struct:
// only partially supported: embedded fields
// and unexported fields may cause panic in reflect.StructOf()
defer func() {
// if a panic happens, return t unmodified
if recover() != nil && ret == nil {
ret = t
}
}()
n := t.NumField()
fields := make([]reflect.StructField, n)
for i := 0; i < n; i++ {
fields[i] = t.Field(i)
}
ret = reflect.StructOf(fields)
}
return ret
}
// TryCast works similar to `v2, ok := v.(T)`
// Except it works through type alias'
func TryCast[T any](v any) (T, bool) {
underlying := Underlying(reflect.TypeOf(v))
def := *new(T)
if underlying != Underlying(reflect.TypeOf(def)) {
return def, false
}
r1 := reflect.ValueOf(v)
if !r1.CanConvert(underlying) {
return def, false
}
r2 := r1.Convert(underlying)
r3 := r2.Interface()
r4, ok := r3.(T)
if !ok {
return def, false
}
return r4, true
}
func TryCastType(v any, dest reflect.Type) (any, bool) {
underlying := Underlying(reflect.TypeOf(v))
if underlying != Underlying(dest) {
return nil, false
}
r1 := reflect.ValueOf(v)
if !r1.CanConvert(underlying) {
return nil, false
}
r2 := r1.Convert(underlying)
if !r2.CanConvert(dest) {
return nil, false
}
r4 := r2.Convert(dest)
return r4.Interface(), true
}

136
reflectext/primStrSer.go Normal file
View File

@@ -0,0 +1,136 @@
package reflectext
import (
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
"strconv"
"strings"
"time"
)
var primitiveSerializer = map[reflect.Type]genSerializer{
reflect.TypeOf(""): newGenSerializer(serStringToString, serStringToString),
reflect.TypeOf(int(0)): newGenSerializer(serIntNumToString[int], serStringToSIntNum[int]),
reflect.TypeOf(int32(0)): newGenSerializer(serIntNumToString[int32], serStringToSIntNum[int32]),
reflect.TypeOf(int64(0)): newGenSerializer(serIntNumToString[int64], serStringToSIntNum[int64]),
reflect.TypeOf(uint(0)): newGenSerializer(serIntNumToString[uint], serStringToUIntNum[uint]),
reflect.TypeOf(uint32(0)): newGenSerializer(serIntNumToString[uint32], serStringToUIntNum[uint32]),
reflect.TypeOf(uint64(0)): newGenSerializer(serIntNumToString[uint64], serStringToUIntNum[uint64]),
reflect.TypeOf(float32(0)): newGenSerializer(serFloatNumToString[float32], serStringToFloatNum[float32]),
reflect.TypeOf(float64(0)): newGenSerializer(serFloatNumToString[float64], serStringToFloatNum[float64]),
reflect.TypeOf(true): newGenSerializer(serBoolToString, serStringToBool),
reflect.TypeOf(primitive.ObjectID{}): newGenSerializer(serObjectIDToString, serStringToObjectID),
reflect.TypeOf(time.Time{}): newGenSerializer(serTimeToString, serStringToTime),
}
type genSerializer struct {
ToString func(v any) (string, error)
FromString func(v string) (any, error)
}
func newGenSerializer[TData any](tostr func(v TData) (string, error), fromstr func(v string) (TData, error)) genSerializer {
return genSerializer{
ToString: func(v any) (string, error) {
if tdv, ok := v.(TData); ok {
rv, err := tostr(tdv)
if err != nil {
return "", err
}
return rv, nil
} else {
return "", errors.New(fmt.Sprintf("cannot convert type %T to TData (%T)", v, *new(TData)))
}
},
FromString: func(v string) (any, error) {
nv, err := fromstr(v)
if err != nil {
return "", err
}
return nv, nil
},
}
}
func serStringToString(v string) (string, error) {
return v, nil
}
func serIntNumToString[TNum langext.IntegerConstraint](v TNum) (string, error) {
return strconv.FormatInt(int64(v), 10), nil
}
func serStringToSIntNum[TNum langext.SignedConstraint](v string) (TNum, error) {
r, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return 0, err
}
return TNum(r), nil
}
func serStringToUIntNum[TNum langext.UnsignedConstraint](v string) (TNum, error) {
r, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return 0, err
}
return TNum(r), nil
}
func serFloatNumToString[TNum langext.FloatConstraint](v TNum) (string, error) {
return strconv.FormatFloat(float64(v), 'f', -1, 64), nil
}
func serStringToFloatNum[TNum langext.FloatConstraint](v string) (TNum, error) {
r, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0, err
}
return TNum(r), nil
}
func serBoolToString(v bool) (string, error) {
return langext.Conditional(v, "true", "false"), nil
}
func serStringToBool(v string) (bool, error) {
if strings.ToLower(v) == "true" {
return true, nil
}
if strings.ToLower(v) == "false" {
return true, nil
}
return false, errors.New(fmt.Sprintf("invalid boolean value '%s'", v))
}
func serObjectIDToString(v primitive.ObjectID) (string, error) {
return v.Hex(), nil
}
func serStringToObjectID(v string) (primitive.ObjectID, error) {
if rv, err := primitive.ObjectIDFromHex(v); err == nil {
return rv, nil
} else {
return primitive.ObjectID{}, err
}
}
func serTimeToString(v time.Time) (string, error) {
return v.Format(time.RFC3339Nano), nil
}
func serStringToTime(v string) (time.Time, error) {
if rv, err := time.Parse(time.RFC3339Nano, v); err == nil {
return rv, nil
} else {
return time.Time{}, err
}
}

View File

@@ -0,0 +1,88 @@
package reflectext
import (
"errors"
"fmt"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect"
)
// PrimitiveStringSerializer is used to serialize primitive types (and a few more) from and to string
// This is not really intended to be user facing, and more as a simple building block for other mechanisms
// supports:
// - golang primitives (ints, uints, floats, bool, string)
// - type aliases
// - time.Time
// - primitive.ObjectID
type PrimitiveStringSerializer struct{}
func (pss PrimitiveStringSerializer) ValueToString(v any) (string, error) {
inType := reflect.TypeOf(v)
if inType.Kind() == reflect.Ptr && langext.IsNil(v) {
return "", nil
}
if inType.Kind() == reflect.Ptr {
rval1 := reflect.ValueOf(v)
rval2 := rval1.Elem()
rval3 := rval2.Interface()
return pss.ValueToString(rval3)
}
if conv, ok := primitiveSerializer[inType]; ok {
return conv.ToString(v)
}
for convType, conv := range primitiveSerializer {
if castV, ok := TryCastType(v, convType); ok {
return conv.ToString(castV)
}
}
return "", errors.New(fmt.Sprintf("failed to find a matching generic <toString> conversion fo type %T", v))
}
func (pss PrimitiveStringSerializer) ValueFromString(str string, outType reflect.Type) (any, error) {
if outType.Kind() == reflect.Ptr && str == "" {
return reflect.Zero(outType).Interface(), nil // = nil.(outType), nil
}
if outType.Kind() == reflect.Ptr {
innerValue, err := pss.ValueFromString(str, outType.Elem())
if err != nil {
return nil, err
}
// this weird piece of shit converts innerValue to &innerValue (while keeping types)
rval1 := reflect.ValueOf(innerValue)
rval2 := rval1.Convert(outType.Elem())
rval3 := reflect.New(outType.Elem())
rval3.Elem().Set(rval2)
rval4 := rval3.Interface()
return rval4, nil
}
if conv, ok := primitiveSerializer[outType]; ok {
return conv.FromString(str)
}
emptyResultVal := reflect.Zero(outType).Interface()
for convType, conv := range primitiveSerializer {
if _, ok := TryCastType(emptyResultVal, convType); ok {
if convVal, err := conv.FromString(str); err == nil {
if resVal, ok := TryCastType(convVal, outType); ok {
return resVal, nil
}
}
}
}
return "", errors.New(fmt.Sprintf("failed to find a matching generic <toString> conversion fo type %s", outType.String()))
}