Compare commits
	
		
			5 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| be24f7a190 | |||
| aae8a706e9 | |||
| 7d64f18f54 | |||
| d08b2e565a | |||
| d29e84894d | 
| @@ -68,6 +68,7 @@ func init() { | ||||
| } | ||||
|  | ||||
| type Builder struct { | ||||
| 	wrappedErr      error | ||||
| 	errorData       *ExErr | ||||
| 	containsGinData bool | ||||
| 	noLog           bool | ||||
| @@ -89,9 +90,9 @@ func Wrap(err error, msg string) *Builder { | ||||
| 	if !pkgconfig.RecursiveErrors { | ||||
| 		v := FromError(err) | ||||
| 		v.Message = msg | ||||
| 		return &Builder{errorData: v} | ||||
| 		return &Builder{wrappedErr: err, errorData: v} | ||||
| 	} | ||||
| 	return &Builder{errorData: wrapExErr(FromError(err), msg, CatWrap, 1)} | ||||
| 	return &Builder{wrappedErr: err, errorData: wrapExErr(FromError(err), msg, CatWrap, 1)} | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
| @@ -414,6 +415,10 @@ func extractHeader(header map[string][]string) []string { | ||||
| func (b *Builder) Build() error { | ||||
| 	warnOnPkgConfigNotInitialized() | ||||
|  | ||||
| 	if pkgconfig.DisableErrorWrapping && b.wrappedErr != nil { | ||||
| 		return b.wrappedErr | ||||
| 	} | ||||
|  | ||||
| 	if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) { | ||||
| 		b.errorData.ShortLog(stackSkipLogger.Error()) | ||||
| 	} else if pkgconfig.ZeroLogAllTraces && !b.noLog { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ type ErrorPackageConfig struct { | ||||
| 	IncludeMetaInGinOutput bool                                             // Log meta fields ( from e.g. `.Str(key, val).Build()` ) to gin in err.Output() | ||||
| 	ExtendGinOutput        func(err *ExErr, json map[string]any)            // (Optionally) extend the gin output with more fields | ||||
| 	ExtendGinDataOutput    func(err *ExErr, depth int, json map[string]any) // (Optionally) extend the gin `__data` output with more fields | ||||
| 	DisableErrorWrapping   bool                                             // Disables the exerr.Wrap()...Build() function - will always return the original error | ||||
| } | ||||
|  | ||||
| type ErrorPackageConfigInit struct { | ||||
| @@ -23,6 +24,7 @@ type ErrorPackageConfigInit struct { | ||||
| 	IncludeMetaInGinOutput *bool | ||||
| 	ExtendGinOutput        func(err *ExErr, json map[string]any) | ||||
| 	ExtendGinDataOutput    func(err *ExErr, depth int, json map[string]any) | ||||
| 	DisableErrorWrapping   *bool | ||||
| } | ||||
|  | ||||
| var initialized = false | ||||
| @@ -35,6 +37,7 @@ var pkgconfig = ErrorPackageConfig{ | ||||
| 	IncludeMetaInGinOutput: true, | ||||
| 	ExtendGinOutput:        func(err *ExErr, json map[string]any) {}, | ||||
| 	ExtendGinDataOutput:    func(err *ExErr, depth int, json map[string]any) {}, | ||||
| 	DisableErrorWrapping:   false, | ||||
| } | ||||
|  | ||||
| // Init initializes the exerr packages | ||||
| @@ -63,6 +66,7 @@ func Init(cfg ErrorPackageConfigInit) { | ||||
| 		IncludeMetaInGinOutput: langext.Coalesce(cfg.IncludeMetaInGinOutput, pkgconfig.IncludeMetaInGinOutput), | ||||
| 		ExtendGinOutput:        ego, | ||||
| 		ExtendGinDataOutput:    egdo, | ||||
| 		DisableErrorWrapping:   langext.Coalesce(cfg.DisableErrorWrapping, pkgconfig.DisableErrorWrapping), | ||||
| 	} | ||||
|  | ||||
| 	initialized = true | ||||
|   | ||||
| @@ -9,6 +9,16 @@ import ( | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| type cookieval struct { | ||||
| 	name     string | ||||
| 	value    string | ||||
| 	maxAge   int | ||||
| 	path     string | ||||
| 	domain   string | ||||
| 	secure   bool | ||||
| 	httpOnly bool | ||||
| } | ||||
|  | ||||
| type headerval struct { | ||||
| 	Key string | ||||
| 	Val string | ||||
| @@ -17,6 +27,7 @@ type headerval struct { | ||||
| type HTTPResponse interface { | ||||
| 	Write(g *gin.Context) | ||||
| 	WithHeader(k string, v string) HTTPResponse | ||||
| 	WithCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool) HTTPResponse | ||||
| 	IsSuccess() bool | ||||
| } | ||||
|  | ||||
| @@ -33,6 +44,7 @@ type jsonHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	data       any | ||||
| 	headers    []headerval | ||||
| 	cookies    []cookieval | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| 		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)) | ||||
| } | ||||
|  | ||||
| @@ -55,6 +70,11 @@ func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
| @@ -82,12 +102,16 @@ func (j jsonHTTPResponse) Headers() []string { | ||||
| type emptyHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	headers    []headerval | ||||
| 	cookies    []cookieval | ||||
| } | ||||
|  | ||||
| func (j emptyHTTPResponse) 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.Status(j.statusCode) | ||||
| } | ||||
|  | ||||
| @@ -96,6 +120,11 @@ func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
| @@ -120,12 +149,16 @@ type textHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	data       string | ||||
| 	headers    []headerval | ||||
| 	cookies    []cookieval | ||||
| } | ||||
|  | ||||
| func (j textHTTPResponse) 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.String(j.statusCode, "%s", j.data) | ||||
| } | ||||
|  | ||||
| @@ -134,6 +167,11 @@ func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
| @@ -159,12 +197,16 @@ type dataHTTPResponse struct { | ||||
| 	data        []byte | ||||
| 	contentType string | ||||
| 	headers     []headerval | ||||
| 	cookies     []cookieval | ||||
| } | ||||
|  | ||||
| func (j dataHTTPResponse) 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.Data(j.statusCode, j.contentType, j.data) | ||||
| } | ||||
|  | ||||
| @@ -173,6 +215,11 @@ func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
| @@ -198,6 +245,7 @@ type fileHTTPResponse struct { | ||||
| 	filepath string | ||||
| 	filename *string | ||||
| 	headers  []headerval | ||||
| 	cookies  []cookieval | ||||
| } | ||||
|  | ||||
| func (j fileHTTPResponse) Write(g *gin.Context) { | ||||
| @@ -209,6 +257,9 @@ func (j fileHTTPResponse) 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.File(j.filepath) | ||||
| } | ||||
|  | ||||
| @@ -217,6 +268,11 @@ func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return true | ||||
| } | ||||
| @@ -247,17 +303,20 @@ type downloadDataHTTPResponse struct { | ||||
| 	data       []byte | ||||
| 	filename   *string | ||||
| 	headers    []headerval | ||||
| 	cookies    []cookieval | ||||
| } | ||||
|  | ||||
| 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... | ||||
| 	if j.filename != nil { | ||||
| 		g.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", *j.filename)) | ||||
|  | ||||
| 	} | ||||
| 	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.Data(j.statusCode, j.mimetype, j.data) | ||||
| } | ||||
|  | ||||
| @@ -266,6 +325,11 @@ func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
| @@ -290,9 +354,16 @@ type redirectHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	url        string | ||||
| 	headers    []headerval | ||||
| 	cookies    []cookieval | ||||
| } | ||||
|  | ||||
| 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) | ||||
| } | ||||
|  | ||||
| @@ -301,6 +372,11 @@ func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
| @@ -324,9 +400,16 @@ func (j redirectHTTPResponse) Headers() []string { | ||||
| type jsonAPIErrResponse struct { | ||||
| 	err     *exerr.ExErr | ||||
| 	headers []headerval | ||||
| 	cookies []cookieval | ||||
| } | ||||
|  | ||||
| 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.CallListener(exerr.MethodOutput) | ||||
| @@ -337,6 +420,11 @@ func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	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 { | ||||
| 	return false | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| package goext | ||||
|  | ||||
| const GoextVersion = "0.0.365" | ||||
| const GoextVersion = "0.0.370" | ||||
|  | ||||
| const GoextVersionTimestamp = "2024-01-09T18:23:46+0100" | ||||
| const GoextVersionTimestamp = "2024-01-13T14:10:25+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))) | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"sync" | ||||
| ) | ||||
| @@ -56,7 +57,7 @@ func (db *database) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Resul | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, exerr.Wrap(err, "Failed to [exec] sql statement").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 	} | ||||
| 	return res, nil | ||||
| } | ||||
| @@ -77,7 +78,7 @@ func (db *database) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Ro | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, exerr.Wrap(err, "Failed to [query] sql statement").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 	} | ||||
| 	return rows, nil | ||||
| } | ||||
| @@ -97,7 +98,7 @@ func (db *database) Ping(ctx context.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return exerr.Wrap(err, "Failed to [ping] sql database").Build() | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -117,7 +118,7 @@ func (db *database) BeginTransaction(ctx context.Context, iso sql.IsolationLevel | ||||
|  | ||||
| 	xtx, err := db.db.BeginTxx(ctx, &sql.TxOptions{Isolation: iso}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, exerr.Wrap(err, "Failed to start sql transaction").Build() | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range db.lstr { | ||||
|   | ||||
| @@ -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