Compare commits

...

6 Commits

Author SHA1 Message Date
aae8a706e9 v0.0.369 autom. allow usage of existing converter for pointer-types (sq)
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m29s
2024-01-13 02:01:30 +01:00
7d64f18f54 v0.0.368
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m9s
2024-01-13 01:29:40 +01:00
d08b2e565a v0.0.367
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m32s
2024-01-12 18:40:29 +01:00
d29e84894d v0.0.366 ginext: set cookies
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m28s
2024-01-12 15:10:48 +01:00
617298c366 v0.0.365
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 1m24s
2024-01-09 18:23:46 +01:00
668f308565 v0.0.364 add ServerHTTP() to GinWrapper for integration testing
All checks were successful
Build Docker and Deploy / Run goext test-suite (push) Successful in 2m59s
2024-01-09 18:17:55 +01:00
9 changed files with 349 additions and 16 deletions

View File

@@ -9,6 +9,7 @@ import (
"gogs.mikescher.com/BlackForestBytes/goext/rext" "gogs.mikescher.com/BlackForestBytes/goext/rext"
"net" "net"
"net/http" "net/http"
"net/http/httptest"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -16,7 +17,7 @@ import (
type GinWrapper struct { type GinWrapper struct {
engine *gin.Engine engine *gin.Engine
SuppressGinLogs bool suppressGinLogs bool
allowCors bool allowCors bool
ginDebug bool ginDebug bool
@@ -50,7 +51,7 @@ func NewEngine(opt Options) *GinWrapper {
wrapper := &GinWrapper{ wrapper := &GinWrapper{
engine: engine, engine: engine,
SuppressGinLogs: false, suppressGinLogs: false,
allowCors: langext.Coalesce(opt.AllowCors, false), allowCors: langext.Coalesce(opt.AllowCors, false),
ginDebug: langext.Coalesce(opt.GinDebug, true), ginDebug: langext.Coalesce(opt.GinDebug, true),
bufferBody: langext.Coalesce(opt.BufferBody, false), bufferBody: langext.Coalesce(opt.BufferBody, false),
@@ -74,7 +75,7 @@ func NewEngine(opt Options) *GinWrapper {
ginlogger := gin.Logger() ginlogger := gin.Logger()
engine.Use(func(context *gin.Context) { engine.Use(func(context *gin.Context) {
if !wrapper.SuppressGinLogs { if !wrapper.suppressGinLogs {
ginlogger(context) ginlogger(context)
} }
}) })
@@ -185,3 +186,10 @@ func (w *GinWrapper) cleanMiddlewareName(fname string) string {
return fname return fname
} }
// ServeHTTP only used for unit tests
func (w *GinWrapper) ServeHTTP(req *http.Request) *httptest.ResponseRecorder {
respRec := httptest.NewRecorder()
w.engine.ServeHTTP(respRec, req)
return respRec
}

View File

@@ -9,6 +9,16 @@ import (
"os" "os"
) )
type cookieval struct {
name string
value string
maxAge int
path string
domain string
secure bool
httpOnly bool
}
type headerval struct { type headerval struct {
Key string Key string
Val string Val string
@@ -17,6 +27,7 @@ type headerval struct {
type HTTPResponse interface { type HTTPResponse interface {
Write(g *gin.Context) Write(g *gin.Context)
WithHeader(k string, v string) HTTPResponse WithHeader(k string, v string) HTTPResponse
WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse
IsSuccess() bool IsSuccess() bool
} }
@@ -33,6 +44,7 @@ type jsonHTTPResponse struct {
statusCode int statusCode int
data any data any
headers []headerval headers []headerval
cookies []cookieval
} }
func (j jsonHTTPResponse) jsonRenderer(g *gin.Context) json.GoJsonRender { func (j jsonHTTPResponse) jsonRenderer(g *gin.Context) json.GoJsonRender {
@@ -47,6 +59,9 @@ func (j jsonHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers { for _, v := range j.headers {
g.Header(v.Key, v.Val) g.Header(v.Key, v.Val)
} }
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Render(j.statusCode, j.jsonRenderer(g)) g.Render(j.statusCode, j.jsonRenderer(g))
} }
@@ -55,6 +70,11 @@ func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j jsonHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j jsonHTTPResponse) IsSuccess() bool { func (j jsonHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399 return j.statusCode >= 200 && j.statusCode <= 399
} }
@@ -82,12 +102,16 @@ func (j jsonHTTPResponse) Headers() []string {
type emptyHTTPResponse struct { type emptyHTTPResponse struct {
statusCode int statusCode int
headers []headerval headers []headerval
cookies []cookieval
} }
func (j emptyHTTPResponse) Write(g *gin.Context) { func (j emptyHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers { for _, v := range j.headers {
g.Header(v.Key, v.Val) g.Header(v.Key, v.Val)
} }
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Status(j.statusCode) g.Status(j.statusCode)
} }
@@ -96,6 +120,11 @@ func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j emptyHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j emptyHTTPResponse) IsSuccess() bool { func (j emptyHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399 return j.statusCode >= 200 && j.statusCode <= 399
} }
@@ -120,12 +149,16 @@ type textHTTPResponse struct {
statusCode int statusCode int
data string data string
headers []headerval headers []headerval
cookies []cookieval
} }
func (j textHTTPResponse) Write(g *gin.Context) { func (j textHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers { for _, v := range j.headers {
g.Header(v.Key, v.Val) g.Header(v.Key, v.Val)
} }
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.String(j.statusCode, "%s", j.data) g.String(j.statusCode, "%s", j.data)
} }
@@ -134,6 +167,11 @@ func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j textHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j textHTTPResponse) IsSuccess() bool { func (j textHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399 return j.statusCode >= 200 && j.statusCode <= 399
} }
@@ -159,12 +197,16 @@ type dataHTTPResponse struct {
data []byte data []byte
contentType string contentType string
headers []headerval headers []headerval
cookies []cookieval
} }
func (j dataHTTPResponse) Write(g *gin.Context) { func (j dataHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers { for _, v := range j.headers {
g.Header(v.Key, v.Val) g.Header(v.Key, v.Val)
} }
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Data(j.statusCode, j.contentType, j.data) g.Data(j.statusCode, j.contentType, j.data)
} }
@@ -173,6 +215,11 @@ func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j dataHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j dataHTTPResponse) IsSuccess() bool { func (j dataHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399 return j.statusCode >= 200 && j.statusCode <= 399
} }
@@ -198,6 +245,7 @@ type fileHTTPResponse struct {
filepath string filepath string
filename *string filename *string
headers []headerval headers []headerval
cookies []cookieval
} }
func (j fileHTTPResponse) Write(g *gin.Context) { func (j fileHTTPResponse) Write(g *gin.Context) {
@@ -209,6 +257,9 @@ func (j fileHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers { for _, v := range j.headers {
g.Header(v.Key, v.Val) g.Header(v.Key, v.Val)
} }
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.File(j.filepath) g.File(j.filepath)
} }
@@ -217,6 +268,11 @@ func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j fileHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j fileHTTPResponse) IsSuccess() bool { func (j fileHTTPResponse) IsSuccess() bool {
return true return true
} }
@@ -247,17 +303,20 @@ type downloadDataHTTPResponse struct {
data []byte data []byte
filename *string filename *string
headers []headerval headers []headerval
cookies []cookieval
} }
func (j downloadDataHTTPResponse) Write(g *gin.Context) { func (j downloadDataHTTPResponse) Write(g *gin.Context) {
g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later... g.Header("Content-Type", j.mimetype) // if we don't set it here gin does weird file-sniffing later...
if j.filename != nil { if j.filename != nil {
g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename)) g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename))
} }
for _, v := range j.headers { for _, v := range j.headers {
g.Header(v.Key, v.Val) g.Header(v.Key, v.Val)
} }
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Data(j.statusCode, j.mimetype, j.data) g.Data(j.statusCode, j.mimetype, j.data)
} }
@@ -266,6 +325,11 @@ func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j downloadDataHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j downloadDataHTTPResponse) IsSuccess() bool { func (j downloadDataHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399 return j.statusCode >= 200 && j.statusCode <= 399
} }
@@ -290,9 +354,16 @@ type redirectHTTPResponse struct {
statusCode int statusCode int
url string url string
headers []headerval headers []headerval
cookies []cookieval
} }
func (j redirectHTTPResponse) Write(g *gin.Context) { func (j redirectHTTPResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
g.Redirect(j.statusCode, j.url) g.Redirect(j.statusCode, j.url)
} }
@@ -301,6 +372,11 @@ func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j redirectHTTPResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j redirectHTTPResponse) IsSuccess() bool { func (j redirectHTTPResponse) IsSuccess() bool {
return j.statusCode >= 200 && j.statusCode <= 399 return j.statusCode >= 200 && j.statusCode <= 399
} }
@@ -324,9 +400,16 @@ func (j redirectHTTPResponse) Headers() []string {
type jsonAPIErrResponse struct { type jsonAPIErrResponse struct {
err *exerr.ExErr err *exerr.ExErr
headers []headerval headers []headerval
cookies []cookieval
} }
func (j jsonAPIErrResponse) Write(g *gin.Context) { func (j jsonAPIErrResponse) Write(g *gin.Context) {
for _, v := range j.headers {
g.Header(v.Key, v.Val)
}
for _, v := range j.cookies {
g.SetCookie(v.name, v.value, v.maxAge, v.path, v.domain, v.secure, v.httpOnly)
}
j.err.Output(g) j.err.Output(g)
j.err.CallListener(exerr.MethodOutput) j.err.CallListener(exerr.MethodOutput)
@@ -337,6 +420,11 @@ func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse {
return j return j
} }
func (j jsonAPIErrResponse) WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse {
j.cookies = append(j.cookies, cookieval{name, value, maxAge, path, domain, secure, httpOnly})
return j
}
func (j jsonAPIErrResponse) IsSuccess() bool { func (j jsonAPIErrResponse) IsSuccess() bool {
return false return false
} }

1
go.sum
View File

@@ -36,6 +36,7 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=

View File

@@ -1,5 +1,5 @@
package goext package goext
const GoextVersion = "0.0.363" const GoextVersion = "0.0.369"
const GoextVersionTimestamp = "2024-01-09T08:51:46+0100" const GoextVersionTimestamp = "2024-01-13T02:01:30+0100"

98
reflectext/mapAccess.go Normal file
View 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
}

View 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() {
}

View File

@@ -53,7 +53,7 @@ func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn strin
return "", nil, err 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)))
} }
} }

