Compare commits
	
		
			46 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3956675e04 | |||
| 10c3780b52 | |||
| 8edc067a3b | |||
| 1007f2c834 | |||
| c25da03217 | |||
| 4b55dbaacf | |||
| c399fa42ae | |||
| 9e586f7706 | |||
| 3cc8dccc63 | |||
| 7fedfbca81 | |||
| 3c439ba428 | |||
| ad24f6db44 | |||
| 1869ff3d75 | |||
| 30ce8c4b60 | |||
| 885bb53244 | |||
| 1c7dc1820a | |||
| 7e16e799e4 | |||
| 890e16241d | |||
| b9d0348735 | |||
| b9e9575b9b | |||
| 295a098eb4 | |||
| b69a082bb1 | |||
| a4a8c83d17 | |||
| e952176bb0 | |||
| d99adb203b | |||
| f1f91f4cfa | |||
| 2afb265ea4 | |||
| be24f7a190 | |||
| aae8a706e9 | |||
| 7d64f18f54 | |||
| d08b2e565a | |||
| d29e84894d | |||
| 617298c366 | |||
| 668f308565 | |||
| 240a8ed7aa | |||
| 70de8e8d04 | |||
| d38fa60fbc | |||
| 5fba7e0e2f | |||
| 8757643399 | |||
| 42bd4cf58d | |||
| 413178e2d3 | |||
| 9264a2e99b | |||
| 2a0471fb3d | |||
| 1497c013f9 | |||
| ef78b7467b | |||
| 0eda32b725 | 
| @@ -40,4 +40,6 @@ Potentially needs `export GOPRIVATE="gogs.mikescher.com"` | ||||
| |             |            |                                                                                                               | | ||||
| | rext        | Mike       | Regex Wrapper, wraps regexp with a better interface                                                           | | ||||
| | wmo         | Mike       | Mongo Wrapper, wraps mongodb with a better interface                                                          | | ||||
| |             |            |                                                                                                               | | ||||
| | scn         | Mike       | SimpleCloudNotifier                                                                                           | | ||||
| |             |            |                                                                                                               | | ||||
							
								
								
									
										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) | ||||
| } | ||||
							
								
								
									
										113
									
								
								dataext/syncMap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								dataext/syncMap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| package dataext | ||||
