Compare commits
	
		
			19 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b55dbaacf | |||
| c399fa42ae | |||
| 9e586f7706 | |||
| 3cc8dccc63 | |||
| 7fedfbca81 | |||
| 3c439ba428 | |||
| ad24f6db44 | |||
| 1869ff3d75 | |||
| 30ce8c4b60 | |||
| 885bb53244 | |||
| 1c7dc1820a | |||
| 7e16e799e4 | |||
| 890e16241d | |||
| b9d0348735 | |||
| b9e9575b9b | |||
| 295a098eb4 | |||
| b69a082bb1 | |||
| a4a8c83d17 | |||
| e952176bb0 | 
							
								
								
									
										263
									
								
								cryptext/pronouncablePassword.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								cryptext/pronouncablePassword.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,263 @@ | ||||
| package cryptext | ||||
|  | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"io" | ||||
| 	"math/big" | ||||
| 	mathrand "math/rand" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	ppStartChar            = "BCDFGHJKLMNPQRSTVWXZ" | ||||
| 	ppEndChar              = "ABDEFIKMNORSTUXYZ" | ||||
| 	ppVowel                = "AEIOUY" | ||||
| 	ppConsonant            = "BCDFGHJKLMNPQRSTVWXZ" | ||||
| 	ppSegmentLenMin        = 3 | ||||
| 	ppSegmentLenMax        = 7 | ||||
| 	ppMaxRepeatedVowel     = 2 | ||||
| 	ppMaxRepeatedConsonant = 2 | ||||
| ) | ||||
|  | ||||
| var ppContinuation = map[uint8]string{ | ||||
| 	'A': "BCDFGHJKLMNPRSTVWXYZ", | ||||
| 	'B': "ADFIKLMNORSTUY", | ||||
| 	'C': "AEIKOUY", | ||||
| 	'D': "AEILORSUYZ", | ||||
| 	'E': "BCDFGHJKLMNPRSTVWXYZ", | ||||
| 	'F': "ADEGIKLOPRTUY", | ||||
| 	'G': "ABDEFHILMNORSTUY", | ||||
| 	'H': "AEIOUY", | ||||
| 	'I': "BCDFGHJKLMNPRSTVWXZ", | ||||
| 	'J': "AEIOUY", | ||||
| 	'K': "ADEFHILMNORSTUY", | ||||
| 	'L': "ADEFGIJKMNOPSTUVWYZ", | ||||
| 	'M': "ABEFIKOPSTUY", | ||||
| 	'N': "ABEFIKOPSTUY", | ||||
| 	'O': "BCDFGHJKLMNPRSTVWXYZ", | ||||
| 	'P': "AEFIJLORSTUY", | ||||
| 	'Q': "AEIOUY", | ||||
| 	'R': "ADEFGHIJKLMNOPSTUVYZ", | ||||
| 	'S': "ACDEIKLOPTUYZ", | ||||
| 	'T': "AEHIJOPRSUWY", | ||||
| 	'U': "BCDFGHJKLMNPRSTVWXZ", | ||||
| 	'V': "AEIOUY", | ||||
| 	'W': "AEIOUY", | ||||
| 	'X': "AEIOUY", | ||||
| 	'Y': "ABCDFGHKLMNPRSTVXZ", | ||||
| 	'Z': "AEILOTUY", | ||||
| } | ||||
|  | ||||
| var ppLog2Map = map[int]float64{ | ||||
| 	1:  0.00000000, | ||||
| 	2:  1.00000000, | ||||
| 	3:  1.58496250, | ||||
| 	4:  2.00000000, | ||||
| 	5:  2.32192809, | ||||
| 	6:  2.58496250, | ||||
| 	7:  2.80735492, | ||||
| 	8:  3.00000000, | ||||
| 	9:  3.16992500, | ||||
| 	10: 3.32192809, | ||||
| 	11: 3.45943162, | ||||
| 	12: 3.58496250, | ||||
| 	13: 3.70043972, | ||||
| 	14: 3.80735492, | ||||
| 	15: 3.90689060, | ||||
| 	16: 4.00000000, | ||||
| 	17: 4.08746284, | ||||
| 	18: 4.16992500, | ||||
| 	19: 4.24792751, | ||||
| 	20: 4.32192809, | ||||
| 	21: 4.39231742, | ||||
| 	22: 4.45943162, | ||||
| 	23: 4.52356196, | ||||
| 	24: 4.58496250, | ||||
| 	25: 4.64385619, | ||||
| 	26: 4.70043972, | ||||
| 	27: 4.75488750, | ||||
| 	28: 4.80735492, | ||||
| 	29: 4.85798100, | ||||
| 	30: 4.90689060, | ||||
| 	31: 4.95419631, | ||||
| 	32: 5.00000000, | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	ppVowelMap     = ppMakeSet(ppVowel) | ||||
| 	ppConsonantMap = ppMakeSet(ppConsonant) | ||||
| 	ppEndCharMap   = ppMakeSet(ppEndChar) | ||||
| ) | ||||
|  | ||||
| func ppMakeSet(v string) map[uint8]bool { | ||||
| 	mp := make(map[uint8]bool, len(v)) | ||||
| 	for _, chr := range v { | ||||
| 		mp[uint8(chr)] = true | ||||
| 	} | ||||
| 	return mp | ||||
| } | ||||
|  | ||||
| func ppRandInt(rng io.Reader, max int) int { | ||||
| 	v, err := rand.Int(rng, big.NewInt(int64(max))) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return int(v.Int64()) | ||||
| } | ||||
|  | ||||
| func ppRand(rng io.Reader, chars string, entropy *float64) uint8 { | ||||
| 	chr := chars[ppRandInt(rng, len(chars))] | ||||
|  | ||||
| 	*entropy = *entropy + ppLog2Map[len(chars)] | ||||
|  | ||||
| 	return chr | ||||
| } | ||||
|  | ||||
| func ppCharType(chr uint8) (bool, bool) { | ||||
| 	_, ok1 := ppVowelMap[chr] | ||||
| 	_, ok2 := ppConsonantMap[chr] | ||||
|  | ||||
| 	return ok1, ok2 | ||||
| } | ||||
|  | ||||
| func ppCharsetRemove(cs string, set map[uint8]bool, allowEmpty bool) string { | ||||
| 	result := "" | ||||
| 	for _, chr := range cs { | ||||
| 		if _, ok := set[uint8(chr)]; !ok { | ||||
| 			result += string(chr) | ||||
| 		} | ||||
| 	} | ||||
| 	if result == "" && !allowEmpty { | ||||
| 		return cs | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func ppCharsetFilter(cs string, set map[uint8]bool, allowEmpty bool) string { | ||||
| 	result := "" | ||||
| 	for _, chr := range cs { | ||||
| 		if _, ok := set[uint8(chr)]; ok { | ||||
| 			result += string(chr) | ||||
| 		} | ||||
| 	} | ||||
| 	if result == "" && !allowEmpty { | ||||
| 		return cs | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func PronouncablePasswordExt(rng io.Reader, pwlen int) (string, float64) { | ||||
|  | ||||
| 	// kinda pseudo markov-chain - with a few extra rules and no weights... | ||||
|  | ||||
| 	if pwlen <= 0 { | ||||
| 		return "", 0 | ||||
| 	} | ||||
|  | ||||
| 	vowelCount := 0 | ||||
| 	consoCount := 0 | ||||
| 	entropy := float64(0) | ||||
|  | ||||
| 	startChar := ppRand(rng, ppStartChar, &entropy) | ||||
|  | ||||
| 	result := string(startChar) | ||||
| 	currentChar := startChar | ||||
|  | ||||
| 	isVowel, isConsonant := ppCharType(currentChar) | ||||
| 	if isVowel { | ||||
| 		vowelCount = 1 | ||||
| 	} | ||||
| 	if isConsonant { | ||||
| 		consoCount = ppMaxRepeatedConsonant | ||||
| 	} | ||||
|  | ||||
| 	segmentLen := 1 | ||||
|  | ||||
| 	segmentLenTarget := ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin) | ||||
|  | ||||
| 	for len(result) < pwlen { | ||||
|  | ||||
| 		charset := ppContinuation[currentChar] | ||||
| 		if vowelCount >= ppMaxRepeatedVowel { | ||||
| 			charset = ppCharsetRemove(charset, ppVowelMap, false) | ||||
| 		} | ||||
| 		if consoCount >= ppMaxRepeatedConsonant { | ||||
| 			charset = ppCharsetRemove(charset, ppConsonantMap, false) | ||||
| 		} | ||||
|  | ||||
| 		lastOfSegment := false | ||||
| 		newSegment := false | ||||
|  | ||||
| 		if len(result)+1 == pwlen { | ||||
| 			// last of result | ||||
| 			charset = ppCharsetFilter(charset, ppEndCharMap, false) | ||||
| 		} else if segmentLen+1 == segmentLenTarget { | ||||
| 			// last of segment | ||||
| 			charsetNew := ppCharsetFilter(charset, ppEndCharMap, true) | ||||
| 			if charsetNew != "" { | ||||
| 				charset = charsetNew | ||||
| 				lastOfSegment = true | ||||
| 			} | ||||
| 		} else if segmentLen >= segmentLenTarget { | ||||
| 			// (perhaps) start of new segment | ||||
| 			if _, ok := ppEndCharMap[currentChar]; ok { | ||||
| 				charset = ppStartChar | ||||
| 				newSegment = true | ||||
| 			} else { | ||||
| 				// continue segment for one more char to (hopefully) find an end-char | ||||
| 				charsetNew := ppCharsetFilter(charset, ppEndCharMap, true) | ||||
| 				if charsetNew != "" { | ||||
| 					charset = charsetNew | ||||
| 					lastOfSegment = true | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			// normal continuation | ||||
| 		} | ||||
|  | ||||
| 		newChar := ppRand(rng, charset, &entropy) | ||||
| 		if lastOfSegment { | ||||
| 			currentChar = newChar | ||||
| 			segmentLen++ | ||||
| 			result += strings.ToLower(string(newChar)) | ||||
| 		} else if newSegment { | ||||
| 			currentChar = newChar | ||||
| 			segmentLen = 1 | ||||
| 			result += strings.ToUpper(string(newChar)) | ||||
| 			segmentLenTarget = ppSegmentLenMin + ppRandInt(rng, ppSegmentLenMax-ppSegmentLenMin) | ||||
| 			vowelCount = 0 | ||||
| 			consoCount = 0 | ||||
| 		} else { | ||||
| 			currentChar = newChar | ||||
| 			segmentLen++ | ||||
| 			result += strings.ToLower(string(newChar)) | ||||
| 		} | ||||
|  | ||||
| 		isVowel, isConsonant := ppCharType(currentChar) | ||||
| 		if isVowel { | ||||
| 			vowelCount++ | ||||
| 			consoCount = 0 | ||||
| 		} | ||||
| 		if isConsonant { | ||||
| 			vowelCount = 0 | ||||
| 			if newSegment { | ||||
| 				consoCount = ppMaxRepeatedConsonant | ||||
| 			} else { | ||||
| 				consoCount++ | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result, entropy | ||||
| } | ||||
|  | ||||
| func PronouncablePassword(len int) string { | ||||
| 	v, _ := PronouncablePasswordExt(rand.Reader, len) | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| func PronouncablePasswordSeeded(seed int64, len int) string { | ||||
|  | ||||
| 	v, _ := PronouncablePasswordExt(mathrand.New(mathrand.NewSource(seed)), len) | ||||
| 	return v | ||||
| } | ||||
							
								
								
									
										35
									
								
								cryptext/pronouncablePassword_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								cryptext/pronouncablePassword_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| package cryptext | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestPronouncablePasswordExt(t *testing.T) { | ||||
| 	for i := 0; i < 20; i++ { | ||||
| 		pw, entropy := PronouncablePasswordExt(rand.New(rand.NewSource(int64(i))), 16) | ||||
| 		fmt.Printf("[%.2f] => %s\n", entropy, pw) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPronouncablePasswordSeeded(t *testing.T) { | ||||
| 	for i := 0; i < 20; i++ { | ||||
| 		pw := PronouncablePasswordSeeded(int64(i), 8) | ||||
| 		fmt.Printf("%s\n", pw) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPronouncablePassword(t *testing.T) { | ||||
| 	for i := 0; i < 20; i++ { | ||||
| 		pw := PronouncablePassword(i + 1) | ||||
| 		fmt.Printf("%s\n", pw) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestPronouncablePasswordWrongLen(t *testing.T) { | ||||
| 	PronouncablePassword(0) | ||||
| 	PronouncablePassword(-1) | ||||
| 	PronouncablePassword(-2) | ||||
| 	PronouncablePassword(-3) | ||||
| } | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"go.mongodb.org/mongo-driver/bson" | ||||
| 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"math" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| @@ -667,6 +668,28 @@ func (v MetaValue) rawValueForJson() any { | ||||
| 		} | ||||
| 		return v.Value.(EnumWrap).ValueString | ||||
| 	} | ||||
| 	if v.DataType == MDTFloat32 { | ||||
| 		if math.IsNaN(float64(v.Value.(float32))) { | ||||
| 			return "float64::NaN" | ||||
| 		} else if math.IsInf(float64(v.Value.(float32)), +1) { | ||||
| 			return "float64::+inf" | ||||
| 		} else if math.IsInf(float64(v.Value.(float32)), -1) { | ||||
| 			return "float64::-inf" | ||||
| 		} else { | ||||
| 			return v.Value | ||||
| 		} | ||||
| 	} | ||||
| 	if v.DataType == MDTFloat64 { | ||||
| 		if math.IsNaN(v.Value.(float64)) { | ||||
| 			return "float64::NaN" | ||||
| 		} else if math.IsInf(v.Value.(float64), +1) { | ||||
| 			return "float64::+inf" | ||||
| 		} else if math.IsInf(v.Value.(float64), -1) { | ||||
| 			return "float64::-inf" | ||||
| 		} else { | ||||
| 			return v.Value | ||||
| 		} | ||||
| 	} | ||||
| 	return v.Value | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -163,16 +163,16 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) { | ||||
|  | ||||
| 	ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout)) | ||||
|  | ||||
| 	actx := CreateAppContext(pctx.ginCtx, ictx, cancel) | ||||
|  | ||||
| 	if pctx.persistantData.sessionObj != nil { | ||||
| 		err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, ictx) | ||||
| 		err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, actx) | ||||
| 		if err != nil { | ||||
| 			cancel() | ||||
| 			actx.Cancel() | ||||
| 			return nil, nil, langext.Ptr(Error(exerr.Wrap(err, "Failed to init session").Build())) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	actx := CreateAppContext(pctx.ginCtx, ictx, cancel) | ||||
|  | ||||
| 	return actx, pctx.ginCtx, nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -53,15 +53,11 @@ func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper { | ||||
| func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper { | ||||
| 	defHandler := langext.ArrCopy(w.defaultHandler) | ||||
| 	defHandler = append(defHandler, middleware...) | ||||
| 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler} | ||||
| 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler, absPath: w.absPath} | ||||
| } | ||||
|  | ||||
| func (w *GinRoutesWrapper) WithJSONFilter(filter string) *GinRoutesWrapper { | ||||
| 	defHandler := langext.ArrCopy(w.defaultHandler) | ||||
| 	defHandler = append(defHandler, func(g *gin.Context) { | ||||
| 		g.Set("goext.jsonfilter", filter) | ||||
| 	}) | ||||
| 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler} | ||||
| 	return w.Use(func(g *gin.Context) { g.Set("goext.jsonfilter", filter) }) | ||||
| } | ||||
|  | ||||
| func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder { | ||||
| @@ -116,10 +112,7 @@ func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder { | ||||
| } | ||||
|  | ||||
| func (w *GinRouteBuilder) WithJSONFilter(filter string) *GinRouteBuilder { | ||||
| 	w.handlers = append(w.handlers, func(g *gin.Context) { | ||||
| 		g.Set("goext.jsonfilter", filter) | ||||
| 	}) | ||||
| 	return w | ||||
| 	return w.Use(func(g *gin.Context) { g.Set("goext.jsonfilter", filter) }) | ||||
| } | ||||
|  | ||||
| func (w *GinRouteBuilder) Handle(handler WHandlerFunc) { | ||||
|   | ||||
| @@ -6,6 +6,6 @@ import ( | ||||
| ) | ||||
|  | ||||
| type SessionObject interface { | ||||
| 	Init(g *gin.Context, ctx context.Context) error | ||||
| 	Init(g *gin.Context, ctx *AppContext) error | ||||
| 	Finish(ctx context.Context, resp HTTPResponse) error | ||||
| } | ||||
|   | ||||
							
								
								
									
										22
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								go.mod
									
									
									
									
									
								
							| @@ -7,15 +7,15 @@ require ( | ||||
| 	github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.- | ||||
| 	github.com/jmoiron/sqlx v1.3.5 | ||||
| 	github.com/rs/xid v1.5.0 | ||||
| 	github.com/rs/zerolog v1.31.0 | ||||
| 	go.mongodb.org/mongo-driver v1.13.1 | ||||
| 	golang.org/x/crypto v0.18.0 | ||||
| 	golang.org/x/sys v0.16.0 | ||||
| 	golang.org/x/term v0.16.0 | ||||
| 	github.com/rs/zerolog v1.32.0 | ||||
| 	go.mongodb.org/mongo-driver v1.14.0 | ||||
| 	golang.org/x/crypto v0.19.0 | ||||
| 	golang.org/x/sys v0.17.0 | ||||
| 	golang.org/x/term v0.17.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/bytedance/sonic v1.10.2 // indirect | ||||
| 	github.com/bytedance/sonic v1.11.0 // indirect | ||||
| 	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect | ||||
| 	github.com/chenzhuoyu/iasm v0.9.1 // indirect | ||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||
| @@ -23,14 +23,14 @@ require ( | ||||
| 	github.com/gin-contrib/sse v0.1.0 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.16.0 // indirect | ||||
| 	github.com/go-playground/validator/v10 v10.18.0 // indirect | ||||
| 	github.com/goccy/go-json v0.10.2 // indirect | ||||
| 	github.com/golang/snappy v0.0.4 // indirect | ||||
| 	github.com/google/uuid v1.5.0 // indirect | ||||
| 	github.com/json-iterator/go v1.1.12 // indirect | ||||
| 	github.com/klauspost/compress v1.17.4 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.6 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | ||||
| 	github.com/klauspost/compress v1.17.6 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.7 // indirect | ||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| @@ -45,7 +45,7 @@ require ( | ||||
| 	github.com/xdg-go/stringprep v1.0.4 // indirect | ||||
| 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | ||||
| 	golang.org/x/arch v0.7.0 // indirect | ||||
| 	golang.org/x/net v0.20.0 // indirect | ||||
| 	golang.org/x/net v0.21.0 // indirect | ||||
| 	golang.org/x/sync v0.6.0 // indirect | ||||
| 	golang.org/x/text v0.14.0 // indirect | ||||
| 	google.golang.org/protobuf v1.32.0 // indirect | ||||
|   | ||||
							
								
								
									
										24
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								go.sum
									
									
									
									
									
								
							| @@ -2,6 +2,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 | ||||
| github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= | ||||
| github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= | ||||
| github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | ||||
| github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo= | ||||
| github.com/bytedance/sonic v1.11.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= | ||||
| github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= | ||||
| @@ -31,6 +33,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= | ||||
| github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||
| github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= | ||||
| github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||
| github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= | ||||
| github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= | ||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | ||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | ||||
| github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= | ||||
| @@ -55,12 +61,18 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm | ||||
| github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | ||||
| github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= | ||||
| github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | ||||
| github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= | ||||
| github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= | ||||
| github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||
| github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= | ||||
| github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||
| github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | ||||
| github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= | ||||
| github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= | ||||
| github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= | ||||
| github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||||
| github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= | ||||
| github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||
| @@ -91,6 +103,8 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= | ||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= | ||||
| github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||
| github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= | ||||
| github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| @@ -118,6 +132,8 @@ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/ | ||||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||
| go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= | ||||
| go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= | ||||
| go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= | ||||
| go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= | ||||
| golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | ||||
| @@ -129,6 +145,8 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= | ||||
| golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | ||||
| golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= | ||||
| golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= | ||||
| golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= | ||||
| golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| @@ -139,6 +157,8 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= | ||||
| golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | ||||
| golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= | ||||
| golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= | ||||
| golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= | ||||
| golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | ||||
| @@ -156,10 +176,14 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= | ||||
| golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= | ||||
| golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= | ||||
| golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= | ||||
| golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= | ||||
| golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| package goext | ||||
|  | ||||
| const GoextVersion = "0.0.373" | ||||
| const GoextVersion = "0.0.391" | ||||
|  | ||||
| const GoextVersionTimestamp = "2024-01-14T00:07:01+0100" | ||||
| const GoextVersionTimestamp = "2024-02-21T16:18:04+0100" | ||||
|   | ||||
| @@ -265,6 +265,15 @@ func ArrFirstIndex[T comparable](arr []T, needle T) int { | ||||
| 	return -1 | ||||
| } | ||||
|  | ||||
| func ArrFirstIndexFunc[T any](arr []T, comp func(v T) bool) int { | ||||
| 	for i, v := range arr { | ||||
| 		if comp(v) { | ||||
| 			return i | ||||
| 		} | ||||
| 	} | ||||
| 	return -1 | ||||
| } | ||||
|  | ||||
| func ArrLastIndex[T comparable](arr []T, needle T) int { | ||||
| 	result := -1 | ||||
| 	for i, v := range arr { | ||||
| @@ -275,6 +284,16 @@ func ArrLastIndex[T comparable](arr []T, needle T) int { | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func ArrLastIndexFunc[T any](arr []T, comp func(v T) bool) int { | ||||
| 	result := -1 | ||||
| 	for i, v := range arr { | ||||
| 		if comp(v) { | ||||
| 			result = i | ||||
| 		} | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func AddToSet[T comparable](set []T, add T) []T { | ||||
| 	for _, v := range set { | ||||
| 		if v == add { | ||||
| @@ -479,3 +498,33 @@ func JoinString(arr []string, delimiter string) string { | ||||
|  | ||||
| 	return str | ||||
| } | ||||
|  | ||||
| // ArrChunk splits the array into buckets of max-size `chunkSize` | ||||
| // order is being kept. | ||||
| // The last chunk may contain less than length elements. | ||||
| // | ||||
| // (chunkSize == -1) means no chunking | ||||
| // | ||||
| // see https://www.php.net/manual/en/function.array-chunk.php | ||||
| func ArrChunk[T any](arr []T, chunkSize int) [][]T { | ||||
| 	if chunkSize == -1 { | ||||
| 		return [][]T{arr} | ||||
| 	} | ||||
|  | ||||
| 	res := make([][]T, 0, 1+len(arr)/chunkSize) | ||||
|  | ||||
| 	i := 0 | ||||
| 	for i < len(arr) { | ||||
|  | ||||
| 		right := i + chunkSize | ||||
| 		if right >= len(arr) { | ||||
| 			right = len(arr) | ||||
| 		} | ||||
|  | ||||
| 		res = append(res, arr[i:right]) | ||||
|  | ||||
| 		i = right | ||||
| 	} | ||||
|  | ||||
| 	return res | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								langext/must.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								langext/must.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package langext | ||||
|  | ||||
| // Must returns a value and panics on error | ||||
| // | ||||
| // Usage: Must(methodWithError(...)) | ||||
| func Must[T any](v T, err error) T { | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // MustBool returns a value and panics on missing | ||||
| // | ||||
| // Usage: MustBool(methodWithOkayReturn(...)) | ||||
| func MustBool[T any](v T, ok bool) T { | ||||
| 	if !ok { | ||||
| 		panic("not ok") | ||||
| 	} | ||||
| 	return v | ||||
| } | ||||
							
								
								
									
										29
									
								
								langext/url.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								langext/url.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| package langext | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func BuildUrl(url, path string, params *map[string]string) string { | ||||
| 	if path[:1] == "/" && url[len(url)-1:] == "/" { | ||||
| 		url += path[1:] | ||||
| 	} else if path[:1] != "/" && url[len(url)-1:] != "/" { | ||||
| 		url += "/" + path | ||||
| 	} else { | ||||
| 		url += path | ||||
| 	} | ||||
|  | ||||
| 	if params == nil { | ||||
| 		return url | ||||
| 	} | ||||
|  | ||||
| 	for key, value := range *params { | ||||
| 		if strings.Contains(url, "?") { | ||||
| 			url += fmt.Sprintf("&%s=%s", key, value) | ||||
| 		} else { | ||||
| 			url += fmt.Sprintf("?%s=%s", key, value) | ||||
| 		} | ||||
| 	} | ||||
| 	return url | ||||
| } | ||||
							
								
								
									
										45
									
								
								langext/url_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								langext/url_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| package langext | ||||
|  | ||||
| import ( | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestBuildUrl(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		Url    string | ||||
| 		Path   string | ||||
| 		Params *map[string]string | ||||
| 		Want   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			Url:    "https://test.heydyno.de/", | ||||
| 			Path:   "/testing-01", | ||||
| 			Params: &map[string]string{"param1": "value1"}, | ||||
| 			Want:   "https://test.heydyno.de/testing-01?param1=value1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Url:    "https://test.heydyno.de", | ||||
| 			Path:   "testing-01", | ||||
| 			Params: &map[string]string{"param1": "value1"}, | ||||
| 			Want:   "https://test.heydyno.de/testing-01?param1=value1", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Url:    "https://test.heydyno.de", | ||||
| 			Path:   "/testing-01", | ||||
| 			Params: nil, | ||||
| 			Want:   "https://test.heydyno.de/testing-01", | ||||
| 		}, | ||||
| 		{ | ||||
| 			Url:    "https://test.heydyno.de/", | ||||
| 			Path:   "testing-01", | ||||
| 			Params: nil, | ||||
| 			Want:   "https://test.heydyno.de/testing-01", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range tests { | ||||
| 		res := BuildUrl(test.Url, test.Path, test.Params) | ||||
| 		tst.AssertEqual(t, res, test.Want) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										72
									
								
								reflectext/convertToMap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								reflectext/convertToMap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| package reflectext | ||||
|  | ||||
| import "reflect" | ||||
|  | ||||
| func ConvertStructToMap(v any) any { | ||||
| 	return reflectToMap(reflect.ValueOf(v)) | ||||
| } | ||||
|  | ||||
| func reflectToMap(fv reflect.Value) any { | ||||
|  | ||||
| 	if fv.Kind() == reflect.Ptr { | ||||
|  | ||||
| 		if fv.IsNil() { | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			return reflectToMap(fv.Elem()) | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if fv.Kind() == reflect.Func { | ||||
|  | ||||
| 		// skip | ||||
| 		return nil | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if fv.Kind() == reflect.Array { | ||||
|  | ||||
| 		arrlen := fv.Len() | ||||
| 		arr := make([]any, arrlen) | ||||
| 		for i := 0; i < arrlen; i++ { | ||||
| 			arr[i] = reflectToMap(fv.Index(i)) | ||||
| 		} | ||||
| 		return arr | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if fv.Kind() == reflect.Slice { | ||||
|  | ||||
| 		arrlen := fv.Len() | ||||
| 		arr := make([]any, arrlen) | ||||
| 		for i := 0; i < arrlen; i++ { | ||||
| 			arr[i] = reflectToMap(fv.Index(i)) | ||||
| 		} | ||||
| 		return arr | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if fv.Kind() == reflect.Chan { | ||||
|  | ||||
| 		// skip | ||||
| 		return nil | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	if fv.Kind() == reflect.Struct { | ||||
|  | ||||
| 		res := make(map[string]any) | ||||
|  | ||||
| 		for i := 0; i < fv.NumField(); i++ { | ||||
| 			if fv.Type().Field(i).IsExported() { | ||||
| 				res[fv.Type().Field(i).Name] = reflectToMap(fv.Field(i)) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return res | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	return fv.Interface() | ||||
| } | ||||
| @@ -1,13 +1,14 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn string) (string, PP, error) { | ||||
| func BuildUpdateStatement[TData any](q Queryable, tableName string, obj TData, idColumn string) (string, PP, error) { | ||||
| 	rval := reflect.ValueOf(obj) | ||||
| 	rtyp := rval.Type() | ||||
|  | ||||
| @@ -70,7 +71,7 @@ func BuildUpdateStatement(q Queryable, tableName string, obj any, idColumn strin | ||||
| 	return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, strings.Join(setClauses, ", "), matchClause), params, nil | ||||
| } | ||||
|  | ||||
| func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, error) { | ||||
| func BuildInsertStatement[TData any](q Queryable, tableName string, obj TData) (string, PP, error) { | ||||
| 	rval := reflect.ValueOf(obj) | ||||
| 	rtyp := rval.Type() | ||||
|  | ||||
| @@ -96,7 +97,7 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e | ||||
| 		if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() { | ||||
|  | ||||
| 			fields = append(fields, columnName) | ||||
| 			values = append(fields, "NULL") | ||||
| 			values = append(values, "NULL") | ||||
|  | ||||
| 		} else { | ||||
|  | ||||
| @@ -106,7 +107,7 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e | ||||
| 			} | ||||
|  | ||||
| 			fields = append(fields, columnName) | ||||
| 			values = append(fields, ":"+params.Add(val)) | ||||
| 			values = append(values, ":"+params.Add(val)) | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
| @@ -116,5 +117,83 @@ func BuildInsertStatement(q Queryable, tableName string, obj any) (string, PP, e | ||||
| 	} | ||||
|  | ||||
| 	//goland:noinspection SqlNoDataSourceInspection | ||||
| 	return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(fields, ", "), values), params, nil | ||||
| 	return fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", tableName, strings.Join(fields, ", "), strings.Join(values, ", ")), params, nil | ||||
| } | ||||
|  | ||||
| func BuildInsertMultipleStatement[TData any](q Queryable, tableName string, vArr []TData) (string, PP, error) { | ||||
|  | ||||
| 	if len(vArr) == 0 { | ||||
| 		return "", nil, errors.New("no data supplied") | ||||
| 	} | ||||
|  | ||||
| 	rtyp := reflect.ValueOf(vArr[0]).Type() | ||||
|  | ||||
| 	sqlPrefix := "" | ||||
| 	{ | ||||
| 		columns := make([]string, 0) | ||||
|  | ||||
| 		for i := 0; i < rtyp.NumField(); i++ { | ||||
| 			rsfield := rtyp.Field(i) | ||||
|  | ||||
| 			if !rsfield.IsExported() { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			columnName := rsfield.Tag.Get("db") | ||||
| 			if columnName == "" || columnName == "-" { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			columns = append(columns, "\""+columnName+"\"") | ||||
| 		} | ||||
|  | ||||
| 		sqlPrefix = fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES", tableName, strings.Join(columns, ", ")) | ||||
| 	} | ||||
|  | ||||
| 	pp := PP{} | ||||
|  | ||||
| 	sqlValuesArr := make([]string, 0) | ||||
|  | ||||
| 	for _, v := range vArr { | ||||
|  | ||||
| 		rval := reflect.ValueOf(v) | ||||
|  | ||||
| 		params := make([]string, 0) | ||||
|  | ||||
| 		for i := 0; i < rtyp.NumField(); i++ { | ||||
|  | ||||
| 			rsfield := rtyp.Field(i) | ||||
| 			rvfield := rval.Field(i) | ||||
|  | ||||
| 			if !rsfield.IsExported() { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			columnName := rsfield.Tag.Get("db") | ||||
| 			if columnName == "" || columnName == "-" { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() { | ||||
|  | ||||
| 				params = append(params, "NULL") | ||||
|  | ||||
| 			} else { | ||||
|  | ||||
| 				val, err := convertValueToDB(q, rvfield.Interface()) | ||||
| 				if err != nil { | ||||
| 					return "", nil, err | ||||
| 				} | ||||
|  | ||||
| 				params = append(params, ":"+pp.Add(val)) | ||||
|  | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		sqlValuesArr = append(sqlValuesArr, fmt.Sprintf("(%s)", strings.Join(params, ", "))) | ||||
| 	} | ||||
|  | ||||
| 	sqlstr := fmt.Sprintf("%s %s", sqlPrefix, strings.Join(sqlValuesArr, ", ")) | ||||
|  | ||||
| 	return sqlstr, pp, nil | ||||
| } | ||||
|   | ||||
| @@ -18,9 +18,9 @@ type DBTypeConverter interface { | ||||
| 	DBToModel(v any) (any, error) | ||||
| } | ||||
|  | ||||
| var ConverterBoolToBit = NewDBTypeConverter[bool, int](func(v bool) (int, error) { | ||||
| 	return langext.Conditional(v, 1, 0), nil | ||||
| }, func(v int) (bool, error) { | ||||
| var ConverterBoolToBit = NewDBTypeConverter[bool, int64](func(v bool) (int64, error) { | ||||
| 	return langext.Conditional(v, int64(1), int64(0)), nil | ||||
| }, func(v int64) (bool, error) { | ||||
| 	if v == 0 { | ||||
| 		return false, nil | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										49
									
								
								sq/filter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								sq/filter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| package sq | ||||
|  | ||||
| import ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||
|  | ||||
| type FilterSort struct { | ||||
| 	Field     string | ||||
| 	Direction ct.SortDirection | ||||
| } | ||||
|  | ||||
| type PaginateFilter interface { | ||||
| 	SQL(params PP) (filterClause string, joinClause string, joinTables []string) | ||||
| 	Sort() []FilterSort | ||||
| } | ||||
|  | ||||
| type genericPaginateFilter struct { | ||||
| 	sql  func(params PP) (filterClause string, joinClause string, joinTables []string) | ||||
| 	sort func() []FilterSort | ||||
| } | ||||
|  | ||||
| func (g genericPaginateFilter) SQL(params PP) (filterClause string, joinClause string, joinTables []string) { | ||||
| 	return g.sql(params) | ||||
| } | ||||
|  | ||||
| func (g genericPaginateFilter) Sort() []FilterSort { | ||||
| 	return g.sort() | ||||
| } | ||||
|  | ||||
| func NewPaginateFilter(sql func(params PP) (filterClause string, joinClause string, joinTables []string), sort []FilterSort) PaginateFilter { | ||||
| 	return genericPaginateFilter{ | ||||
| 		sql: func(params PP) (filterClause string, joinClause string, joinTables []string) { | ||||
| 			return sql(params) | ||||
| 		}, | ||||
| 		sort: func() []FilterSort { | ||||
| 			return sort | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func NewSimplePaginateFilter(filterClause string, filterParams PP, sort []FilterSort) PaginateFilter { | ||||
| 	return genericPaginateFilter{ | ||||
| 		sql: func(params PP) (string, string, []string) { | ||||
| 			params.AddAll(filterParams) | ||||
| 			return filterClause, "", nil | ||||
| 		}, | ||||
| 		sort: func() []FilterSort { | ||||
| 			return sort | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										15
									
								
								sq/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								sq/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestMain(m *testing.M) { | ||||
| 	if !exerr.Initialized() { | ||||
| 		exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse}) | ||||
| 	} | ||||
| 	os.Exit(m.Run()) | ||||
| } | ||||
| @@ -3,22 +3,11 @@ package sq | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" | ||||
| ) | ||||
|  | ||||
| type PaginateFilter interface { | ||||
| 	SQL(params PP) (filterClause string, joinClause string, joinTables []string) | ||||
| 	Sort() []FilterSort | ||||
| } | ||||
|  | ||||
| type FilterSort struct { | ||||
| 	Field     string | ||||
| 	Direction ct.SortDirection | ||||
| } | ||||
|  | ||||
| func Paginate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int) ([]TData, pag.Pagination, error) { | ||||
| 	prepParams := PP{} | ||||
|  | ||||
|   | ||||
| @@ -20,6 +20,12 @@ func (pp *PP) Add(v any) string { | ||||
| 	return id | ||||
| } | ||||
|  | ||||
| func (pp *PP) AddAll(other PP) { | ||||
| 	for id, v := range other { | ||||
| 		(*pp)[id] = v | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func PPID() string { | ||||
| 	return "p_" + langext.RandBase62(8) | ||||
| } | ||||
|   | ||||
							
								
								
									
										179
									
								
								sq/scanner.go
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								sq/scanner.go
									
									
									
									
									
								
							| @@ -6,8 +6,9 @@ import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| type StructScanMode string | ||||
| @@ -26,43 +27,11 @@ const ( | ||||
|  | ||||
| func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData) (sql.Result, error) { | ||||
|  | ||||
| 	rval := reflect.ValueOf(v) | ||||
| 	rtyp := rval.Type() | ||||
|  | ||||
| 	columns := make([]string, 0) | ||||
| 	params := make([]string, 0) | ||||
| 	pp := PP{} | ||||
|  | ||||
| 	for i := 0; i < rtyp.NumField(); i++ { | ||||
|  | ||||
| 		rsfield := rtyp.Field(i) | ||||
| 		rvfield := rval.Field(i) | ||||
|  | ||||
| 		if !rsfield.IsExported() { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		columnName := rsfield.Tag.Get("db") | ||||
| 		if columnName == "" || columnName == "-" { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		paramkey := fmt.Sprintf("_%s", columnName) | ||||
|  | ||||
| 		columns = append(columns, "\""+columnName+"\"") | ||||
| 		params = append(params, ":"+paramkey) | ||||
|  | ||||
| 		val, err := convertValueToDB(q, rvfield.Interface()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		pp[paramkey] = val | ||||
|  | ||||
| 	sqlstr, pp, err := BuildInsertStatement(q, tableName, v) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	sqlstr := fmt.Sprintf("INSERT"+" INTO \"%s\" (%s) VALUES (%s)", tableName, strings.Join(columns, ", "), strings.Join(params, ", ")) | ||||
|  | ||||
| 	sqlr, err := q.Exec(ctx, sqlstr, pp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -71,6 +40,127 @@ func InsertSingle[TData any](ctx context.Context, q Queryable, tableName string, | ||||
| 	return sqlr, nil | ||||
| } | ||||
|  | ||||
| func InsertAndQuerySingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string, mode StructScanMode, sec StructScanSafety) (TData, error) { | ||||
|  | ||||
| 	rval := reflect.ValueOf(v) | ||||
|  | ||||
| 	idRVal := fieldByTag(rval, "db", idColumn) | ||||
| 	if !idRVal.IsValid() || idRVal.IsZero() { | ||||
| 		return *new(TData), fmt.Errorf("failed to find idColumn '%s' in %T", idColumn, v) | ||||
| 	} | ||||
|  | ||||
| 	idValue, err := convertValueToDB(q, idRVal.Interface()) | ||||
| 	if err != nil { | ||||
| 		return *new(TData), err | ||||
| 	} | ||||
|  | ||||
| 	_, err = InsertSingle[TData](ctx, q, tableName, v) | ||||
| 	if err != nil { | ||||
| 		return *new(TData), err | ||||
| 	} | ||||
|  | ||||
| 	pp := PP{} | ||||
|  | ||||
| 	//goland:noinspection ALL | ||||
| 	sqlstr := fmt.Sprintf("SELECT * FROM %s WHERE %s = :%s", tableName, idColumn, pp.Add(idValue)) | ||||
|  | ||||
| 	return QuerySingle[TData](ctx, q, sqlstr, pp, mode, sec) | ||||
| } | ||||
|  | ||||
| func fieldByTag(rval reflect.Value, tagkey string, tagval string) reflect.Value { | ||||
| 	rtyp := rval.Type() | ||||
| 	for i := 0; i < rtyp.NumField(); i++ { | ||||
| 		rsfield := rtyp.Field(i) | ||||
|  | ||||
| 		if !rsfield.IsExported() { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if rsfield.Tag.Get(tagkey) == tagval { | ||||
| 			return rval.Field(i) | ||||
| 		} | ||||
| 	} | ||||
| 	panic(fmt.Sprintf("tag %s = '%s' not found in %s", tagkey, tagval, rtyp.Name())) | ||||
| } | ||||
|  | ||||
| func InsertMultiple[TData any](ctx context.Context, q Queryable, tableName string, vArr []TData, maxBatch int) ([]sql.Result, error) { | ||||
|  | ||||
| 	if len(vArr) == 0 { | ||||
| 		return make([]sql.Result, 0), nil | ||||
| 	} | ||||
|  | ||||
| 	chunks := langext.ArrChunk(vArr, maxBatch) | ||||
|  | ||||
| 	sqlstrArr := make([]string, 0) | ||||
| 	ppArr := make([]PP, 0) | ||||
|  | ||||
| 	for _, chunk := range chunks { | ||||
|  | ||||
| 		sqlstr, pp, err := BuildInsertMultipleStatement(q, tableName, chunk) | ||||
| 		if err != nil { | ||||
| 			return nil, exerr.Wrap(err, "").Build() | ||||
| 		} | ||||
|  | ||||
| 		sqlstrArr = append(sqlstrArr, sqlstr) | ||||
| 		ppArr = append(ppArr, pp) | ||||
| 	} | ||||
|  | ||||
| 	res := make([]sql.Result, 0, len(sqlstrArr)) | ||||
|  | ||||
| 	for i := 0; i < len(sqlstrArr); i++ { | ||||
| 		sqlr, err := q.Exec(ctx, sqlstrArr[i], ppArr[i]) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		res = append(res, sqlr) | ||||
| 	} | ||||
|  | ||||
| 	return res, nil | ||||
| } | ||||
|  | ||||
| func UpdateSingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string) (sql.Result, error) { | ||||
|  | ||||
| 	sqlstr, pp, err := BuildUpdateStatement(q, tableName, v, idColumn) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	sqlr, err := q.Exec(ctx, sqlstr, pp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return sqlr, nil | ||||
| } | ||||
|  | ||||
| func UpdateAndQuerySingle[TData any](ctx context.Context, q Queryable, tableName string, v TData, idColumn string, mode StructScanMode, sec StructScanSafety) (TData, error) { | ||||
|  | ||||
| 	rval := reflect.ValueOf(v) | ||||
|  | ||||
| 	idRVal := fieldByTag(rval, "db", idColumn) | ||||
| 	if !idRVal.IsValid() || idRVal.IsZero() { | ||||
| 		return *new(TData), fmt.Errorf("failed to find idColumn '%s' in %T", idColumn, v) | ||||
| 	} | ||||
|  | ||||
| 	idValue, err := convertValueToDB(q, idRVal.Interface()) | ||||
| 	if err != nil { | ||||
| 		return *new(TData), err | ||||
| 	} | ||||
|  | ||||
| 	_, err = UpdateSingle[TData](ctx, q, tableName, v, idColumn) | ||||
| 	if err != nil { | ||||
| 		return *new(TData), err | ||||
| 	} | ||||
|  | ||||
| 	pp := PP{} | ||||
|  | ||||
| 	//goland:noinspection ALL | ||||
| 	sqlstr := fmt.Sprintf("SELECT * FROM %s WHERE %s = :%s", tableName, idColumn, pp.Add(idValue)) | ||||
|  | ||||
| 	return QuerySingle[TData](ctx, q, sqlstr, pp, mode, sec) | ||||
| } | ||||
|  | ||||
| func QuerySingle[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) (TData, error) { | ||||
| 	rows, err := q.Query(ctx, sql, pp) | ||||
| 	if err != nil { | ||||
| @@ -85,6 +175,23 @@ func QuerySingle[TData any](ctx context.Context, q Queryable, sql string, pp PP, | ||||
| 	return data, nil | ||||
| } | ||||
|  | ||||
| func QuerySingleOpt[TData any](ctx context.Context, q Queryable, sqlstr string, pp PP, mode StructScanMode, sec StructScanSafety) (*TData, error) { | ||||
| 	rows, err := q.Query(ctx, sqlstr, pp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	data, err := ScanSingle[TData](ctx, q, rows, mode, sec, true) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &data, nil | ||||
| } | ||||
|  | ||||
| func QueryAll[TData any](ctx context.Context, q Queryable, sql string, pp PP, mode StructScanMode, sec StructScanSafety) ([]TData, error) { | ||||
| 	rows, err := q.Query(ctx, sql, pp) | ||||
| 	if err != nil { | ||||
|   | ||||
							
								
								
									
										237
									
								
								sq/scanner_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								sq/scanner_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"github.com/glebarez/go-sqlite" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestInsertSingle(t *testing.T) { | ||||
|  | ||||
| 	type request struct { | ||||
| 		ID        string  `json:"id"        db:"id"` | ||||
| 		Timestamp int     `json:"timestamp" db:"timestamp"` | ||||
| 		StrVal    string  `json:"strVal"    db:"str_val"` | ||||
| 		FloatVal  float64 `json:"floatVal"    db:"float_val"` | ||||
| 		Dummy     bool    `json:"dummyBool" db:"dummy_bool"` | ||||
| 		JsonVal   JsonObj `json:"jsonVal" db:"json_val"` | ||||
| 	} | ||||
|  | ||||
| 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||
| 		sqlite.RegisterAsSQLITE3() | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	dbdir := t.TempDir() | ||||
| 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||
|  | ||||
| 	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 NOT NULL,  | ||||
| 		    str_val TEXT NOT NULL,  | ||||
| 		    float_val REAL NOT NULL,  | ||||
| 		    dummy_bool INTEGER NOT NULL  CHECK(dummy_bool IN (0, 1)),  | ||||
| 		    json_val TEXT NOT NULL,  | ||||
| 		    PRIMARY KEY (id)  | ||||
| 		) STRICT | ||||
| `, PP{}) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	_, err = InsertSingle(ctx, db, "requests", request{ | ||||
| 		ID:        "9927", | ||||
| 		Timestamp: 12321, | ||||
| 		StrVal:    "hello world", | ||||
| 		Dummy:     true, | ||||
| 		FloatVal:  3.14159, | ||||
| 		JsonVal: JsonObj{ | ||||
| 			"firs":   1, | ||||
| 			"second": true, | ||||
| 		}, | ||||
| 	}) | ||||
| 	tst.AssertNoErr(t, err) | ||||
| } | ||||
|  | ||||
| func TestUpdateSingle(t *testing.T) { | ||||
|  | ||||
| 	type request struct { | ||||
| 		ID        string  `json:"id"        db:"id"` | ||||
| 		Timestamp int     `json:"timestamp" db:"timestamp"` | ||||
| 		StrVal    string  `json:"strVal"    db:"str_val"` | ||||
| 		FloatVal  float64 `json:"floatVal"    db:"float_val"` | ||||
| 		Dummy     bool    `json:"dummyBool" db:"dummy_bool"` | ||||
| 		JsonVal   JsonObj `json:"jsonVal" db:"json_val"` | ||||
| 	} | ||||
|  | ||||
| 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||
| 		sqlite.RegisterAsSQLITE3() | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	dbdir := t.TempDir() | ||||
| 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||
|  | ||||
| 	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 NOT NULL,  | ||||
| 		    str_val TEXT NOT NULL,  | ||||
| 		    float_val REAL NOT NULL,  | ||||
| 		    dummy_bool INTEGER NOT NULL  CHECK(dummy_bool IN (0, 1)),  | ||||
| 		    json_val TEXT NOT NULL,  | ||||
| 		    PRIMARY KEY (id)  | ||||
| 		) STRICT | ||||
| `, PP{}) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	_, err = InsertSingle(ctx, db, "requests", request{ | ||||
| 		ID:        "9927", | ||||
| 		Timestamp: 12321, | ||||
| 		StrVal:    "hello world", | ||||
| 		Dummy:     true, | ||||
| 		FloatVal:  3.14159, | ||||
| 		JsonVal: JsonObj{ | ||||
| 			"first":  1, | ||||
| 			"second": true, | ||||
| 		}, | ||||
| 	}) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	v, err := QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '9927' LIMIT 1", PP{}, SModeExtended, Safe) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	tst.AssertEqual(t, v.Timestamp, 12321) | ||||
| 	tst.AssertEqual(t, v.StrVal, "hello world") | ||||
| 	tst.AssertEqual(t, v.Dummy, true) | ||||
| 	tst.AssertEqual(t, v.FloatVal, 3.14159) | ||||
| 	tst.AssertStrRepEqual(t, v.JsonVal["first"], 1) | ||||
| 	tst.AssertStrRepEqual(t, v.JsonVal["second"], true) | ||||
|  | ||||
| 	_, err = UpdateSingle(ctx, db, "requests", request{ | ||||
| 		ID:        "9927", | ||||
| 		Timestamp: 9999, | ||||
| 		StrVal:    "9999 hello world", | ||||
| 		Dummy:     false, | ||||
| 		FloatVal:  123.222, | ||||
| 		JsonVal: JsonObj{ | ||||
| 			"first":  2, | ||||
| 			"second": false, | ||||
| 		}, | ||||
| 	}, "id") | ||||
|  | ||||
| 	v, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '9927' LIMIT 1", PP{}, SModeExtended, Safe) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	tst.AssertEqual(t, v.Timestamp, 9999) | ||||
| 	tst.AssertEqual(t, v.StrVal, "9999 hello world") | ||||
| 	tst.AssertEqual(t, v.Dummy, false) | ||||
| 	tst.AssertEqual(t, v.FloatVal, 123.222) | ||||
| 	tst.AssertStrRepEqual(t, v.JsonVal["first"], 2) | ||||
| 	tst.AssertStrRepEqual(t, v.JsonVal["second"], false) | ||||
| } | ||||
|  | ||||
| func TestInsertMultiple(t *testing.T) { | ||||
|  | ||||
| 	type request struct { | ||||
| 		ID        string  `json:"id"        db:"id"` | ||||
| 		Timestamp int     `json:"timestamp" db:"timestamp"` | ||||
| 		StrVal    string  `json:"strVal"    db:"str_val"` | ||||
| 		FloatVal  float64 `json:"floatVal"    db:"float_val"` | ||||
| 		Dummy     bool    `json:"dummyBool" db:"dummy_bool"` | ||||
| 		JsonVal   JsonObj `json:"jsonVal" db:"json_val"` | ||||
| 	} | ||||
|  | ||||
| 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||
| 		sqlite.RegisterAsSQLITE3() | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	dbdir := t.TempDir() | ||||
| 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||
|  | ||||
| 	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 NOT NULL,  | ||||
| 		    str_val TEXT NOT NULL,  | ||||
| 		    float_val REAL NOT NULL,  | ||||
| 		    dummy_bool INTEGER NOT NULL  CHECK(dummy_bool IN (0, 1)),  | ||||
| 		    json_val TEXT NOT NULL,  | ||||
| 		    PRIMARY KEY (id)  | ||||
| 		) STRICT | ||||
| `, PP{}) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	_, err = InsertMultiple(ctx, db, "requests", []request{ | ||||
| 		{ | ||||
| 			ID:        "1", | ||||
| 			Timestamp: 1000, | ||||
| 			StrVal:    "one", | ||||
| 			Dummy:     true, | ||||
| 			FloatVal:  0.1, | ||||
| 			JsonVal: JsonObj{ | ||||
| 				"arr": []int{0}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:        "2", | ||||
| 			Timestamp: 2000, | ||||
| 			StrVal:    "two", | ||||
| 			Dummy:     true, | ||||
| 			FloatVal:  0.2, | ||||
| 			JsonVal: JsonObj{ | ||||
| 				"arr": []int{0, 0}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:        "3", | ||||
| 			Timestamp: 3000, | ||||
| 			StrVal:    "three", | ||||
| 			Dummy:     true, | ||||
| 			FloatVal:  0.3, | ||||
| 			JsonVal: JsonObj{ | ||||
| 				"arr": []int{0, 0, 0}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, -1) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '1' LIMIT 1", PP{}, SModeExtended, Safe) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '2' LIMIT 1", PP{}, SModeExtended, Safe) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	_, err = QuerySingle[request](ctx, db, "SELECT * FROM requests WHERE id = '3' LIMIT 1", PP{}, SModeExtended, Safe) | ||||
| 	tst.AssertNoErr(t, err) | ||||
| } | ||||
| @@ -2,6 +2,7 @@ package tst | ||||
|  | ||||
| import ( | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"runtime/debug" | ||||
| 	"testing" | ||||
| @@ -125,3 +126,17 @@ func AssertNoErr(t *testing.T, anerr error) { | ||||
| 		t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack())) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func AssertStrRepEqual(t *testing.T, actual any, expected any) { | ||||
| 	t.Helper() | ||||
| 	if fmt.Sprintf("%v", actual) != fmt.Sprintf("%v", expected) { | ||||
| 		t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func AssertStrRepNotEqual(t *testing.T, actual any, expected any) { | ||||
| 	t.Helper() | ||||
| 	if fmt.Sprintf("%v", actual) == fmt.Sprintf("%v", expected) { | ||||
| 		t.Errorf("values do not differ: Actual: '%v', Expected: '%v'", actual, expected) | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user