View File

@@ -93,3 +93,62 @@ func TestTypeConverter2(t *testing.T) {
tst.AssertEqual(t, "002", r.ID) tst.AssertEqual(t, "002", r.ID)
tst.AssertEqual(t, t0.UnixNano(), r.Timestamp.UnixNano()) 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)
}
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/jmoiron/sqlx/reflectx" "github.com/jmoiron/sqlx/reflectx"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"reflect" "reflect"
"strings"
) )
// forked from sqlx, but added ability to unmarshal optional-nested structs // forked from sqlx, but added ability to unmarshal optional-nested structs
@@ -18,7 +19,7 @@ type StructScanner struct {
fields [][]int fields [][]int
values []any values []any
converter []DBTypeConverter converter []ssConverter
columns []string 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 { func (r *StructScanner) Start(dest any) error {
v := reflect.ValueOf(dest) 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) return fmt.Errorf("missing destination name %s in %T", columns[f], dest)
} }
r.values = make([]interface{}, len(columns)) r.values = make([]interface{}, len(columns))
r.converter = make([]DBTypeConverter, len(columns)) r.converter = make([]ssConverter, len(columns))
return nil return nil
} }
@@ -143,13 +149,19 @@ func (r *StructScanner) StructScanExt(q Queryable, dest any) error {
f.Set(reflect.Zero(f.Type())) // set to nil f.Set(reflect.Zero(f.Type())) // set to nil
} else { } else {
if r.converter[i] != nil { if r.converter[i].Converter != nil {
val3 := val2.Elem().Interface() val3 := val2.Elem()
conv3, err := r.converter[i].DBToModel(val3) conv3, err := r.converter[i].Converter.DBToModel(val3.Interface())
if err != nil { if err != nil {
return err 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 { } else {
f.Set(val2.Elem()) 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 // 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) v = reflect.Indirect(v)
if v.Kind() != reflect.Struct { if v.Kind() != reflect.Struct {
return errors.New("argument not a 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) _v := langext.Ptr[any](nil)
values[i] = _v values[i] = _v
foundConverter = true foundConverter = true
converter[i] = conv converter[i] = ssConverter{Converter: conv, RefCount: 0}
break 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 { if !foundConverter {
values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface() values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface()
converter[i] = nil converter[i] = ssConverter{Converter: nil, RefCount: -1}
} }
} }
return nil return nil