|  | ||||
| import "sync" | ||||
|  | ||||
| type SyncMap[TKey comparable, TData any] struct { | ||||
| 	data map[TKey]TData | ||||
| 	lock sync.Mutex | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) Set(key TKey, data TData) { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	s.data[key] = data | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) SetIfNotContains(key TKey, data TData) bool { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	if _, existsInPreState := s.data[key]; existsInPreState { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	s.data[key] = data | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) Get(key TKey) (TData, bool) { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	if v, ok := s.data[key]; ok { | ||||
| 		return v, true | ||||
| 	} else { | ||||
| 		return *new(TData), false | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) Delete(key TKey) bool { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	_, ok := s.data[key] | ||||
|  | ||||
| 	delete(s.data, key) | ||||
|  | ||||
| 	return ok | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) Contains(key TKey) bool { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	_, ok := s.data[key] | ||||
|  | ||||
| 	return ok | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) GetAllKeys() []TKey { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	r := make([]TKey, 0, len(s.data)) | ||||
|  | ||||
| 	for k := range s.data { | ||||
| 		r = append(r, k) | ||||
| 	} | ||||
|  | ||||
| 	return r | ||||
| } | ||||
|  | ||||
| func (s *SyncMap[TKey, TData]) GetAllValues() []TData { | ||||
| 	s.lock.Lock() | ||||
| 	defer s.lock.Unlock() | ||||
|  | ||||
| 	if s.data == nil { | ||||
| 		s.data = make(map[TKey]TData) | ||||
| 	} | ||||
|  | ||||
| 	r := make([]TData, 0, len(s.data)) | ||||
|  | ||||
| 	for _, v := range s.data { | ||||
| 		r = append(r, v) | ||||
| 	} | ||||
|  | ||||
| 	return r | ||||
| } | ||||
| @@ -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,13 +415,17 @@ 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 { | ||||
| 		b.errorData.ShortLog(stackSkipLogger.Error()) | ||||
| 	} | ||||
|  | ||||
| 	b.CallListener(MethodBuild) | ||||
| 	b.errorData.CallListener(MethodBuild) | ||||
|  | ||||
| 	return b.errorData | ||||
| } | ||||
| @@ -442,7 +447,7 @@ func (b *Builder) Output(ctx context.Context, g *gin.Context) { | ||||
| 		b.errorData.Log(stackSkipLogger.Warn()) | ||||
| 	} | ||||
|  | ||||
| 	b.CallListener(MethodOutput) | ||||
| 	b.errorData.CallListener(MethodOutput) | ||||
| } | ||||
|  | ||||
| // Print prints the error | ||||
| @@ -454,7 +459,7 @@ func (b *Builder) Print() { | ||||
| 		b.errorData.ShortLog(stackSkipLogger.Warn()) | ||||
| 	} | ||||
|  | ||||
| 	b.CallListener(MethodPrint) | ||||
| 	b.errorData.CallListener(MethodPrint) | ||||
| } | ||||
|  | ||||
| func (b *Builder) Format(level LogPrintLevel) string { | ||||
| @@ -467,7 +472,7 @@ func (b *Builder) Fatal() { | ||||
| 	b.errorData.Severity = SevFatal | ||||
| 	b.errorData.Log(stackSkipLogger.WithLevel(zerolog.FatalLevel)) | ||||
|  | ||||
| 	b.CallListener(MethodFatal) | ||||
| 	b.errorData.CallListener(MethodFatal) | ||||
|  | ||||
| 	os.Exit(1) | ||||
| } | ||||
|   | ||||
| @@ -48,6 +48,10 @@ var ( | ||||
| 	TypeMongoReflection   = NewType("MONGO_REFLECTION", langext.Ptr(500)) | ||||
| 	TypeMongoInvalidOpt   = NewType("MONGO_INVALIDOPT", langext.Ptr(500)) | ||||
|  | ||||
| 	TypeSQLQuery  = NewType("SQL_QUERY", langext.Ptr(500)) | ||||
| 	TypeSQLBuild  = NewType("SQL_BUILD", langext.Ptr(500)) | ||||
| 	TypeSQLDecode = NewType("SQL_DECODE", langext.Ptr(500)) | ||||
|  | ||||
| 	TypeWrap = NewType("Wrap", nil) | ||||
|  | ||||
| 	TypeBindFailURI      = NewType("BINDFAIL_URI", langext.Ptr(400)) | ||||
| @@ -65,18 +69,21 @@ var ( | ||||
| 	TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401)) | ||||
| 	TypeAuthFailed   = NewType("AUTH_FAILED", langext.Ptr(401)) | ||||
|  | ||||
| 	// other values come the used package | ||||
| 	// other values come from the downstream application that uses goext | ||||
| ) | ||||
|  | ||||
| var registeredTypes = dataext.SyncSet[string]{} | ||||
| var registeredTypes = dataext.SyncMap[string, ErrorType]{} | ||||
|  | ||||
| func NewType(key string, defStatusCode *int) ErrorType { | ||||
| 	insertOkay := registeredTypes.Add(key) | ||||
| 	if !insertOkay { | ||||
| 		panic("Cannot register same ErrType ('" + key + "') more than once") | ||||
| 	} | ||||
| 	et := ErrorType{key, defStatusCode} | ||||
|  | ||||
| 	return ErrorType{key, defStatusCode} | ||||
| 	registeredTypes.Set(key, et) | ||||
|  | ||||
| 	return et | ||||
| } | ||||
|  | ||||
| func ListRegisteredTypes() []ErrorType { | ||||
| 	return registeredTypes.GetAllValues() | ||||
| } | ||||
|  | ||||
| type LogPrintLevel string | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -258,6 +258,73 @@ func (ee *ExErr) Depth() int { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetMeta returns the meta value with the specified key | ||||
| // this method recurses through all wrapped errors and returns the first matching meta value | ||||
| func (ee *ExErr) GetMeta(key string) (any, bool) { | ||||
| 	for curr := ee; curr != nil; curr = curr.OriginalError { | ||||
| 		if v, ok := curr.Meta[key]; ok { | ||||
| 			return v.Value, true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil, false | ||||
| } | ||||
|  | ||||
| // GetMetaString functions the same as GetMeta, but returns false if the type does not match | ||||
| func (ee *ExErr) GetMetaString(key string) (string, bool) { | ||||
| 	if v1, ok := ee.GetMeta(key); ok { | ||||
| 		if v2, ok := v1.(string); ok { | ||||
| 			return v2, true | ||||
| 		} | ||||
| 	} | ||||
| 	return "", false | ||||
| } | ||||
|  | ||||
| func (ee *ExErr) GetMetaBool(key string) (bool, bool) { | ||||
| 	if v1, ok := ee.GetMeta(key); ok { | ||||
| 		if v2, ok := v1.(bool); ok { | ||||
| 			return v2, true | ||||
| 		} | ||||
| 	} | ||||
| 	return false, false | ||||
| } | ||||
|  | ||||
| func (ee *ExErr) GetMetaInt(key string) (int, bool) { | ||||
| 	if v1, ok := ee.GetMeta(key); ok { | ||||
| 		if v2, ok := v1.(int); ok { | ||||
| 			return v2, true | ||||
| 		} | ||||
| 	} | ||||
| 	return 0, false | ||||
| } | ||||
|  | ||||
| func (ee *ExErr) GetMetaFloat32(key string) (float32, bool) { | ||||
| 	if v1, ok := ee.GetMeta(key); ok { | ||||
| 		if v2, ok := v1.(float32); ok { | ||||
| 			return v2, true | ||||
| 		} | ||||
| 	} | ||||
| 	return 0, false | ||||
| } | ||||
|  | ||||
| func (ee *ExErr) GetMetaFloat64(key string) (float64, bool) { | ||||
| 	if v1, ok := ee.GetMeta(key); ok { | ||||
| 		if v2, ok := v1.(float64); ok { | ||||
| 			return v2, true | ||||
| 		} | ||||
| 	} | ||||
| 	return 0, false | ||||
| } | ||||
|  | ||||
| func (ee *ExErr) GetMetaTime(key string) (time.Time, bool) { | ||||
| 	if v1, ok := ee.GetMeta(key); ok { | ||||
| 		if v2, ok := v1.(time.Time); ok { | ||||
| 			return v2, true | ||||
| 		} | ||||
| 	} | ||||
| 	return time.Time{}, false | ||||
| } | ||||
|  | ||||
| // contains test if the supplied error is contained in this error (anywhere in the chain) | ||||
| func (ee *ExErr) contains(original *ExErr) (*ExErr, bool) { | ||||
| 	if original == nil { | ||||
|   | ||||
| @@ -25,13 +25,11 @@ func RegisterListener(l Listener) { | ||||
| 	listener = append(listener, l) | ||||
| } | ||||
|  | ||||
| func (b *Builder) CallListener(m Method) { | ||||
| 	valErr := b.errorData | ||||
|  | ||||
| func (ee *ExErr) CallListener(m Method) { | ||||
| 	listenerLock.Lock() | ||||
| 	defer listenerLock.Unlock() | ||||
|  | ||||
| 	for _, v := range listener { | ||||
| 		v(m, valErr) | ||||
| 		v(m, ee) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rext" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| @@ -16,7 +17,7 @@ import ( | ||||
|  | ||||
| type GinWrapper struct { | ||||
| 	engine          *gin.Engine | ||||
| 	SuppressGinLogs bool | ||||
| 	suppressGinLogs bool | ||||
|  | ||||
| 	allowCors             bool | ||||
| 	ginDebug              bool | ||||
| @@ -50,7 +51,7 @@ func NewEngine(opt Options) *GinWrapper { | ||||
|  | ||||
| 	wrapper := &GinWrapper{ | ||||
| 		engine:                engine, | ||||
| 		SuppressGinLogs:       false, | ||||
| 		suppressGinLogs:       false, | ||||
| 		allowCors:             langext.Coalesce(opt.AllowCors, false), | ||||
| 		ginDebug:              langext.Coalesce(opt.GinDebug, true), | ||||
| 		bufferBody:            langext.Coalesce(opt.BufferBody, false), | ||||
| @@ -74,7 +75,7 @@ func NewEngine(opt Options) *GinWrapper { | ||||
|  | ||||
| 		ginlogger := gin.Logger() | ||||
| 		engine.Use(func(context *gin.Context) { | ||||
| 			if !wrapper.SuppressGinLogs { | ||||
| 			if !wrapper.suppressGinLogs { | ||||
| 				ginlogger(context) | ||||
| 			} | ||||
| 		}) | ||||
| @@ -185,3 +186,10 @@ func (w *GinWrapper) cleanMiddlewareName(fname string) string { | ||||
|  | ||||
| 	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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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,10 +400,19 @@ 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) | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| @@ -335,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 | ||||
| } | ||||
| @@ -359,6 +449,10 @@ func (j jsonAPIErrResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) Unwrap() error { | ||||
| 	return j.err | ||||
| } | ||||
|  | ||||
| func Status(sc int) HTTPResponse { | ||||
| 	return &emptyHTTPResponse{statusCode: sc} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										35
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								go.mod
									
									
									
									
									
								
							| @@ -4,47 +4,54 @@ go 1.21 | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-gonic/gin v1.9.1 | ||||
| 	github.com/glebarez/go-sqlite v1.22.0 // only needed for tests -.- | ||||
| 	github.com/jmoiron/sqlx v1.3.5 | ||||
| 	github.com/mattn/go-sqlite3 v1.14.19 // only needed for tests -.- | ||||
| 	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.17.0 | ||||
| 	golang.org/x/sys v0.15.0 | ||||
| 	golang.org/x/term v0.15.0 | ||||
| 	github.com/rs/zerolog v1.32.0 | ||||
| 	go.mongodb.org/mongo-driver v1.14.0 | ||||
| 	golang.org/x/crypto v0.20.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.2 // 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 | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect | ||||
| 	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.7 // 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 | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/montanaflynn/stats v0.7.1 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||
| 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect | ||||
| 	github.com/xdg-go/scram v1.1.2 // indirect | ||||
| 	github.com/xdg-go/stringprep v1.0.4 // indirect | ||||
| 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | ||||
| 	golang.org/x/arch v0.6.0 // indirect | ||||
| 	golang.org/x/net v0.19.0 // indirect | ||||
| 	golang.org/x/sync v0.5.0 // indirect | ||||
| 	golang.org/x/arch v0.7.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 | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| 	modernc.org/libc v1.37.6 // indirect | ||||
| 	modernc.org/mathutil v1.6.0 // indirect | ||||
| 	modernc.org/memory v1.7.2 // indirect | ||||
| 	modernc.org/sqlite v1.28.0 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										75
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										75
									
								
								go.sum
									
									
									
									
									
								
							| @@ -2,6 +2,12 @@ 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/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4= | ||||
| github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= | ||||
| github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A= | ||||
| github.com/bytedance/sonic v1.11.2/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= | ||||
| @@ -13,12 +19,16 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= | ||||
| github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= | ||||
| github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= | ||||
| github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= | ||||
| github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= | ||||
| github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= | ||||
| github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= | ||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||
| github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| @@ -27,11 +37,16 @@ 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= | ||||
| 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/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.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= | ||||
| github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||
| @@ -39,6 +54,10 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ | ||||
| github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= | ||||
| github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= | ||||
| github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= | ||||
| github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= | ||||
| github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= | ||||
| github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||
| @@ -46,12 +65,20 @@ 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/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= | ||||
| github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | ||||
| 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= | ||||
| @@ -60,7 +87,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ | ||||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= | ||||
| github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= | ||||
| github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= | ||||
| @@ -77,10 +103,14 @@ github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdU | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| 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= | ||||
| @@ -108,15 +138,23 @@ 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.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= | ||||
| golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | ||||
| golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= | ||||
| golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||||
| 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/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= | ||||
| golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= | ||||
| 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= | ||||
| @@ -125,10 +163,14 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| 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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= | ||||
| golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | ||||
| golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| @@ -140,12 +182,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc | ||||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| 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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | ||||
| golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| 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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= | ||||
| golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= | ||||
| 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= | ||||
| @@ -158,8 +204,9 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= | ||||
| golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= | ||||
| google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= | ||||
| google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| @@ -167,5 +214,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= | ||||
| modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= | ||||
| modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= | ||||
| modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= | ||||
| modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= | ||||
| modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= | ||||
| modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= | ||||
| modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= | ||||
| nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | ||||
| rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| package goext | ||||
|  | ||||
| const GoextVersion = "0.0.351" | ||||
| const GoextVersion = "0.0.396" | ||||
|  | ||||
| const GoextVersionTimestamp = "2023-12-29T19:25:36+0100" | ||||
| const GoextVersionTimestamp = "2024-02-28T14:28:48+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 | ||||
| } | ||||
|   | ||||
| @@ -71,3 +71,19 @@ func ForceMap[K comparable, V any](v map[K]V) map[K]V { | ||||
| 		return v | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func MapMerge[K comparable, V any](base map[K]V, arr ...map[K]V) map[K]V { | ||||
| 	res := make(map[K]V, len(base)*(1+len(arr))) | ||||
|  | ||||
| 	for k, v := range base { | ||||
| 		res[k] = v | ||||
| 	} | ||||
|  | ||||
| 	for _, m := range arr { | ||||
| 		for k, v := range m { | ||||
| 			res[k] = v | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 	} | ||||
| } | ||||
| @@ -5,7 +5,7 @@ import ( | ||||
| 	"go.mongodb.org/mongo-driver/mongo" | ||||
| ) | ||||
|  | ||||
| type Filter interface { | ||||
| type MongoFilter interface { | ||||
| 	FilterQuery() mongo.Pipeline | ||||
| 	Sort() bson.D | ||||
| } | ||||
| @@ -23,6 +23,6 @@ func (d dynamicFilter) Sort() bson.D { | ||||
| 	return d.sort | ||||
| } | ||||
|  | ||||
| func CreateFilter(pipeline mongo.Pipeline, sort bson.D) Filter { | ||||
| func CreateFilter(pipeline mongo.Pipeline, sort bson.D) MongoFilter { | ||||
| 	return dynamicFilter{pipeline: pipeline, sort: sort} | ||||
| } | ||||
|   | ||||
| @@ -12,5 +12,8 @@ func CalcPaginationTotalPages(totalItems int, limit int) int { | ||||
| 	if totalItems == 0 { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	if limit == 0 { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	return 1 + (totalItems-1)/limit | ||||
| } | ||||
|   | ||||
							
								
								
									
										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() | ||||
| } | ||||
							
								
								
									
										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() { | ||||
| } | ||||
							
								
								
									
										148
									
								
								rfctime/time.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								rfctime/time.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| package rfctime | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Time struct { | ||||
| 	Hour       int | ||||
| 	Minute     int | ||||
| 	Second     int | ||||
| 	NanoSecond int | ||||
| } | ||||
|  | ||||
| func (t Time) Serialize() string { | ||||
| 	return fmt.Sprintf("%04d:%02d:%02d.%09d", t.Hour, t.Minute, t.Second, t.NanoSecond) | ||||
| } | ||||
|  | ||||
| func (t Time) SerializeShort() string { | ||||
| 	if t.NanoSecond == 0 && t.Second == 0 { | ||||
| 		return fmt.Sprintf("%02d:%02d", t.Hour, t.Minute) | ||||
| 	} else if t.NanoSecond == 0 { | ||||
| 		return fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second) | ||||
| 	} else { | ||||
| 		return fmt.Sprintf("%02d:%02d:%02d.%09d", t.Hour, t.Minute, t.Second, t.NanoSecond) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *Time) Deserialize(v string) error { | ||||
|  | ||||
| 	var h, m, s, ns string | ||||
|  | ||||
| 	split1 := strings.Split(v, ".") | ||||
|  | ||||
| 	if len(split1) == 2 { | ||||
|  | ||||
| 		split2 := strings.Split(split1[0], ":") | ||||
| 		if len(split2) == 3 { | ||||
|  | ||||
| 			h = split2[0] | ||||
| 			m = split2[1] | ||||
| 			s = split2[2] | ||||
| 			ns = split1[1] | ||||
|  | ||||
| 		} else { | ||||
| 			return fmt.Errorf("invalid time format: %s", v) | ||||
| 		} | ||||
|  | ||||
| 	} else if len(split1) == 1 { | ||||
|  | ||||
| 		split2 := strings.Split(split1[0], ":") | ||||
| 		if len(split2) == 2 { | ||||
|  | ||||
| 			h = split2[0] | ||||
| 			m = split2[1] | ||||
| 			s = "0" | ||||
| 			ns = "0" | ||||
|  | ||||
| 		} else if len(split2) == 3 { | ||||
|  | ||||
| 			h = split2[0] | ||||
| 			m = split2[1] | ||||
| 			s = split2[2] | ||||
| 			ns = "0" | ||||
|  | ||||
| 		} else { | ||||
| 			return fmt.Errorf("invalid time format: %s", v) | ||||
| 		} | ||||
|  | ||||
| 	} else { | ||||
| 		return fmt.Errorf("invalid time format: %s", v) | ||||
| 	} | ||||
|  | ||||
| 	ns = langext.StrPadRight(ns, "0", 9) | ||||
|  | ||||
| 	hh, err := strconv.ParseInt(h, 10, 32) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("invalid time format: %s", v) | ||||
| 	} | ||||
|  | ||||
| 	mm, err := strconv.ParseInt(m, 10, 32) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("invalid time format: %s", v) | ||||
| 	} | ||||
|  | ||||
| 	ss, err := strconv.ParseInt(s, 10, 32) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("invalid time format: %s", v) | ||||
| 	} | ||||
|  | ||||
| 	nss, err := strconv.ParseInt(ns, 10, 32) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("invalid time format: %s", v) | ||||
| 	} | ||||
|  | ||||
| 	t.Hour = int(hh) | ||||
| 	t.Minute = int(mm) | ||||
| 	t.Second = int(ss) | ||||
| 	t.NanoSecond = int(nss) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t Time) FormatStr() string { | ||||
| 	return "15:04:05.999999999" | ||||
| } | ||||
|  | ||||
| func (t Time) GoString() string { | ||||
| 	return fmt.Sprintf("rfctime.NewTime(%d, %d, %d, %d)", t.Hour, t.Minute, t.Second, t.NanoSecond) | ||||
| } | ||||
|  | ||||
| func (t Time) String() string { | ||||
| 	return fmt.Sprintf("%04d:%02d:%02d.%09d", t.Hour, t.Minute, t.Second, t.NanoSecond) | ||||
| } | ||||
|  | ||||
| func NewTime(h int, m int, s int, ns int) Time { | ||||
| 	return Time{ | ||||
| 		Hour:       h, | ||||
| 		Minute:     m, | ||||
| 		Second:     s, | ||||
| 		NanoSecond: ns, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func NewTimeFromTS(t time.Time) Time { | ||||
| 	return Time{ | ||||
| 		Hour:       t.Hour(), | ||||
| 		Minute:     t.Minute(), | ||||
| 		Second:     t.Second(), | ||||
| 		NanoSecond: t.Nanosecond(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func NowTime(loc *time.Location) Time { | ||||
| 	now := time.Now().In(loc) | ||||
| 	return NewTime(now.Hour(), now.Minute(), now.Second(), now.Nanosecond()) | ||||
| } | ||||
|  | ||||
| func NowTimeLoc() Time { | ||||
| 	return NowTime(time.UTC) | ||||
| } | ||||
|  | ||||
| func NowTimeUTC() Time { | ||||
| 	return NowTime(time.Local) | ||||
| } | ||||
							
								
								
									
										13
									
								
								scn/scn.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								scn/scn.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| package scn | ||||
|  | ||||
| type Connection struct { | ||||
| 	uid   string | ||||
| 	token string | ||||
| } | ||||
|  | ||||
| func New(userid string, token string) *Connection { | ||||
| 	return &Connection{ | ||||
| 		uid:   userid, | ||||
| 		token: token, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										171
									
								
								scn/send.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								scn/send.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| package scn | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	json "gogs.mikescher.com/BlackForestBytes/goext/gojson" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrAuthFailed        = exerr.NewType("GOEXT_SCN_AUTHFAILED", nil) | ||||
| 	ErrQuota             = exerr.NewType("GOEXT_SCN_QUOTAEXCEEDED", nil) | ||||
| 	ErrBadRequest        = exerr.NewType("GOEXT_SCN_BADREQUEST", nil) | ||||
| 	ErrInternalServerErr = exerr.NewType("GOEXT_SCN_INTERNALSERVER", nil) | ||||
| 	ErrOther             = exerr.NewType("GOEXT_SCN_OTHER", nil) | ||||
| ) | ||||
|  | ||||
| type MessageResponse struct { | ||||
| 	ErrHighlight int    `json:"errhighlight"` | ||||
| 	Error        int    `json:"error"` | ||||
| 	IsPro        bool   `json:"is_pro"` | ||||
| 	Message      string `json:"message"` | ||||
| 	Messagecount int    `json:"messagecount"` | ||||
| 	Quota        int    `json:"quota"` | ||||
| 	QuotaMax     int    `json:"quota_max"` | ||||
| 	SCNMessageID string `json:"scn_msg_id"` | ||||
| 	Success      bool   `json:"success"` | ||||
| 	SuppressSend bool   `json:"suppress_send"` | ||||
| } | ||||
|  | ||||
| type MessageErrResponse struct { | ||||
| 	Errhighlight int    `json:"errhighlight"` | ||||
| 	Error        int    `json:"error"` | ||||
| 	Message      string `json:"message"` | ||||
| 	Success      bool   `json:"success"` | ||||
| } | ||||
|  | ||||
| type MessageBuilder struct { | ||||
| 	conn       *Connection | ||||
| 	title      string | ||||
| 	content    *string | ||||
| 	channel    *string | ||||
| 	time       *time.Time | ||||
| 	sendername *string | ||||
| 	priority   *int | ||||
| } | ||||
|  | ||||
| func (c *Connection) Message(title string) *MessageBuilder { | ||||
| 	return &MessageBuilder{conn: c, title: title} | ||||
| } | ||||
|  | ||||
| func (c *MessageBuilder) Channel(channel string) *MessageBuilder { | ||||
| 	c.channel = &channel | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (c *MessageBuilder) Content(content string) *MessageBuilder { | ||||
| 	c.content = &content | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (c *MessageBuilder) Time(t time.Time) *MessageBuilder { | ||||
| 	c.time = &t | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (c *MessageBuilder) SenderName(sn string) *MessageBuilder { | ||||
| 	c.sendername = &sn | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (c *MessageBuilder) Priority(p int) *MessageBuilder { | ||||
| 	c.priority = &p | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (c *MessageBuilder) Send(ctx context.Context) (MessageResponse, error) { | ||||
| 	client := http.Client{Timeout: 5 * time.Second} | ||||
|  | ||||
| 	body := langext.H{} | ||||
|  | ||||
| 	body["user_id"] = c.conn.uid | ||||
| 	body["key"] = c.conn.token | ||||
|  | ||||
| 	if c.channel != nil { | ||||
| 		body["channel"] = *c.channel | ||||
| 	} | ||||
|  | ||||
| 	body["title"] = c.title | ||||
|  | ||||
| 	if c.content != nil { | ||||
| 		body["content"] = *c.content | ||||
| 	} | ||||
|  | ||||
| 	if c.sendername != nil { | ||||
| 		body["content"] = *c.sendername | ||||
| 	} | ||||
|  | ||||
| 	if c.time != nil { | ||||
| 		body["timestamp"] = c.time.Unix() | ||||
| 	} | ||||
|  | ||||
| 	if c.priority != nil { | ||||
| 		body["priority"] = *c.priority | ||||
| 	} | ||||
|  | ||||
| 	body["msg_id"] = langext.MustHexUUID() | ||||
|  | ||||
| 	rawbody, err := json.Marshal(body) | ||||
| 	if err != nil { | ||||
| 		return MessageResponse{}, err | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequestWithContext(ctx, "POST", "https://simplecloudnotifier.de/", bytes.NewBuffer(rawbody)) | ||||
| 	if err != nil { | ||||
| 		return MessageResponse{}, err | ||||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
|  | ||||
| 	resp, err := client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return MessageResponse{}, err | ||||
| 	} | ||||
|  | ||||
| 	defer func() { _ = resp.Body.Close() }() | ||||
|  | ||||
| 	if resp.StatusCode >= 200 && resp.StatusCode < 300 { | ||||
|  | ||||
| 		raw, err := io.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			return MessageResponse{}, err | ||||
| 		} | ||||
|  | ||||
| 		var mr MessageResponse | ||||
| 		err = json.Unmarshal(raw, &mr) | ||||
| 		if err != nil { | ||||
| 			return MessageResponse{}, err | ||||
| 		} | ||||
|  | ||||
| 		return mr, nil | ||||
| 	} else { | ||||
| 		errMessage := resp.Status | ||||
|  | ||||
| 		if raw, err := io.ReadAll(resp.Body); err == nil { | ||||
| 			var mr MessageErrResponse | ||||
| 			if err = json.Unmarshal(raw, &mr); err == nil { | ||||
| 				errMessage = mr.Message | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if resp.StatusCode == 400 { | ||||
| 			return MessageResponse{}, exerr.New(ErrBadRequest, errMessage).Build() | ||||
| 		} | ||||
| 		if resp.StatusCode == 401 { | ||||
| 			return MessageResponse{}, exerr.New(ErrAuthFailed, errMessage).Build() | ||||
| 		} | ||||
| 		if resp.StatusCode == 403 { | ||||
| 			return MessageResponse{}, exerr.New(ErrQuota, errMessage).Build() | ||||
| 		} | ||||
| 		if resp.StatusCode == 500 { | ||||
| 			return MessageResponse{}, exerr.New(ErrInternalServerErr, errMessage).Build() | ||||
| 		} | ||||
|  | ||||
| 		return MessageResponse{}, exerr.New(ErrOther, errMessage).Build() | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										24
									
								
								scn/send_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								scn/send_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| package scn | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestSendSCN(t *testing.T) { | ||||
| 	t.Skip() | ||||
| 	return | ||||
|  | ||||
| 	mr, err := New("TODO", "TODO"). | ||||
| 		Message("Hello Test"). | ||||
| 		Content("This is a test / goext"). | ||||
| 		Send(context.Background()) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("%+v\n", mr) | ||||
|  | ||||
| } | ||||
							
								
								
									
										199
									
								
								sq/builder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								sq/builder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func BuildUpdateStatement[TData any](q Queryable, tableName string, obj TData, idColumn string) (string, PP, error) { | ||||
| 	rval := reflect.ValueOf(obj) | ||||
| 	rtyp := rval.Type() | ||||
|  | ||||
| 	params := PP{} | ||||
|  | ||||
| 	setClauses := make([]string, 0) | ||||
|  | ||||
| 	matchClause := "" | ||||
|  | ||||
| 	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 idColumn == columnName { | ||||
| 			idValue, err := convertValueToDB(q, rvfield.Interface()) | ||||
| 			if err != nil { | ||||
| 				return "", nil, err | ||||
| 			} | ||||
|  | ||||
| 			matchClause = fmt.Sprintf("(%s = :%s)", columnName, params.Add(idValue)) | ||||
|  | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if rsfield.Type.Kind() == reflect.Ptr && rvfield.IsNil() { | ||||
|  | ||||
| 			setClauses = append(setClauses, fmt.Sprintf("%s = NULL", columnName)) | ||||
|  | ||||
| 		} else { | ||||
|  | ||||
| 			val, err := convertValueToDB(q, rvfield.Interface()) | ||||
| 			if err != nil { | ||||
| 				return "", nil, err | ||||
| 			} | ||||
|  | ||||
| 			setClauses = append(setClauses, fmt.Sprintf("%s = :%s", columnName, params.Add(val))) | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(setClauses) == 0 { | ||||
| 		return "", nil, exerr.New(exerr.TypeSQLBuild, "no updates clauses found in object").Build() | ||||
| 	} | ||||
|  | ||||
| 	if matchClause == "" { | ||||
| 		return "", nil, exerr.New(exerr.TypeSQLBuild, "id column not found in object").Build() | ||||
| 	} | ||||
|  | ||||
| 	//goland:noinspection SqlNoDataSourceInspection | ||||
| 	return fmt.Sprintf("UPDATE %s SET %s WHERE %s", tableName, strings.Join(setClauses, ", "), matchClause), params, nil | ||||
| } | ||||
|  | ||||
| func BuildInsertStatement[TData any](q Queryable, tableName string, obj TData) (string, PP, error) { | ||||
| 	rval := reflect.ValueOf(obj) | ||||
| 	rtyp := rval.Type() | ||||
|  | ||||
| 	params := PP{} | ||||
|  | ||||
| 	fields := make([]string, 0) | ||||
| 	values := 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() { | ||||
|  | ||||
| 			fields = append(fields, columnName) | ||||
| 			values = append(values, "NULL") | ||||
|  | ||||
| 		} else { | ||||
|  | ||||
| 			val, err := convertValueToDB(q, rvfield.Interface()) | ||||
| 			if err != nil { | ||||
| 				return "", nil, err | ||||
| 			} | ||||
|  | ||||
| 			fields = append(fields, columnName) | ||||
| 			values = append(values, ":"+params.Add(val)) | ||||
|  | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(fields) == 0 { | ||||
| 		return "", nil, exerr.New(exerr.TypeSQLBuild, "no fields found in object").Build() | ||||
| 	} | ||||
|  | ||||
| 	//goland:noinspection SqlNoDataSourceInspection | ||||
| 	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 | ||||
| } | ||||
							
								
								
									
										87
									
								
								sq/builder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								sq/builder_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| type dummyQueryable struct { | ||||
| } | ||||
|  | ||||
| func (d dummyQueryable) Exec(ctx context.Context, sql string, prep PP) (sql.Result, error) { | ||||
| 	return nil, errors.New("err") | ||||
| } | ||||
|  | ||||
| func (d dummyQueryable) Query(ctx context.Context, sql string, prep PP) (*sqlx.Rows, error) { | ||||
| 	return nil, errors.New("err") | ||||
| } | ||||
|  | ||||
| func (d dummyQueryable) ListConverter() []DBTypeConverter { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestCreateUpdateStatement(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, PRIMARY KEY (id) ) STRICT", PP{}) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	sqlStr, pp, err := BuildUpdateStatement(db, "requests", request{ | ||||
| 		ID:        "9927", | ||||
| 		Timestamp: 12321, | ||||
| 		StrVal:    "hello world", | ||||
| 		Dummy:     true, | ||||
| 		FloatVal:  3.14159, | ||||
| 		JsonVal: JsonObj{ | ||||
| 			"firs":   1, | ||||
| 			"second": true, | ||||
| 		}, | ||||
| 	}, "id") | ||||
|  | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	fmt.Printf("\n\n") | ||||
|  | ||||
| 	fmt.Printf("######## PP:\n") | ||||
| 	for k, v := range pp { | ||||
| 		fmt.Printf("['%s']       =>       %+v\n", k, v) | ||||
| 	} | ||||
| 	fmt.Printf("\n\n") | ||||
|  | ||||
| 	fmt.Printf("######## SQL:\n%+v\n\n", sqlStr) | ||||
|  | ||||
| 	fmt.Printf("\n\n") | ||||
|  | ||||
| } | ||||
							
								
								
									
										125
									
								
								sq/converter.go
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								sq/converter.go
									
									
									
									
									
								
							| @@ -1,11 +1,16 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rfctime" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/timeext" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| @@ -16,9 +21,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 | ||||
| 	} | ||||
| @@ -52,26 +57,132 @@ var ConverterRFCUnixTimeToUnixSeconds = NewDBTypeConverter[rfctime.UnixTime, int | ||||
| 	return rfctime.NewUnix(time.Unix(v, 0)), nil | ||||
| }) | ||||
|  | ||||
| // ConverterRFC339TimeToString | ||||
| // Does not really use RFC339 - but sqlite does not understand timezones and the `T` delimiter | ||||
| var ConverterRFC339TimeToString = NewDBTypeConverter[rfctime.RFC3339Time, string](func(v rfctime.RFC3339Time) (string, error) { | ||||
| 	return v.Format(time.RFC3339), nil | ||||
| 	return v.Time().In(time.UTC).Format("2006-01-02 15:04:05"), nil | ||||
| }, func(v string) (rfctime.RFC3339Time, error) { | ||||
| 	t, err := time.Parse(time.RFC3339Nano, v) | ||||
| 	t, err := time.Parse("2006-01-02 15:04:05", v) | ||||
| 	if err != nil { | ||||
| 		return rfctime.RFC3339Time{}, err | ||||
| 	} | ||||
| 	return rfctime.NewRFC3339(t), nil | ||||
| }) | ||||
|  | ||||
| // ConverterRFC339NanoTimeToString | ||||
| // Does not really use RFC339 - but sqlite does not understand timezones and the `T` delimiter | ||||
| var ConverterRFC339NanoTimeToString = NewDBTypeConverter[rfctime.RFC3339NanoTime, string](func(v rfctime.RFC3339NanoTime) (string, error) { | ||||
| 	return v.Format(time.RFC3339Nano), nil | ||||
| 	return v.Time().In(time.UTC).Format("2006-01-02 15:04:05.999999999"), nil | ||||
| }, func(v string) (rfctime.RFC3339NanoTime, error) { | ||||
| 	t, err := time.Parse(time.RFC3339Nano, v) | ||||
| 	t, err := time.ParseInLocation("2006-01-02 15:04:05.999999999", v, time.UTC) | ||||
| 	if err != nil { | ||||
| 		return rfctime.RFC3339NanoTime{}, err | ||||
| 	} | ||||
| 	return rfctime.NewRFC3339Nano(t), nil | ||||
| }) | ||||
|  | ||||
| var ConverterRFCDateToString = NewDBTypeConverter[rfctime.Date, string](func(v rfctime.Date) (string, error) { | ||||
| 	return fmt.Sprintf("%04d-%02d-%02d", v.Year, v.Month, v.Day), nil | ||||
| }, func(v string) (rfctime.Date, error) { | ||||
| 	split := strings.Split(v, "-") | ||||
| 	if len(split) != 3 { | ||||
| 		return rfctime.Date{}, errors.New("invalid date format: " + v) | ||||
| 	} | ||||
| 	year, err := strconv.ParseInt(split[0], 10, 32) | ||||
| 	if err != nil { | ||||
| 		return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) | ||||
| 	} | ||||
| 	month, err := strconv.ParseInt(split[0], 10, 32) | ||||
| 	if err != nil { | ||||
| 		return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) | ||||
| 	} | ||||
| 	day, err := strconv.ParseInt(split[0], 10, 32) | ||||
| 	if err != nil { | ||||
| 		return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return rfctime.Date{Year: int(year), Month: int(month), Day: int(day)}, nil | ||||
| }) | ||||
|  | ||||
| var ConverterRFCTimeToString = NewDBTypeConverter[rfctime.Time, string](func(v rfctime.Time) (string, error) { | ||||
| 	return v.SerializeShort(), nil | ||||
| }, func(v string) (rfctime.Time, error) { | ||||
| 	res := rfctime.Time{} | ||||
| 	err := res.Deserialize(v) | ||||
| 	if err != nil { | ||||
| 		return rfctime.Time{}, err | ||||
| 	} | ||||
| 	return res, nil | ||||
| }) | ||||
|  | ||||
| var ConverterRFCSecondsF64ToString = NewDBTypeConverter[rfctime.SecondsF64, float64](func(v rfctime.SecondsF64) (float64, error) { | ||||
| 	return v.Seconds(), nil | ||||
| }, func(v float64) (rfctime.SecondsF64, error) { | ||||
| 	return rfctime.NewSecondsF64(timeext.FromSeconds(v)), nil | ||||
| }) | ||||
|  | ||||
| var ConverterJsonObjToString = NewDBTypeConverter[JsonObj, string](func(v JsonObj) (string, error) { | ||||
| 	mrsh, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return string(mrsh), nil | ||||
| }, func(v string) (JsonObj, error) { | ||||
| 	var mrsh JsonObj | ||||
| 	if err := json.Unmarshal([]byte(v), &mrsh); err != nil { | ||||
| 		return JsonObj{}, err | ||||
| 	} | ||||
| 	return mrsh, nil | ||||
| }) | ||||
|  | ||||
| var ConverterJsonArrToString = NewDBTypeConverter[JsonArr, string](func(v JsonArr) (string, error) { | ||||
| 	mrsh, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return string(mrsh), nil | ||||
| }, func(v string) (JsonArr, error) { | ||||
| 	var mrsh JsonArr | ||||
| 	if err := json.Unmarshal([]byte(v), &mrsh); err != nil { | ||||
| 		return JsonArr{}, err | ||||
| 	} | ||||
| 	return mrsh, nil | ||||
| }) | ||||
|  | ||||
| var ConverterExErrCategoryToString = NewDBTypeConverter[exerr.ErrorCategory, string](func(v exerr.ErrorCategory) (string, error) { | ||||
| 	return v.Category, nil | ||||
| }, func(v string) (exerr.ErrorCategory, error) { | ||||
| 	for _, cat := range exerr.AllCategories { | ||||
| 		if cat.Category == v { | ||||
| 			return cat, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return exerr.CatUser, errors.New("failed to convert '" + v + "' to exerr.ErrorCategory") | ||||
| }) | ||||
|  | ||||
| var ConverterExErrSeverityToString = NewDBTypeConverter[exerr.ErrorSeverity, string](func(v exerr.ErrorSeverity) (string, error) { | ||||
| 	return v.Severity, nil | ||||
| }, func(v string) (exerr.ErrorSeverity, error) { | ||||
| 	for _, sev := range exerr.AllSeverities { | ||||
| 		if sev.Severity == v { | ||||
| 			return sev, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return exerr.SevErr, errors.New("failed to convert '" + v + "' to exerr.ErrorSeverity") | ||||
| }) | ||||
|  | ||||
| var ConverterExErrTypeToString = NewDBTypeConverter[exerr.ErrorType, string](func(v exerr.ErrorType) (string, error) { | ||||
| 	return v.Key, nil | ||||
| }, func(v string) (exerr.ErrorType, error) { | ||||
| 	for _, etp := range exerr.ListRegisteredTypes() { | ||||
| 		if etp.Key == v { | ||||
| 			return etp, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return exerr.NewType(v, nil), nil | ||||
| }) | ||||
|  | ||||
| type dbTypeConverterImpl[TModelData any, TDBData any] struct { | ||||
| 	dbTypeString    string | ||||
| 	modelTypeString string | ||||
|   | ||||
| @@ -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" | ||||
| ) | ||||
| @@ -45,7 +46,7 @@ func (db *database) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Resul | ||||
| 	for _, v := range db.lstr { | ||||
| 		err := v.PreExec(ctx, nil, &sqlstr, &prep) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, exerr.Wrap(err, "failed to call SQL pre-exec listener").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -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 | ||||
| } | ||||
| @@ -66,7 +67,7 @@ func (db *database) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx.Ro | ||||
| 	for _, v := range db.lstr { | ||||
| 		err := v.PreQuery(ctx, nil, &sqlstr, &prep) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, exerr.Wrap(err, "failed to call SQL pre-query listener").Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -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 { | ||||
| @@ -142,10 +143,22 @@ func (db *database) RegisterConverter(conv DBTypeConverter) { | ||||
|  | ||||
| func (db *database) RegisterDefaultConverter() { | ||||
| 	db.RegisterConverter(ConverterBoolToBit) | ||||
|  | ||||
| 	db.RegisterConverter(ConverterTimeToUnixMillis) | ||||
|  | ||||
| 	db.RegisterConverter(ConverterRFCUnixMilliTimeToUnixMillis) | ||||
| 	db.RegisterConverter(ConverterRFCUnixNanoTimeToUnixNanos) | ||||
| 	db.RegisterConverter(ConverterRFCUnixTimeToUnixSeconds) | ||||
| 	db.RegisterConverter(ConverterRFC339TimeToString) | ||||
| 	db.RegisterConverter(ConverterRFC339NanoTimeToString) | ||||
| 	db.RegisterConverter(ConverterRFCDateToString) | ||||
| 	db.RegisterConverter(ConverterRFCTimeToString) | ||||
| 	db.RegisterConverter(ConverterRFCSecondsF64ToString) | ||||
|  | ||||
| 	db.RegisterConverter(ConverterJsonObjToString) | ||||
| 	db.RegisterConverter(ConverterJsonArrToString) | ||||
|  | ||||
| 	db.RegisterConverter(ConverterExErrCategoryToString) | ||||
| 	db.RegisterConverter(ConverterExErrSeverityToString) | ||||
| 	db.RegisterConverter(ConverterExErrTypeToString) | ||||
| } | ||||
|   | ||||
							
								
								
									
										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 | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										5
									
								
								sq/json.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								sq/json.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package sq | ||||
|  | ||||
| type JsonObj map[string]any | ||||
|  | ||||
| type JsonArr []any | ||||
							
								
								
									
										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()) | ||||
| } | ||||
							
								
								
									
										115
									
								
								sq/paginate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								sq/paginate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| package sq | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" | ||||
| ) | ||||
|  | ||||
| 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{} | ||||
|  | ||||
| 	sortOrder := filter.Sort() | ||||
| 	sortCond := "" | ||||
| 	if len(sortOrder) > 0 { | ||||
| 		sortCond = "ORDER BY " | ||||
| 		for i, v := range sortOrder { | ||||
| 			if i > 0 { | ||||
| 				sortCond += ", " | ||||
| 			} | ||||
| 			sortCond += v.Field + " " + string(v.Direction) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	pageCond := "" | ||||
| 	if limit != nil { | ||||
| 		pageCond += fmt.Sprintf("LIMIT :%s OFFSET :%s", prepParams.Add(*limit+1), prepParams.Add(*limit*(page-1))) | ||||
| 	} | ||||
|  | ||||
| 	filterCond, joinCond, joinTables := filter.SQL(prepParams) | ||||
|  | ||||
| 	selectCond := table + ".*" | ||||
| 	for _, v := range joinTables { | ||||
| 		selectCond += ", " + v + ".*" | ||||
| 	} | ||||
|  | ||||
| 	sqlQueryData := "SELECT " + selectCond + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " ) " + sortCond + " " + pageCond | ||||
| 	sqlQueryCount := "SELECT " + "COUNT(*)" + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " ) " | ||||
|  | ||||
| 	rows, err := q.Query(ctx, sqlQueryData, prepParams) | ||||
| 	if err != nil { | ||||
| 		return nil, pag.Pagination{}, exerr.Wrap(err, "failed to list paginated entries from DB").Str("table", table).Any("filter", filter).Int("page", page).Any("limit", limit).Build() | ||||
| 	} | ||||
|  | ||||
| 	entities, err := ScanAll[TData](ctx, q, rows, scanMode, scanSec, true) | ||||
| 	if err != nil { | ||||
| 		return nil, pag.Pagination{}, exerr.Wrap(err, "failed to decode paginated entries from DB").Str("table", table).Int("page", page).Any("limit", limit).Str("scanMode", string(scanMode)).Str("scanSec", string(scanSec)).Build() | ||||
| 	} | ||||
|  | ||||
| 	if page == 1 && (limit == nil || len(entities) <= *limit) { | ||||
| 		return entities, pag.Pagination{ | ||||
| 			Page:             1, | ||||
| 			Limit:            langext.Coalesce(limit, len(entities)), | ||||
| 			TotalPages:       1, | ||||
| 			TotalItems:       len(entities), | ||||
| 			CurrentPageCount: 1, | ||||
| 		}, nil | ||||
| 	} else { | ||||
|  | ||||
| 		countRows, err := q.Query(ctx, sqlQueryCount, prepParams) | ||||
| 		if err != nil { | ||||
| 			return nil, pag.Pagination{}, exerr.Wrap(err, "failed to query total-count of paginated entries from DB").Str("table", table).Build() | ||||
| 		} | ||||
|  | ||||
| 		if !countRows.Next() { | ||||
| 			return nil, pag.Pagination{}, exerr.New(exerr.TypeSQLDecode, "SQL COUNT(*) query returned no rows").Str("table", table).Any("filter", filter).Build() | ||||
| 		} | ||||
|  | ||||
| 		var countRes int | ||||
| 		err = countRows.Scan(&countRes) | ||||
| 		if err != nil { | ||||
| 			return nil, pag.Pagination{}, exerr.Wrap(err, "failed to decode total-count of paginated entries from DB").Str("table", table).Build() | ||||
| 		} | ||||
|  | ||||
| 		if len(entities) > *limit { | ||||
| 			entities = entities[:*limit] | ||||
| 		} | ||||
|  | ||||
| 		paginationObj := pag.Pagination{ | ||||
| 			Page:             page, | ||||
| 			Limit:            langext.Coalesce(limit, countRes), | ||||
| 			TotalPages:       pag.CalcPaginationTotalPages(countRes, langext.Coalesce(limit, countRes)), | ||||
| 			TotalItems:       countRes, | ||||
| 			CurrentPageCount: len(entities), | ||||
| 		} | ||||
|  | ||||
| 		return entities, paginationObj, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Count(ctx context.Context, q Queryable, table string, filter PaginateFilter) (int, error) { | ||||
| 	prepParams := PP{} | ||||
|  | ||||
| 	filterCond, joinCond, _ := filter.SQL(prepParams) | ||||
|  | ||||
| 	sqlQueryCount := "SELECT " + "COUNT(*)" + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " )" | ||||
|  | ||||
| 	countRows, err := q.Query(ctx, sqlQueryCount, prepParams) | ||||
| 	if err != nil { | ||||
| 		return 0, exerr.Wrap(err, "failed to query count of entries from DB").Str("table", table).Build() | ||||
| 	} | ||||
|  | ||||
| 	if !countRows.Next() { | ||||
| 		return 0, exerr.New(exerr.TypeSQLDecode, "SQL COUNT(*) query returned no rows").Str("table", table).Any("filter", filter).Build() | ||||
| 	} | ||||
|  | ||||
| 	var countRes int | ||||
| 	err = countRows.Scan(&countRes) | ||||
| 	if err != nil { | ||||
| 		return 0, exerr.Wrap(err, "failed to decode count of entries from DB").Str("table", table).Build() | ||||
| 	} | ||||
|  | ||||
| 	return countRes, nil | ||||
| } | ||||
							
								
								
									
										18
									
								
								sq/params.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								sq/params.go
									
									
									
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| package sq | ||||
|  | ||||
| import "gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
|  | ||||
| type PP map[string]any | ||||
|  | ||||
| func Join(pps ...PP) PP { | ||||
| @@ -11,3 +13,19 @@ func Join(pps ...PP) PP { | ||||
| 	} | ||||
| 	return r | ||||
| } | ||||
|  | ||||
| func (pp *PP) Add(v any) string { | ||||
| 	id := PPID() | ||||
| 	(*pp)[id] = v | ||||
| 	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,9 +2,10 @@ package sq | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"github.com/glebarez/go-sqlite" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	"github.com/mattn/go-sqlite3" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rfctime" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||
| @@ -20,18 +21,20 @@ func TestTypeConverter1(t *testing.T) { | ||||
| 		Timestamp time.Time `db:"timestamp"` | ||||
| 	} | ||||
|  | ||||
| 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||
| 		sqlite.RegisterAsSQLITE3() | ||||
| 	} | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	dbdir := t.TempDir() | ||||
| 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||
|  | ||||
| 	sqlite3.Version() // ensure loaded | ||||
|  | ||||
| 	tst.AssertNoErr(t, os.MkdirAll(dbdir, os.ModePerm)) | ||||
|  | ||||
| 	url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", dbfile1, "DELETE", 1000, "true", 1000) | ||||
| 	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("sqlite3", url))(t) | ||||
| 	xdb := tst.Must(sqlx.Open("sqlite", url))(t) | ||||
|  | ||||
| 	db := NewDB(xdb) | ||||
| 	db.RegisterDefaultConverter() | ||||
| @@ -47,6 +50,11 @@ func TestTypeConverter1(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestTypeConverter2(t *testing.T) { | ||||
|  | ||||
| 	if !langext.InArray("sqlite3", sql.Drivers()) { | ||||
| 		sqlite.RegisterAsSQLITE3() | ||||
| 	} | ||||
|  | ||||
| 	type RequestData struct { | ||||
| 		ID        string                `db:"id"` | ||||
| 		Timestamp rfctime.UnixMilliTime `db:"timestamp"` | ||||
| @@ -57,13 +65,11 @@ func TestTypeConverter2(t *testing.T) { | ||||
| 	dbdir := t.TempDir() | ||||
| 	dbfile1 := filepath.Join(dbdir, langext.MustHexUUID()+".sqlite3") | ||||
|  | ||||
| 	sqlite3.Version() // ensure loaded | ||||
|  | ||||
| 	tst.AssertNoErr(t, os.MkdirAll(dbdir, os.ModePerm)) | ||||
|  | ||||
| 	url := fmt.Sprintf("file:%s?_journal=%s&_timeout=%d&_fk=%s&_busy_timeout=%d", dbfile1, "DELETE", 1000, "true", 1000) | ||||
| 	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("sqlite3", url))(t) | ||||
| 	xdb := tst.Must(sqlx.Open("sqlite", url))(t) | ||||
|  | ||||
| 	db := NewDB(xdb) | ||||
| 	db.RegisterDefaultConverter() | ||||
| @@ -87,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 | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 	"github.com/jmoiron/sqlx" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| ) | ||||
|  | ||||
| @@ -48,7 +49,7 @@ func (tx *transaction) Rollback() error { | ||||
| 	for _, v := range tx.db.lstr { | ||||
| 		err := v.PreTxRollback(tx.id) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return exerr.Wrap(err, "failed to call SQL pre-rollback listener").Int("tx.id", int(tx.id)).Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -69,7 +70,7 @@ func (tx *transaction) Commit() error { | ||||
| 	for _, v := range tx.db.lstr { | ||||
| 		err := v.PreTxCommit(tx.id) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return exerr.Wrap(err, "failed to call SQL pre-commit listener").Int("tx.id", int(tx.id)).Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -91,7 +92,7 @@ func (tx *transaction) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Re | ||||
| 	for _, v := range tx.db.lstr { | ||||
| 		err := v.PreExec(ctx, langext.Ptr(tx.id), &sqlstr, &prep) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, exerr.Wrap(err, "failed to call SQL pre-exec listener").Int("tx.id", int(tx.id)).Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -106,7 +107,7 @@ func (tx *transaction) Exec(ctx context.Context, sqlstr string, prep PP) (sql.Re | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, exerr.Wrap(err, "Failed to [exec] sql statement").Int("tx.id", int(tx.id)).Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 	} | ||||
| 	return res, nil | ||||
| } | ||||
| @@ -116,7 +117,7 @@ func (tx *transaction) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx | ||||
| 	for _, v := range tx.db.lstr { | ||||
| 		err := v.PreQuery(ctx, langext.Ptr(tx.id), &sqlstr, &prep) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, exerr.Wrap(err, "failed to call SQL pre-query listener").Int("tx.id", int(tx.id)).Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -131,7 +132,7 @@ func (tx *transaction) Query(ctx context.Context, sqlstr string, prep PP) (*sqlx | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, exerr.Wrap(err, "Failed to [query] sql statement").Int("tx.id", int(tx.id)).Str("original_sql", origsql).Str("sql", sqlstr).Any("sql_params", prep).Build() | ||||
| 	} | ||||
| 	return rows, nil | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -45,7 +45,7 @@ type Coll[TData any] struct { | ||||
| 	customDecoder       *func(ctx context.Context, dec Decodable) (TData, error) // custom decoding function (useful if TData is an interface) | ||||
| 	isInterfaceDataType bool                                                     // true if TData is an interface (not a struct) | ||||
| 	unmarshalHooks      []func(d TData) TData                                    // called for every object after unmarshalling | ||||
| 	extraModPipeline    mongo.Pipeline                                           // appended to pipelines after filter/limit/skip/sort, used for $lookup, $set, $unset, $project, etc | ||||
| 	extraModPipeline    []func(ctx context.Context) mongo.Pipeline               // appended to pipelines after filter/limit/skip/sort, used for $lookup, $set, $unset, $project, etc | ||||
| } | ||||
|  | ||||
| func (c *Coll[TData]) Collection() *mongo.Collection { | ||||
| @@ -83,7 +83,13 @@ func (c *Coll[TData]) WithUnmarshalHook(fn func(d TData) TData) *Coll[TData] { | ||||
| } | ||||
|  | ||||
| func (c *Coll[TData]) WithModifyingPipeline(p mongo.Pipeline) *Coll[TData] { | ||||
| 	c.extraModPipeline = append(c.extraModPipeline, p...) | ||||
| 	c.extraModPipeline = append(c.extraModPipeline, func(ctx context.Context) mongo.Pipeline { return p }) | ||||
|  | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| func (c *Coll[TData]) WithModifyingPipelineFunc(fn func(ctx context.Context) mongo.Pipeline) *Coll[TData] { | ||||
| 	c.extraModPipeline = append(c.extraModPipeline, fn) | ||||
|  | ||||
| 	return c | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,9 @@ import ( | ||||
|  | ||||
| func (c *Coll[TData]) Aggregate(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) ([]TData, error) { | ||||
|  | ||||
| 	pipeline = langext.ArrConcat(pipeline, c.extraModPipeline) | ||||
| 	for _, ppl := range c.extraModPipeline { | ||||
| 		pipeline = langext.ArrConcat(pipeline, ppl(ctx)) | ||||
| 	} | ||||
|  | ||||
| 	cursor, err := c.coll.Aggregate(ctx, pipeline, opts...) | ||||
| 	if err != nil { | ||||
| @@ -27,7 +29,9 @@ func (c *Coll[TData]) Aggregate(ctx context.Context, pipeline mongo.Pipeline, op | ||||
|  | ||||
| func (c *Coll[TData]) AggregateOneOpt(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (*TData, error) { | ||||
|  | ||||
| 	pipeline = langext.ArrConcat(pipeline, c.extraModPipeline) | ||||
| 	for _, ppl := range c.extraModPipeline { | ||||
| 		pipeline = langext.ArrConcat(pipeline, ppl(ctx)) | ||||
| 	} | ||||
|  | ||||
| 	cursor, err := c.coll.Aggregate(ctx, pipeline, opts...) | ||||
| 	if err != nil { | ||||
| @@ -47,7 +51,9 @@ func (c *Coll[TData]) AggregateOneOpt(ctx context.Context, pipeline mongo.Pipeli | ||||
|  | ||||
| func (c *Coll[TData]) AggregateOne(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (TData, error) { | ||||
|  | ||||
| 	pipeline = langext.ArrConcat(pipeline, c.extraModPipeline) | ||||
| 	for _, ppl := range c.extraModPipeline { | ||||
| 		pipeline = langext.ArrConcat(pipeline, ppl(ctx)) | ||||
| 	} | ||||
|  | ||||
| 	cursor, err := c.coll.Aggregate(ctx, pipeline, opts...) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -32,7 +32,9 @@ func (c *Coll[TData]) Find(ctx context.Context, filter bson.M, opts ...*options. | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	pipeline = langext.ArrConcat(pipeline, c.extraModPipeline) | ||||
| 	for _, ppl := range c.extraModPipeline { | ||||
| 		pipeline = langext.ArrConcat(pipeline, ppl(ctx)) | ||||
| 	} | ||||
|  | ||||
| 	for _, opt := range opts { | ||||
| 		if opt != nil && opt.Projection != nil { | ||||
|   | ||||
| @@ -71,7 +71,9 @@ func (c *Coll[TData]) findOneInternal(ctx context.Context, filter bson.M, allowN | ||||
| 		pipeline = append(pipeline, bson.D{{Key: "$match", Value: filter}}) | ||||
| 		pipeline = append(pipeline, bson.D{{Key: "$limit", Value: 1}}) | ||||
|  | ||||
| 		pipeline = langext.ArrConcat(pipeline, c.extraModPipeline) | ||||
| 		for _, ppl := range c.extraModPipeline { | ||||
| 			pipeline = langext.ArrConcat(pipeline, ppl(ctx)) | ||||
| 		} | ||||
|  | ||||
| 		cursor, err := c.coll.Aggregate(ctx, pipeline) | ||||
| 		if err != nil { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
| 	"go.mongodb.org/mongo-driver/mongo" | ||||
| 	ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| ) | ||||
|  | ||||
| func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int, inTok ct.CursorToken) ([]TData, ct.CursorToken, error) { | ||||
| @@ -50,7 +51,10 @@ func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int, | ||||
| 	} | ||||
|  | ||||
| 	pipeline = append(pipeline, paginationPipeline...) | ||||
| 	pipeline = append(pipeline, c.extraModPipeline...) | ||||
|  | ||||
| 	for _, ppl := range c.extraModPipeline { | ||||
| 		pipeline = langext.ArrConcat(pipeline, ppl(ctx)) | ||||
| 	} | ||||
|  | ||||
| 	cursor, err := c.coll.Aggregate(ctx, pipeline) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import ( | ||||
| 	pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" | ||||
| ) | ||||
|  | ||||
| func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.Filter, page int, limit *int) ([]TData, pag.Pagination, error) { | ||||
| func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.MongoFilter, page int, limit *int) ([]TData, pag.Pagination, error) { | ||||
| 	type totalCountResult struct { | ||||
| 		Count int `bson:"count"` | ||||
| 	} | ||||
| @@ -42,7 +42,12 @@ func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.Filter, page int, | ||||
| 	pipelineCount := mongo.Pipeline{} | ||||
| 	pipelineCount = append(pipelineCount, bson.D{{Key: "$count", Value: "count"}}) | ||||
|  | ||||
| 	pipelineList := langext.ArrConcat(pipelineFilter, pipelineSort, pipelinePaginate, c.extraModPipeline, pipelineSort) | ||||
| 	extrModPipelineResolved := mongo.Pipeline{} | ||||
| 	for _, ppl := range c.extraModPipeline { | ||||
| 		extrModPipelineResolved = langext.ArrConcat(extrModPipelineResolved, ppl(ctx)) | ||||
| 	} | ||||
|  | ||||
| 	pipelineList := langext.ArrConcat(pipelineFilter, pipelineSort, pipelinePaginate, extrModPipelineResolved, pipelineSort) | ||||
| 	pipelineTotalCount := langext.ArrConcat(pipelineFilter, pipelineCount) | ||||
|  | ||||
| 	cursorList, err := c.coll.Aggregate(ctx, pipelineList) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user