Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
aae8a706e9
|
|||
7d64f18f54
|
|||
d08b2e565a
|
@@ -1,5 +1,5 @@
|
||||
package goext
|
||||
|
||||
const GoextVersion = "0.0.366"
|
||||
const GoextVersion = "0.0.369"
|
||||
|
||||
const GoextVersionTimestamp = "2024-01-12T15:10:48+0100"
|
||||
const GoextVersionTimestamp = "2024-01-13T02:01:30+0100"
|
||||
|
98
reflectext/mapAccess.go
Normal file
98
reflectext/mapAccess.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package reflectext
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetMapPath returns the value deep inside a hierahically nested map structure
|
||||
// eg:
|
||||
// x := langext.H{"K1": langext.H{"K2": 665}}
|
||||
// GetMapPath[int](x, "K1.K2") == 665
|
||||
func GetMapPath[TData any](mapval any, path string) (TData, bool) {
|
||||
var ok bool
|
||||
|
||||
split := strings.Split(path, ".")
|
||||
|
||||
for i, key := range split {
|
||||
|
||||
if i < len(split)-1 {
|
||||
mapval, ok = GetMapField[any](mapval, key)
|
||||
if !ok {
|
||||
return *new(TData), false
|
||||
}
|
||||
} else {
|
||||
return GetMapField[TData](mapval, key)
|
||||
}
|
||||
}
|
||||
|
||||
return *new(TData), false
|
||||
}
|
||||
|
||||
// GetMapField gets the value of a map, without knowing the actual types (mapval is any)
|
||||
// eg:
|
||||
// x := langext.H{"K1": 665}
|
||||
// GetMapPath[int](x, "K1") == 665
|
||||
//
|
||||
// works with aliased types and autom. dereferences pointes
|
||||
func GetMapField[TData any, TKey comparable](mapval any, key TKey) (TData, bool) {
|
||||
|
||||
rval := reflect.ValueOf(mapval)
|
||||
|
||||
for rval.Kind() == reflect.Ptr && !rval.IsNil() {
|
||||
rval = rval.Elem()
|
||||
}
|
||||
|
||||
if rval.Kind() != reflect.Map {
|
||||
return *new(TData), false // mapval is not a map
|
||||
}
|
||||
|
||||
kval := reflect.ValueOf(key)
|
||||
|
||||
if !kval.Type().AssignableTo(rval.Type().Key()) {
|
||||
return *new(TData), false // key cannot index mapval
|
||||
}
|
||||
|
||||
eval := rval.MapIndex(kval)
|
||||
if !eval.IsValid() {
|
||||
return *new(TData), false // key does not exist in mapval
|
||||
}
|
||||
|
||||
destType := reflect.TypeOf(new(TData)).Elem()
|
||||
|
||||
if eval.Type() == destType {
|
||||
return eval.Interface().(TData), true
|
||||
}
|
||||
|
||||
if eval.CanConvert(destType) && !preventConvert(eval.Type(), destType) {
|
||||
return eval.Convert(destType).Interface().(TData), true
|
||||
}
|
||||
|
||||
if (eval.Kind() == reflect.Ptr || eval.Kind() == reflect.Interface) && eval.IsNil() && destType.Kind() == reflect.Ptr {
|
||||
return *new(TData), false // special case: mapval[key] is nil
|
||||
}
|
||||
|
||||
for (eval.Kind() == reflect.Ptr || eval.Kind() == reflect.Interface) && !eval.IsNil() {
|
||||
eval = eval.Elem()
|
||||
|
||||
if eval.Type() == destType {
|
||||
return eval.Interface().(TData), true
|
||||
}
|
||||
|
||||
if eval.CanConvert(destType) && !preventConvert(eval.Type(), destType) {
|
||||
return eval.Convert(destType).Interface().(TData), true
|
||||
}
|
||||
}
|
||||
|
||||
return *new(TData), false // mapval[key] is not of type TData
|
||||
}
|
||||
|
||||
func preventConvert(t1 reflect.Type, t2 reflect.Type) bool {
|
||||
if t1.Kind() == reflect.String && t1.Kind() != reflect.String {
|
||||
return true
|
||||
}
|
||||
if t2.Kind() == reflect.String && t1.Kind() != reflect.String {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
55
reflectext/mapAccess_test.go
Normal file
55
reflectext/mapAccess_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package reflectext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/tst"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetMapPath(t *testing.T) {
|
||||
type PseudoInt = int64
|
||||
|
||||
mymap2 := map[string]map[string]any{"Test": {"Second": 3}}
|
||||
|
||||
var maany2 any = mymap2
|
||||
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[int](maany2, "Test.Second")), "3 true")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[int](maany2, "Test2.Second")), "0 false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[int](maany2, "Test.Second2")), "0 false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[string](maany2, "Test.Second")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[string](maany2, "Test2.Second")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[string](maany2, "Test.Second2")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[PseudoInt](maany2, "Test.Second")), "3 true")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[PseudoInt](maany2, "Test2.Second")), "0 false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapPath[PseudoInt](maany2, "Test.Second2")), "0 false")
|
||||
}
|
||||
|
||||
func TestGetMapField(t *testing.T) {
|
||||
type PseudoInt = int64
|
||||
|
||||
mymap1 := map[string]any{"Test": 12}
|
||||
mymap2 := map[string]int{"Test": 12}
|
||||
|
||||
var maany1 any = mymap1
|
||||
var maany2 any = mymap2
|
||||
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany1, "Test")), "12 true")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany1, "Test2")), "0 false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany1, "Test")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany1, "Test2")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany1, "Test")), "12 true")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany1, "Test2")), "0 false")
|
||||
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany2, "Test")), "12 true")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[int](maany2, "Test2")), "0 false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany2, "Test")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[string](maany2, "Test2")), "false")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany2, "Test")), "12 true")
|
||||
tst.AssertEqual(t, fmt.Sprint(GetMapField[PseudoInt](maany2, "Test2")), "0 false")
|
||||
}
|
||||
|
||||
func main2() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
}
|
@@ -53,7 +53,7 @@ func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn strin
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
setClauses = append(setClauses, fmt.Sprintf("(%s = :%s)", columnName, params.Add(val)))
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = :%s", columnName, params.Add(val)))
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -93,3 +93,62 @@ func TestTypeConverter2(t *testing.T) {
|
||||
tst.AssertEqual(t, "002", r.ID)
|
||||
tst.AssertEqual(t, t0.UnixNano(), r.Timestamp.UnixNano())
|
||||
}
|
||||
|
||||
func TestTypeConverter3(t *testing.T) {
|
||||
|
||||
if !langext.InArray("sqlite3", sql.Drivers()) {
|
||||
sqlite.RegisterAsSQLITE3()
|
||||
}
|
||||
|
||||
type RequestData struct {
|
||||
ID string `db:"id"`
|
||||
Timestamp *rfctime.UnixMilliTime `db:"timestamp"`
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
dbdir := t.TempDir()
|
||||
dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3")
|
||||
|
||||
tst.AssertNoErr(t, os.MkdirAll(dbdir, os.ModePerm))
|
||||
|
||||
url := fmt.Sprintf("file:%s?_pragma=journal_mode(%s)&_pragma=timeout(%d)&_pragma=foreign_keys(%s)&_pragma=busy_timeout(%d)", dbfile1, "DELETE", 1000, "true", 1000)
|
||||
|
||||
xdb := tst.Must(sqlx.Open("sqlite", url))(t)
|
||||
|
||||
db := NewDB(xdb)
|
||||
db.RegisterDefaultConverter()
|
||||
|
||||
_, err := db.Exec(ctx, "CREATE TABLE `requests` ( id TEXT NOT NULL, timestamp INTEGER NULL, PRIMARY KEY (id) ) STRICT", PP{})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
t0 := rfctime.NewUnixMilli(time.Date(2012, 03, 01, 16, 0, 0, 0, time.UTC))
|
||||
|
||||
_, err = InsertSingle(ctx, db, "requests", RequestData{
|
||||
ID: "001",
|
||||
Timestamp: &t0,
|
||||
})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
_, err = InsertSingle(ctx, db, "requests", RequestData{
|
||||
ID: "002",
|
||||
Timestamp: nil,
|
||||
})
|
||||
tst.AssertNoErr(t, err)
|
||||
|
||||
{
|
||||
r1, err := QuerySingle[RequestData](ctx, db, "SELECT * FROM requests WHERE id = '001'", PP{}, SModeExtended, Safe)
|
||||
tst.AssertNoErr(t, err)
|
||||
fmt.Printf("%+v\n", r1)
|
||||
tst.AssertEqual(t, "001", r1.ID)
|
||||
tst.AssertEqual(t, t0.UnixNano(), r1.Timestamp.UnixNano())
|
||||
}
|
||||
|
||||
{
|
||||
r2, err := QuerySingle[RequestData](ctx, db, "SELECT * FROM requests WHERE id = '002'", PP{}, SModeExtended, Safe)
|
||||
tst.AssertNoErr(t, err)
|
||||
fmt.Printf("%+v\n", r2)
|
||||
tst.AssertEqual(t, "002", r2.ID)
|
||||
tst.AssertEqual(t, nil, r2.Timestamp)
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// forked from sqlx, but added ability to unmarshal optional-nested structs
|
||||
@@ -18,7 +19,7 @@ type StructScanner struct {
|
||||
|
||||
fields [][]int
|
||||
values []any
|
||||
converter []DBTypeConverter
|
||||
converter []ssConverter
|
||||
columns []string
|
||||
}
|
||||
|
||||
@@ -30,6 +31,11 @@ func NewStructScanner(rows *sqlx.Rows, unsafe bool) *StructScanner {
|
||||
}
|
||||
}
|
||||
|
||||
type ssConverter struct {
|
||||
Converter DBTypeConverter
|
||||
RefCount int
|
||||
}
|
||||
|
||||
func (r *StructScanner) Start(dest any) error {
|
||||
v := reflect.ValueOf(dest)
|
||||
|
||||
@@ -49,7 +55,7 @@ func (r *StructScanner) Start(dest any) error {
|
||||
return fmt.Errorf("missing destination name %s in %T", columns[f], dest)
|
||||
}
|
||||
r.values = make([]interface{}, len(columns))
|
||||
r.converter = make([]DBTypeConverter, len(columns))
|
||||
r.converter = make([]ssConverter, len(columns))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -143,13 +149,19 @@ func (r *StructScanner) StructScanExt(q Queryable, dest any) error {
|
||||
|
||||
f.Set(reflect.Zero(f.Type())) // set to nil
|
||||
} else {
|
||||
if r.converter[i] != nil {
|
||||
val3 := val2.Elem().Interface()
|
||||
conv3, err := r.converter[i].DBToModel(val3)
|
||||
if r.converter[i].Converter != nil {
|
||||
val3 := val2.Elem()
|
||||
conv3, err := r.converter[i].Converter.DBToModel(val3.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Set(reflect.ValueOf(conv3))
|
||||
conv3RVal := reflect.ValueOf(conv3)
|
||||
for j := 0; j < r.converter[i].RefCount; j++ {
|
||||
newConv3Val := reflect.New(conv3RVal.Type())
|
||||
newConv3Val.Elem().Set(conv3RVal)
|
||||
conv3RVal = newConv3Val
|
||||
}
|
||||
f.Set(conv3RVal)
|
||||
} else {
|
||||
f.Set(val2.Elem())
|
||||
}
|
||||
@@ -184,7 +196,7 @@ func (r *StructScanner) StructScanBase(dest any) error {
|
||||
}
|
||||
|
||||
// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
|
||||
func fieldsByTraversalExtended(q Queryable, v reflect.Value, traversals [][]int, values []interface{}, converter []DBTypeConverter) error {
|
||||
func fieldsByTraversalExtended(q Queryable, v reflect.Value, traversals [][]int, values []interface{}, converter []ssConverter) error {
|
||||
v = reflect.Indirect(v)
|
||||
if v.Kind() != reflect.Struct {
|
||||
return errors.New("argument not a struct")
|
||||
@@ -205,14 +217,26 @@ func fieldsByTraversalExtended(q Queryable, v reflect.Value, traversals [][]int,
|
||||
_v := langext.Ptr[any](nil)
|
||||
values[i] = _v
|
||||
foundConverter = true
|
||||
converter[i] = conv
|
||||
converter[i] = ssConverter{Converter: conv, RefCount: 0}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundConverter {
|
||||
// also allow non-pointer converter for pointer-types
|
||||
for _, conv := range q.ListConverter() {
|
||||
if conv.ModelTypeString() == strings.TrimLeft(typeStr, "*") {
|
||||
_v := langext.Ptr[any](nil)
|
||||
values[i] = _v
|
||||
foundConverter = true
|
||||
converter[i] = ssConverter{Converter: conv, RefCount: len(typeStr) - len(strings.TrimLeft(typeStr, "*"))} // kind hacky way to get the amount of ptr before <f>, but it works...
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundConverter {
|
||||
values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface()
|
||||
converter[i] = nil
|
||||
converter[i] = ssConverter{Converter: nil, RefCount: -1}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
Reference in New Issue
Block a user