Compare commits
	
		
			49 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1ea6695f82 | |||
| 5273ff7600 | |||
| caa69c3629 | |||
| 0ff5f0aa28 | |||
| d5cb1e48ed | |||
| 1c2d3f541f | |||
| ec62ad436f | |||
| 8d0ef0f002 | |||
| d78550672e | |||
| 1d629f6db8 | |||
| f7d291056d | |||
| 710c257c64 | |||
| c320bb3d90 | |||
| 2f01a1d50f | |||
| ffc57b7e89 | |||
| d88cd3c22b | |||
| ac5ad640bd | |||
| 21d241f9b1 | |||
| 2569c165f8 | |||
| ee262a94fb | |||
| 7977c0e59c | |||
| ceff0161c6 | |||
| a30da61419 | |||
| b613b122e3 | |||
| d017530444 | |||
| 8de83cc290 | |||
| 603ec82b83 | |||
| 93c4cf31a8 | |||
| dc2d8a9103 | |||
| 6589e8d5cd | |||
| 0006c6859d | |||
| 827b3fc1b7 | |||
| f7dce4a102 | |||
| 45d4fd7101 | |||
| c7df9d2264 | |||
| d0954bf133 | |||
| 8affa81bb9 | |||
| fe9ebf0bab | |||
| a4b5f33d15 | |||
| e89e2c18f2 | |||
| b16d5152c7 | |||
| 5fb2f8a312 | |||
| 2ad820be8d | |||
| 555096102a | |||
| d76d7b5cb9 | |||
| 6622c9003d | |||
| b02e1d2e85 | |||
| c338d23070 | |||
| 1fbae343a4 | 
							
								
								
									
										6
									
								
								.idea/goext.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/goext.iml
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,10 @@ | |||||||
| <?xml version="1.0" encoding="UTF-8"?> | <?xml version="1.0" encoding="UTF-8"?> | ||||||
| <module type="WEB_MODULE" version="4"> | <module type="WEB_MODULE" version="4"> | ||||||
|   <component name="Go" enabled="true" /> |   <component name="Go" enabled="true"> | ||||||
|  |     <buildTags> | ||||||
|  |       <option name="goVersion" value="1.19" /> | ||||||
|  |     </buildTags> | ||||||
|  |   </component> | ||||||
|   <component name="NewModuleRootManager"> |   <component name="NewModuleRootManager"> | ||||||
|     <content url="file://$MODULE_DIR$" /> |     <content url="file://$MODULE_DIR$" /> | ||||||
|     <orderEntry type="inheritedJdk" /> |     <orderEntry type="inheritedJdk" /> | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								README.md
									
									
									
									
									
								
							| @@ -6,3 +6,36 @@ A collection of general & useful library methods | |||||||
| This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features... | This should not have any heavy dependencies (gin, mongo, etc) and add missing basic language features... | ||||||
|  |  | ||||||
| Potentially needs `export GOPRIVATE="gogs.mikescher.com"` | Potentially needs `export GOPRIVATE="gogs.mikescher.com"` | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Packages: | ||||||
|  |  | ||||||
|  | | Name         | Maintainer | Description                                                                                                   | | ||||||
|  | |--------------|------------|---------------------------------------------------------------------------------------------------------------| | ||||||
|  | | langext      | Mike       | General uttility/helper functions, (everything thats missing from go standard library)                        | | ||||||
|  | | mathext      | Mike       | Utility/Helper functions for math                                                                             | | ||||||
|  | | cryptext     | Mike       | Utility/Helper functions for encryption                                                                       | | ||||||
|  | | syncext      | Mike       | Utility/Helper funtions for multi-threading / mutex / channels                                                | | ||||||
|  | | dataext      | Mike       | Various useful data structures                                                                                | | ||||||
|  | | zipext       | Mike       | Utility for zip/gzip/tar etc                                                                                  | | ||||||
|  | | reflectext   | Mike       | Utility for golagn reflection                                                                                 | | ||||||
|  | |              |            |                                                                                                               | | ||||||
|  | | mongoext     | Mike       | Utility/Helper functions for mongodb                                                                          | | ||||||
|  | | cursortoken  | Mike       | MongoDB cursortoken implementation                                                                            | | ||||||
|  | |              |            |                                                                                                               | | ||||||
|  | | totpext      | Mike       | Implementation of TOTP (2-Factor-Auth)                                                                        | | ||||||
|  | | termext      | Mike       | Utilities for terminals (mostly color output)                                                                 | | ||||||
|  | | confext      | Mike       | Parses environment configuration into structs                                                                 | | ||||||
|  | | cmdext       | Mike       | Runner for external commands/processes                                                                        | | ||||||
|  | |              |            |                                                                                                               | | ||||||
|  | | sq           | Mike       | Utility functions for sql based databases                                                                     | | ||||||
|  | | tst          | Mike       | Utility functions for unit tests                                                                              | | ||||||
|  | |              |            |                                                                                                               | | ||||||
|  | | rfctime      | Mike       | Classes for time seriallization, with different marshallign method for mongo and json                         | | ||||||
|  | | gojson       | Mike       | Same interface for marshalling/unmarshalling as go/json, except with proper serialization of null arrays/maps | | ||||||
|  | |              |            |                                                                                                               | | ||||||
|  | | bfcodegen    | Mike       | Various codegen tools (run via go generate)                                                                   | | ||||||
|  | |              |            |                                                                                                               | | ||||||
|  | | rext         | Mike       | Regex Wrapper, wraps regexp with a better interface                                                           | | ||||||
|  | | wmo          | Mike       | Mongo Wrapper, wraps mongodb with a better interface                                                          | | ||||||
|  | |              |            |                                                                                                               | | ||||||
							
								
								
									
										13
									
								
								TODO.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								TODO.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  - cronext | ||||||
|  |  | ||||||
|  |  - cursortoken | ||||||
|  |  | ||||||
|  |  - typed/geenric mongo wrapper | ||||||
|  |  | ||||||
|  |  - error package | ||||||
|  |  | ||||||
|  | - rfctime.DateOnly | ||||||
|  | - rfctime.HMSTimeOnly | ||||||
|  | - rfctime.NanoTimeOnly | ||||||
| @@ -23,6 +23,8 @@ fi | |||||||
|  |  | ||||||
| git pull --ff | git pull --ff | ||||||
|  |  | ||||||
|  | go get -u ./... | ||||||
|  |  | ||||||
| curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g') | curr_vers=$(git describe --tags --abbrev=0 | sed 's/v//g') | ||||||
|  |  | ||||||
| next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') | next_ver=$(echo "$curr_vers" | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}') | ||||||
| @@ -32,10 +34,22 @@ echo "> Current Version: ${curr_vers}" | |||||||
| echo "> Next    Version: ${next_ver}" | echo "> Next    Version: ${next_ver}" | ||||||
| echo "" | echo "" | ||||||
|  |  | ||||||
|  | printf "package goext\n\nconst GoextVersion = \"%s\"\n\nconst GoextVersionTimestamp = \"%s\"\n" "${next_ver}" "$( date +"%Y-%m-%dT%H:%M:%S%z" )" > "goextVersion.go" | ||||||
|  |  | ||||||
| git add --verbose . | git add --verbose . | ||||||
|  |  | ||||||
|  | echo -n "Insert option commit message: " | ||||||
|  |  | ||||||
|  | read commitMessage | ||||||
|  |  | ||||||
| msg="v${next_ver}" | msg="v${next_ver}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if [[ "$commitMessage" != "" ]]; then | ||||||
|  |   msg="${msg} ${commitMessage}" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  |  | ||||||
| if [ $# -gt 0 ]; then | if [ $# -gt 0 ]; then | ||||||
|   msg="$1" |   msg="$1" | ||||||
| fi | fi | ||||||
|   | |||||||
| @@ -3,11 +3,15 @@ package bfcodegen | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext" | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/cmdext" | 	"gogs.mikescher.com/BlackForestBytes/goext/cmdext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/cryptext" | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rext" | 	"gogs.mikescher.com/BlackForestBytes/goext/rext" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -21,6 +25,7 @@ type EnumDefVal struct { | |||||||
|  |  | ||||||
| type EnumDef struct { | type EnumDef struct { | ||||||
| 	File         string | 	File         string | ||||||
|  | 	FileRelative string | ||||||
| 	EnumTypeName string | 	EnumTypeName string | ||||||
| 	Type         string | 	Type         string | ||||||
| 	Values       []EnumDefVal | 	Values       []EnumDefVal | ||||||
| @@ -32,6 +37,8 @@ var rexEnumDef = rext.W(regexp.MustCompile("^\\s*type\\s+(?P<name>[A-Za-z0-9_]+) | |||||||
|  |  | ||||||
| var rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*=\\s*(?P<value>(\"[A-Za-z0-9_:]+\"|[0-9]+))\\s*(//(?P<descr>.*))?.*$")) | var rexValueDef = rext.W(regexp.MustCompile("^\\s*(?P<name>[A-Za-z0-9_]+)\\s+(?P<type>[A-Za-z0-9_]+)\\s*=\\s*(?P<value>(\"[A-Za-z0-9_:]+\"|[0-9]+))\\s*(//(?P<descr>.*))?.*$")) | ||||||
|  |  | ||||||
|  | var rexChecksumConst = rext.W(regexp.MustCompile("const ChecksumGenerator = \"(?P<cs>[A-Za-z0-9_]*)\"")) | ||||||
|  |  | ||||||
| func GenerateEnumSpecs(sourceDir string, destFile string) error { | func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||||
|  |  | ||||||
| 	files, err := os.ReadDir(sourceDir) | 	files, err := os.ReadDir(sourceDir) | ||||||
| @@ -39,17 +46,46 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	oldChecksum := "N/A" | ||||||
|  | 	if _, err := os.Stat(destFile); !os.IsNotExist(err) { | ||||||
|  | 		content, err := os.ReadFile(destFile) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if m, ok := rexChecksumConst.MatchFirst(string(content)); ok { | ||||||
|  | 			oldChecksum = m.GroupByName("cs").Value() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return v.Name() != path.Base(destFile) }) | ||||||
|  | 	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return strings.HasSuffix(v.Name(), ".go") }) | ||||||
|  | 	langext.SortBy(files, func(v os.DirEntry) string { return v.Name() }) | ||||||
|  |  | ||||||
|  | 	newChecksumStr := goext.GoextVersion | ||||||
|  | 	for _, f := range files { | ||||||
|  | 		content, err := os.ReadFile(path.Join(sourceDir, f.Name())) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	newChecksum := cryptext.BytesSha256([]byte(newChecksumStr)) | ||||||
|  |  | ||||||
|  | 	if newChecksum != oldChecksum { | ||||||
|  | 		fmt.Printf("[EnumGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Printf("[EnumGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	allEnums := make([]EnumDef, 0) | 	allEnums := make([]EnumDef, 0) | ||||||
|  |  | ||||||
| 	pkgname := "" | 	pkgname := "" | ||||||
|  |  | ||||||
| 	for _, f := range files { | 	for _, f := range files { | ||||||
| 		if !strings.HasSuffix(f.Name(), ".go") { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		fmt.Printf("========= %s =========\n\n", f.Name()) | 		fmt.Printf("========= %s =========\n\n", f.Name()) | ||||||
| 		fileEnums, pn, err := processFile(f.Name()) | 		fileEnums, pn, err := processFile(sourceDir, path.Join(sourceDir, f.Name())) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @@ -67,7 +103,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | |||||||
| 		return errors.New("no package name found in any file") | 		return errors.New("no package name found in any file") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = os.WriteFile(destFile, []byte(fmtOutput(allEnums, pkgname)), 0o755) | 	err = os.WriteFile(destFile, []byte(fmtOutput(newChecksum, allEnums, pkgname)), 0o755) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -89,7 +125,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func processFile(fn string) ([]EnumDef, string, error) { | func processFile(basedir string, fn string) ([]EnumDef, string, error) { | ||||||
| 	file, err := os.Open(fn) | 	file, err := os.Open(fn) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, "", err | 		return nil, "", err | ||||||
| @@ -119,8 +155,15 @@ func processFile(fn string) ([]EnumDef, string, error) { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if match, ok := rexEnumDef.MatchFirst(line); ok { | 		if match, ok := rexEnumDef.MatchFirst(line); ok { | ||||||
|  |  | ||||||
|  | 			rfp, err := filepath.Rel(basedir, fn) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, "", err | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			def := EnumDef{ | 			def := EnumDef{ | ||||||
| 				File:         fn, | 				File:         fn, | ||||||
|  | 				FileRelative: rfp, | ||||||
| 				EnumTypeName: match.GroupByName("name").Value(), | 				EnumTypeName: match.GroupByName("name").Value(), | ||||||
| 				Type:         match.GroupByName("type").Value(), | 				Type:         match.GroupByName("type").Value(), | ||||||
| 				Values:       make([]EnumDefVal, 0), | 				Values:       make([]EnumDefVal, 0), | ||||||
| @@ -159,8 +202,8 @@ func processFile(fn string) ([]EnumDef, string, error) { | |||||||
| 	return enums, pkgname, nil | 	return enums, pkgname, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func fmtOutput(enums []EnumDef, pkgname string) string { | func fmtOutput(cs string, enums []EnumDef, pkgname string) string { | ||||||
| 	str := "// Code generated by permissions_gen.sh DO NOT EDIT.\n" | 	str := "// Code generated by enum-generate.go DO NOT EDIT.\n" | ||||||
| 	str += "\n" | 	str += "\n" | ||||||
| 	str += "package " + pkgname + "\n" | 	str += "package " + pkgname + "\n" | ||||||
| 	str += "\n" | 	str += "\n" | ||||||
| @@ -168,6 +211,9 @@ func fmtOutput(enums []EnumDef, pkgname string) string { | |||||||
| 	str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n" | 	str += "import \"gogs.mikescher.com/BlackForestBytes/goext/langext\"" + "\n" | ||||||
| 	str += "\n" | 	str += "\n" | ||||||
|  |  | ||||||
|  | 	str += "const ChecksumGenerator = \"" + cs + "\"" + "\n" | ||||||
|  | 	str += "\n" | ||||||
|  |  | ||||||
| 	str += "type Enum interface {" + "\n" | 	str += "type Enum interface {" + "\n" | ||||||
| 	str += "    Valid() bool" + "\n" | 	str += "    Valid() bool" + "\n" | ||||||
| 	str += "    ValuesAny() []any" + "\n" | 	str += "    ValuesAny() []any" + "\n" | ||||||
| @@ -202,7 +248,7 @@ func fmtOutput(enums []EnumDef, pkgname string) string { | |||||||
|  |  | ||||||
| 		str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n" | 		str += "// ================================ " + enumdef.EnumTypeName + " ================================" + "\n" | ||||||
| 		str += "//" + "\n" | 		str += "//" + "\n" | ||||||
| 		str += "// File:       " + enumdef.File + "\n" | 		str += "// File:       " + enumdef.FileRelative + "\n" | ||||||
| 		str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n" | 		str += "// StringEnum: " + langext.Conditional(hasStr, "true", "false") + "\n" | ||||||
| 		str += "// DescrEnum:  " + langext.Conditional(hasDescr, "true", "false") + "\n" | 		str += "// DescrEnum:  " + langext.Conditional(hasDescr, "true", "false") + "\n" | ||||||
| 		str += "//" + "\n" | 		str += "//" + "\n" | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								bfcodegen/enum-generate_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								bfcodegen/enum-generate_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | package bfcodegen | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestApplyEnvOverridesSimple(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	err := GenerateEnumSpecs("/home/mike/Code/reiff/badennet/bnet-backend/models", "/home/mike/Code/reiff/badennet/bnet-backend/models/enums_gen.go") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Error(err) | ||||||
|  | 		t.Fail() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -178,9 +178,9 @@ func parseEnvToValue(envval string, fullEnvKey string, rvtype reflect.Type) (ref | |||||||
| 		if strings.TrimSpace(strings.ToLower(envval)) == "true" { | 		if strings.TrimSpace(strings.ToLower(envval)) == "true" { | ||||||
| 			return reflect.ValueOf(true).Convert(rvtype), nil | 			return reflect.ValueOf(true).Convert(rvtype), nil | ||||||
| 		} else if strings.TrimSpace(strings.ToLower(envval)) == "false" { | 		} else if strings.TrimSpace(strings.ToLower(envval)) == "false" { | ||||||
| 			return reflect.ValueOf(true).Convert(rvtype), nil |  | ||||||
| 		} else if strings.TrimSpace(strings.ToLower(envval)) == "1" { |  | ||||||
| 			return reflect.ValueOf(false).Convert(rvtype), nil | 			return reflect.ValueOf(false).Convert(rvtype), nil | ||||||
|  | 		} else if strings.TrimSpace(strings.ToLower(envval)) == "1" { | ||||||
|  | 			return reflect.ValueOf(true).Convert(rvtype), nil | ||||||
| 		} else if strings.TrimSpace(strings.ToLower(envval)) == "0" { | 		} else if strings.TrimSpace(strings.ToLower(envval)) == "0" { | ||||||
| 			return reflect.ValueOf(false).Convert(rvtype), nil | 			return reflect.ValueOf(false).Convert(rvtype), nil | ||||||
| 		} else { | 		} else { | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package cryptext | |||||||
| import ( | import ( | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
|  | 	"crypto/sha512" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"errors" | 	"errors" | ||||||
| @@ -14,14 +15,15 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const LatestPassHashVersion = 4 | const LatestPassHashVersion = 5 | ||||||
|  |  | ||||||
| // PassHash | // PassHash | ||||||
| // - [v0]: plaintext password ( `0|...` ) | // - [v0]: plaintext password ( `0|...` )                  // simple, used to write PW's directly in DB | ||||||
| // - [v1]: sha256(plaintext) | // - [v1]: sha256(plaintext)                               // simple hashing | ||||||
| // - [v2]: seed | sha256<seed>(plaintext) | // - [v2]: seed | sha256<seed>(plaintext)                  // add seed | ||||||
| // - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)] | // - [v3]: seed | sha256<seed>(plaintext) | [hex(totp)]    // add TOTP support | ||||||
| // - [v4]: bcrypt(plaintext) | [hex(totp)] | // - [v4]: bcrypt(plaintext) | [hex(totp)]                 // use proper bcrypt | ||||||
|  | // - [v5]: bcrypt(sha512(plaintext)) | [hex(totp)]         // hash pw before bcrypt (otherwise max pw-len = 72) | ||||||
| type PassHash string | type PassHash string | ||||||
|  |  | ||||||
| func (ph PassHash) Valid() bool { | func (ph PassHash) Valid() bool { | ||||||
| @@ -109,7 +111,21 @@ func (ph PassHash) Data() (_version int, _seed []byte, _payload []byte, _totp bo | |||||||
| 		totp := false | 		totp := false | ||||||
| 		totpsecret := make([]byte, 0) | 		totpsecret := make([]byte, 0) | ||||||
| 		if split[2] != "0" { | 		if split[2] != "0" { | ||||||
| 			totpsecret, err = hex.DecodeString(split[3]) | 			totpsecret, err = hex.DecodeString(split[2]) | ||||||
|  | 			totp = true | ||||||
|  | 		} | ||||||
|  | 		return int(version), nil, payload, totp, totpsecret, true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if version == 5 { | ||||||
|  | 		if len(split) != 3 { | ||||||
|  | 			return -1, nil, nil, false, nil, false | ||||||
|  | 		} | ||||||
|  | 		payload := []byte(split[1]) | ||||||
|  | 		totp := false | ||||||
|  | 		totpsecret := make([]byte, 0) | ||||||
|  | 		if split[2] != "0" { | ||||||
|  | 			totpsecret, err = hex.DecodeString(split[2]) | ||||||
| 			totp = true | 			totp = true | ||||||
| 		} | 		} | ||||||
| 		return int(version), nil, payload, totp, totpsecret, true | 		return int(version), nil, payload, totp, totpsecret, true | ||||||
| @@ -156,6 +172,14 @@ func (ph PassHash) Verify(plainpass string, totp *string) bool { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if version == 5 { | ||||||
|  | 		if !hastotp { | ||||||
|  | 			return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil | ||||||
|  | 		} else { | ||||||
|  | 			return bcrypt.CompareHashAndPassword(payload, hash512(plainpass)) == nil && totpext.Validate(totpsecret, *totp) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -209,6 +233,12 @@ func (ph PassHash) ClearTOTP() (PassHash, error) { | |||||||
| 		return PassHash(strings.Join(split, "|")), nil | 		return PassHash(strings.Join(split, "|")), nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if version == 5 { | ||||||
|  | 		split := strings.Split(string(ph), "|") | ||||||
|  | 		split[2] = "0" | ||||||
|  | 		return PassHash(strings.Join(split, "|")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return "", errors.New("unknown version") | 	return "", errors.New("unknown version") | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -242,6 +272,12 @@ func (ph PassHash) WithTOTP(totpSecret []byte) (PassHash, error) { | |||||||
| 		return PassHash(strings.Join(split, "|")), nil | 		return PassHash(strings.Join(split, "|")), nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if version == 5 { | ||||||
|  | 		split := strings.Split(string(ph), "|") | ||||||
|  | 		split[2] = hex.EncodeToString(totpSecret) | ||||||
|  | 		return PassHash(strings.Join(split, "|")), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return "", errors.New("unknown version") | 	return "", errors.New("unknown version") | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -271,6 +307,10 @@ func (ph PassHash) Change(newPlainPass string) (PassHash, error) { | |||||||
| 		return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) | 		return HashPasswordV4(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if version == 5 { | ||||||
|  | 		return HashPasswordV5(newPlainPass, langext.Conditional(hastotp, totpsecret, nil)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return "", errors.New("unknown version") | 	return "", errors.New("unknown version") | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -279,7 +319,24 @@ func (ph PassHash) String() string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { | func HashPassword(plainpass string, totpSecret []byte) (PassHash, error) { | ||||||
| 	return HashPasswordV4(plainpass, totpSecret) | 	return HashPasswordV5(plainpass, totpSecret) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func HashPasswordV5(plainpass string, totpSecret []byte) (PassHash, error) { | ||||||
|  | 	var strtotp string | ||||||
|  |  | ||||||
|  | 	if totpSecret == nil { | ||||||
|  | 		strtotp = "0" | ||||||
|  | 	} else { | ||||||
|  | 		strtotp = hex.EncodeToString(totpSecret) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	payload, err := bcrypt.GenerateFromPassword(hash512(plainpass), bcrypt.MinCost) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return PassHash(fmt.Sprintf("5|%s|%s", string(payload), strtotp)), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) { | func HashPasswordV4(plainpass string, totpSecret []byte) (PassHash, error) { | ||||||
| @@ -340,6 +397,13 @@ func HashPasswordV0(plainpass string) (PassHash, error) { | |||||||
| 	return PassHash(fmt.Sprintf("0|%s", plainpass)), nil | 	return PassHash(fmt.Sprintf("0|%s", plainpass)), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func hash512(s string) []byte { | ||||||
|  | 	h := sha512.New() | ||||||
|  | 	h.Write([]byte(s)) | ||||||
|  | 	bs := h.Sum(nil) | ||||||
|  | 	return bs | ||||||
|  | } | ||||||
|  |  | ||||||
| func hash256(s string) []byte { | func hash256(s string) []byte { | ||||||
| 	h := sha256.New() | 	h := sha256.New() | ||||||
| 	h.Write([]byte(s)) | 	h.Write([]byte(s)) | ||||||
|   | |||||||
							
								
								
									
										210
									
								
								cryptext/passHash_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								cryptext/passHash_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | |||||||
|  | package cryptext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/totpext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestPassHash1(t *testing.T) { | ||||||
|  | 	ph, err := HashPassword("test123", nil) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashTOTP(t *testing.T) { | ||||||
|  | 	sec, err := totpext.GenerateSecret() | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	ph, err := HashPassword("test123", sec) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertTrue(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V0(t *testing.T) { | ||||||
|  | 	ph, err := HashPasswordV0("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V1(t *testing.T) { | ||||||
|  | 	ph, err := HashPasswordV1("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V2(t *testing.T) { | ||||||
|  | 	ph, err := HashPasswordV2("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V3(t *testing.T) { | ||||||
|  | 	ph, err := HashPasswordV3("test123", nil) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V3_TOTP(t *testing.T) { | ||||||
|  | 	sec, err := totpext.GenerateSecret() | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	ph, err := HashPasswordV3("test123", sec) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertTrue(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertTrue(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V4(t *testing.T) { | ||||||
|  | 	ph, err := HashPasswordV4("test123", nil) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertFalse(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestPassHashUpgrade_V4_TOTP(t *testing.T) { | ||||||
|  | 	sec, err := totpext.GenerateSecret() | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	ph, err := HashPasswordV4("test123", sec) | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertTrue(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertTrue(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  |  | ||||||
|  | 	ph, err = ph.Upgrade("test123") | ||||||
|  | 	tst.AssertNoErr(t, err) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, ph.Valid()) | ||||||
|  | 	tst.AssertTrue(t, ph.HasTOTP()) | ||||||
|  | 	tst.AssertFalse(t, ph.NeedsPasswordUpgrade()) | ||||||
|  |  | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test123", nil)) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | 	tst.AssertTrue(t, ph.Verify("test123", langext.Ptr(totpext.TOTP(sec)))) | ||||||
|  | 	tst.AssertFalse(t, ph.Verify("test124", nil)) | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								cursortoken/direction.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								cursortoken/direction.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | package cursortoken | ||||||
|  |  | ||||||
|  | type SortDirection string //@enum:type | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	SortASC  SortDirection = "ASC" | ||||||
|  | 	SortDESC SortDirection = "DESC" | ||||||
|  | ) | ||||||
							
								
								
									
										10
									
								
								cursortoken/filter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cursortoken/filter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | package cursortoken | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Filter interface { | ||||||
|  | 	FilterQuery() mongo.Pipeline | ||||||
|  | 	Pagination() (string, SortDirection, string, SortDirection) | ||||||
|  | } | ||||||
							
								
								
									
										184
									
								
								cursortoken/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								cursortoken/token.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | |||||||
|  | package cursortoken | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/base32" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Mode string | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	CTMStart  Mode = "START" | ||||||
|  | 	CTMNormal Mode = "NORMAL" | ||||||
|  | 	CTMEnd    Mode = "END" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Extra struct { | ||||||
|  | 	Timestamp *time.Time | ||||||
|  | 	Id        *string | ||||||
|  | 	Page      *int | ||||||
|  | 	PageSize  *int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type CursorToken struct { | ||||||
|  | 	Mode               Mode | ||||||
|  | 	ValuePrimary       string | ||||||
|  | 	ValueSecondary     string | ||||||
|  | 	Direction          SortDirection | ||||||
|  | 	DirectionSecondary SortDirection | ||||||
|  | 	PageSize           int | ||||||
|  | 	Extra              Extra | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type cursorTokenSerialize struct { | ||||||
|  | 	ValuePrimary       *string        `json:"v1,omitempty"` | ||||||
|  | 	ValueSecondary     *string        `json:"v2,omitempty"` | ||||||
|  | 	Direction          *SortDirection `json:"dir,omitempty"` | ||||||
|  | 	DirectionSecondary *SortDirection `json:"dir2,omitempty"` | ||||||
|  | 	PageSize           *int           `json:"size,omitempty"` | ||||||
|  |  | ||||||
|  | 	ExtraTimestamp *time.Time `json:"ts,omitempty"` | ||||||
|  | 	ExtraId        *string    `json:"id,omitempty"` | ||||||
|  | 	ExtraPage      *int       `json:"pg,omitempty"` | ||||||
|  | 	ExtraPageSize  *int       `json:"sz,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Start() CursorToken { | ||||||
|  | 	return CursorToken{ | ||||||
|  | 		Mode:               CTMStart, | ||||||
|  | 		ValuePrimary:       "", | ||||||
|  | 		ValueSecondary:     "", | ||||||
|  | 		Direction:          "", | ||||||
|  | 		DirectionSecondary: "", | ||||||
|  | 		PageSize:           0, | ||||||
|  | 		Extra:              Extra{}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func End() CursorToken { | ||||||
|  | 	return CursorToken{ | ||||||
|  | 		Mode:               CTMEnd, | ||||||
|  | 		ValuePrimary:       "", | ||||||
|  | 		ValueSecondary:     "", | ||||||
|  | 		Direction:          "", | ||||||
|  | 		DirectionSecondary: "", | ||||||
|  | 		PageSize:           0, | ||||||
|  | 		Extra:              Extra{}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *CursorToken) Token() string { | ||||||
|  | 	if c.Mode == CTMStart { | ||||||
|  | 		return "@start" | ||||||
|  | 	} | ||||||
|  | 	if c.Mode == CTMEnd { | ||||||
|  | 		return "@end" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We kinda manually implement omitempty for the CursorToken here | ||||||
|  | 	// because omitempty does not work for time.Time and otherwise we would always | ||||||
|  | 	// get weird time values when decoding a token that initially didn't have an Timestamp set | ||||||
|  | 	// For this usecase we treat Unix=0 as an empty timestamp | ||||||
|  |  | ||||||
|  | 	sertok := cursorTokenSerialize{} | ||||||
|  |  | ||||||
|  | 	if c.ValuePrimary != "" { | ||||||
|  | 		sertok.ValuePrimary = &c.ValuePrimary | ||||||
|  | 	} | ||||||
|  | 	if c.ValueSecondary != "" { | ||||||
|  | 		sertok.ValueSecondary = &c.ValueSecondary | ||||||
|  | 	} | ||||||
|  | 	if c.Direction != "" { | ||||||
|  | 		sertok.Direction = &c.Direction | ||||||
|  | 	} | ||||||
|  | 	if c.DirectionSecondary != "" { | ||||||
|  | 		sertok.DirectionSecondary = &c.DirectionSecondary | ||||||
|  | 	} | ||||||
|  | 	if c.PageSize != 0 { | ||||||
|  | 		sertok.PageSize = &c.PageSize | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sertok.ExtraTimestamp = c.Extra.Timestamp | ||||||
|  | 	sertok.ExtraId = c.Extra.Id | ||||||
|  | 	sertok.ExtraPage = c.Extra.Page | ||||||
|  | 	sertok.ExtraPageSize = c.Extra.PageSize | ||||||
|  |  | ||||||
|  | 	body, err := json.Marshal(sertok) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "tok_" + base32.StdEncoding.EncodeToString(body) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Decode(tok string) (CursorToken, error) { | ||||||
|  | 	if tok == "" { | ||||||
|  | 		return Start(), nil | ||||||
|  | 	} | ||||||
|  | 	if strings.ToLower(tok) == "@start" { | ||||||
|  | 		return Start(), nil | ||||||
|  | 	} | ||||||
|  | 	if strings.ToLower(tok) == "@end" { | ||||||
|  | 		return End(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !strings.HasPrefix(tok, "tok_") { | ||||||
|  | 		return CursorToken{}, errors.New("could not decode token, missing prefix") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	body, err := base32.StdEncoding.DecodeString(tok[len("tok_"):]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return CursorToken{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var tokenDeserialize cursorTokenSerialize | ||||||
|  | 	err = json.Unmarshal(body, &tokenDeserialize) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return CursorToken{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	token := CursorToken{Mode: CTMNormal} | ||||||
|  |  | ||||||
|  | 	if tokenDeserialize.ValuePrimary != nil { | ||||||
|  | 		token.ValuePrimary = *tokenDeserialize.ValuePrimary | ||||||
|  | 	} | ||||||
|  | 	if tokenDeserialize.ValueSecondary != nil { | ||||||
|  | 		token.ValueSecondary = *tokenDeserialize.ValueSecondary | ||||||
|  | 	} | ||||||
|  | 	if tokenDeserialize.Direction != nil { | ||||||
|  | 		token.Direction = *tokenDeserialize.Direction | ||||||
|  | 	} | ||||||
|  | 	if tokenDeserialize.DirectionSecondary != nil { | ||||||
|  | 		token.DirectionSecondary = *tokenDeserialize.DirectionSecondary | ||||||
|  | 	} | ||||||
|  | 	if tokenDeserialize.PageSize != nil { | ||||||
|  | 		token.PageSize = *tokenDeserialize.PageSize | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	token.Extra.Timestamp = tokenDeserialize.ExtraTimestamp | ||||||
|  | 	token.Extra.Id = tokenDeserialize.ExtraId | ||||||
|  | 	token.Extra.Page = tokenDeserialize.ExtraPage | ||||||
|  | 	token.Extra.PageSize = tokenDeserialize.ExtraPageSize | ||||||
|  |  | ||||||
|  | 	return token, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *CursorToken) ValuePrimaryObjectId() (primitive.ObjectID, bool) { | ||||||
|  | 	if oid, err := primitive.ObjectIDFromHex(c.ValuePrimary); err == nil { | ||||||
|  | 		return oid, true | ||||||
|  | 	} else { | ||||||
|  | 		return primitive.ObjectID{}, false | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *CursorToken) ValueSecondaryObjectId() (primitive.ObjectID, bool) { | ||||||
|  | 	if oid, err := primitive.ObjectIDFromHex(c.ValueSecondary); err == nil { | ||||||
|  | 		return oid, true | ||||||
|  | 	} else { | ||||||
|  | 		return primitive.ObjectID{}, false | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								ginext/apiError.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								ginext/apiError.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | type apiError struct { | ||||||
|  | 	ErrorCode        string  `json:"errorcode"` | ||||||
|  | 	Message          string  `json:"message"` | ||||||
|  | 	FAPIErrorMessage *string `json:"fapiMessage,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type extAPIError struct { | ||||||
|  | 	ErrorCode        string  `json:"errorcode"` | ||||||
|  | 	Message          string  `json:"message"` | ||||||
|  | 	FAPIErrorMessage *string `json:"fapiMessage,omitempty"` | ||||||
|  |  | ||||||
|  | 	RawError *string  `json:"__error"` | ||||||
|  | 	Trace    []string `json:"__trace"` | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								ginext/appContext.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								ginext/appContext.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type AppContext struct { | ||||||
|  | 	inner      context.Context | ||||||
|  | 	cancelFunc context.CancelFunc | ||||||
|  | 	cancelled  bool | ||||||
|  | 	GinContext *gin.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func CreateAppContext(g *gin.Context, innerCtx context.Context, cancelFn context.CancelFunc) *AppContext { | ||||||
|  | 	return &AppContext{ | ||||||
|  | 		inner:      innerCtx, | ||||||
|  | 		cancelFunc: cancelFn, | ||||||
|  | 		cancelled:  false, | ||||||
|  | 		GinContext: g, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) Deadline() (deadline time.Time, ok bool) { | ||||||
|  | 	return ac.inner.Deadline() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) Done() <-chan struct{} { | ||||||
|  | 	return ac.inner.Done() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) Err() error { | ||||||
|  | 	return ac.inner.Err() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) Value(key any) any { | ||||||
|  | 	return ac.inner.Value(key) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) Cancel() { | ||||||
|  | 	ac.cancelled = true | ||||||
|  | 	ac.cancelFunc() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) RequestURI() string { | ||||||
|  | 	if ac.GinContext != nil && ac.GinContext.Request != nil { | ||||||
|  | 		return ac.GinContext.Request.Method + " :: " + ac.GinContext.Request.RequestURI | ||||||
|  | 	} else { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ac *AppContext) FinishSuccess(res HTTPResponse) HTTPResponse { | ||||||
|  | 	if ac.cancelled { | ||||||
|  | 		panic("Cannot finish a cancelled request") | ||||||
|  | 	} | ||||||
|  | 	return res | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								ginext/commonApiErr/enums.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ginext/commonApiErr/enums.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | package commonApiErr | ||||||
|  |  | ||||||
|  | type APIErrorCode struct { | ||||||
|  | 	HTTPStatusCode int | ||||||
|  | 	Key            string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //goland:noinspection GoSnakeCaseUsage | ||||||
|  | var ( | ||||||
|  | 	NotImplemented = APIErrorCode{500, "NOT_IMPLEMENTED"} | ||||||
|  | 	InternalError  = APIErrorCode{500, "INTERNAL_ERROR"} | ||||||
|  | 	Panic          = APIErrorCode{500, "PANIC"} | ||||||
|  |  | ||||||
|  | 	BindFailURI      = APIErrorCode{400, "BINDFAIL_URI"} | ||||||
|  | 	BindFailQuery    = APIErrorCode{400, "BINDFAIL_QUERY"} | ||||||
|  | 	BindFailJSON     = APIErrorCode{400, "BINDFAIL_JSON"} | ||||||
|  | 	BindFailFormData = APIErrorCode{400, "BINDFAIL_FORMDATA"} | ||||||
|  |  | ||||||
|  | 	Unauthorized = APIErrorCode{401, "UNAUTHORIZED"} | ||||||
|  | 	AuthFailed   = APIErrorCode{401, "AUTH_FAILED"} | ||||||
|  | ) | ||||||
							
								
								
									
										23
									
								
								ginext/commonHandler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								ginext/commonHandler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func RedirectFound(newuri string) WHandlerFunc { | ||||||
|  | 	return func(pctx PreContext) HTTPResponse { | ||||||
|  | 		return Redirect(http.StatusFound, newuri) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RedirectTemporary(newuri string) WHandlerFunc { | ||||||
|  | 	return func(pctx PreContext) HTTPResponse { | ||||||
|  | 		return Redirect(http.StatusTemporaryRedirect, newuri) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RedirectPermanent(newuri string) WHandlerFunc { | ||||||
|  | 	return func(pctx PreContext) HTTPResponse { | ||||||
|  | 		return Redirect(http.StatusPermanentRedirect, newuri) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								ginext/cors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ginext/cors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func CorsMiddleware() gin.HandlerFunc { | ||||||
|  | 	return func(c *gin.Context) { | ||||||
|  | 		c.Writer.Header().Set("Access-Control-Allow-Origin", "*") | ||||||
|  | 		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") | ||||||
|  | 		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") | ||||||
|  | 		c.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE, COUNT") | ||||||
|  |  | ||||||
|  | 		if c.Request.Method == "OPTIONS" { | ||||||
|  | 			c.AbortWithStatus(http.StatusOK) | ||||||
|  | 		} else { | ||||||
|  | 			c.Next() | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								ginext/engine.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ginext/engine.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type GinWrapper struct { | ||||||
|  | 	engine          *gin.Engine | ||||||
|  | 	SuppressGinLogs bool | ||||||
|  |  | ||||||
|  | 	allowCors       bool | ||||||
|  | 	ginDebug        bool | ||||||
|  | 	returnRawErrors bool | ||||||
|  | 	requestTimeout  time.Duration | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewEngine(allowCors bool, ginDebug bool, returnRawErrors bool, timeout time.Duration) *GinWrapper { | ||||||
|  | 	engine := gin.New() | ||||||
|  |  | ||||||
|  | 	wrapper := &GinWrapper{ | ||||||
|  | 		engine:          engine, | ||||||
|  | 		SuppressGinLogs: false, | ||||||
|  | 		allowCors:       allowCors, | ||||||
|  | 		ginDebug:        ginDebug, | ||||||
|  | 		returnRawErrors: returnRawErrors, | ||||||
|  | 		requestTimeout:  timeout, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	engine.RedirectFixedPath = false | ||||||
|  | 	engine.RedirectTrailingSlash = false | ||||||
|  |  | ||||||
|  | 	if allowCors { | ||||||
|  | 		engine.Use(CorsMiddleware()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ginDebug { | ||||||
|  | 		ginlogger := gin.Logger() | ||||||
|  | 		engine.Use(func(context *gin.Context) { | ||||||
|  | 			if !wrapper.SuppressGinLogs { | ||||||
|  | 				ginlogger(context) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return wrapper | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinWrapper) ServeHTTP(writer http.ResponseWriter, request *http.Request) { | ||||||
|  | 	w.engine.ServeHTTP(writer, request) | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								ginext/funcWrapper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								ginext/funcWrapper.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/rs/zerolog/log" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type WHandlerFunc func(PreContext) HTTPResponse | ||||||
|  |  | ||||||
|  | func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc { | ||||||
|  |  | ||||||
|  | 	return func(g *gin.Context) { | ||||||
|  |  | ||||||
|  | 		g.Set("__returnRawErrors", w.returnRawErrors) | ||||||
|  |  | ||||||
|  | 		reqctx := g.Request.Context() | ||||||
|  |  | ||||||
|  | 		wrap, stackTrace, panicObj := callPanicSafe(fn, PreContext{wrapper: w, ginCtx: g}) | ||||||
|  | 		if panicObj != nil { | ||||||
|  | 			fmt.Printf("\n======== ======== STACKTRACE ======== ========\n%s\n======== ======== ======== ========\n\n", stackTrace) | ||||||
|  | 			log.Error(). | ||||||
|  | 				Interface("panicObj", panicObj). | ||||||
|  | 				Str("trace", stackTrace). | ||||||
|  | 				Msg("Panic occured (in gin handler)") | ||||||
|  | 			wrap = APIError(g, commonApiErr.Panic, "A panic occured in the HTTP handler", errors.New(fmt.Sprintf("%+v", panicObj))) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if g.Writer.Written() { | ||||||
|  | 			panic("Writing in WrapperFunc is not supported") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if reqctx.Err() == nil { | ||||||
|  | 			wrap.Write(g) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								ginext/preContext.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								ginext/preContext.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/gin-gonic/gin/binding" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"runtime/debug" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type PreContext struct { | ||||||
|  | 	ginCtx  *gin.Context | ||||||
|  | 	wrapper *GinWrapper | ||||||
|  | 	uri     any | ||||||
|  | 	query   any | ||||||
|  | 	body    any | ||||||
|  | 	form    any | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pctx *PreContext) URI(uri any) *PreContext { | ||||||
|  | 	pctx.uri = uri | ||||||
|  | 	return pctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pctx *PreContext) Query(query any) *PreContext { | ||||||
|  | 	pctx.query = query | ||||||
|  | 	return pctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pctx *PreContext) Body(body any) *PreContext { | ||||||
|  | 	pctx.body = body | ||||||
|  | 	return pctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pctx *PreContext) Form(form any) *PreContext { | ||||||
|  | 	pctx.form = form | ||||||
|  | 	return pctx | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) { | ||||||
|  | 	if pctx.uri != nil { | ||||||
|  | 		if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil { | ||||||
|  | 			return nil, nil, langext.Ptr(APIError(pctx.ginCtx, commonApiErr.BindFailURI, "Failed to read uri", err)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if pctx.query != nil { | ||||||
|  | 		if err := pctx.ginCtx.ShouldBindQuery(pctx.query); err != nil { | ||||||
|  | 			return nil, nil, langext.Ptr(APIError(pctx.ginCtx, commonApiErr.BindFailQuery, "Failed to read query", err)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if pctx.body != nil { | ||||||
|  | 		if pctx.ginCtx.ContentType() == "application/json" { | ||||||
|  | 			if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil { | ||||||
|  | 				return nil, nil, langext.Ptr(APIError(pctx.ginCtx, commonApiErr.BindFailJSON, "Failed to read body", err)) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			return nil, nil, langext.Ptr(APIError(pctx.ginCtx, commonApiErr.BindFailJSON, "missing JSON body", nil)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if pctx.form != nil { | ||||||
|  | 		if pctx.ginCtx.ContentType() == "multipart/form-data" { | ||||||
|  | 			if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil { | ||||||
|  | 				return nil, nil, langext.Ptr(APIError(pctx.ginCtx, commonApiErr.BindFailFormData, "Failed to read multipart-form", err)) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			return nil, nil, langext.Ptr(APIError(pctx.ginCtx, commonApiErr.BindFailJSON, "missing form body", nil)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ictx, cancel := context.WithTimeout(context.Background(), pctx.wrapper.requestTimeout) | ||||||
|  | 	actx := CreateAppContext(pctx.ginCtx, ictx, cancel) | ||||||
|  |  | ||||||
|  | 	return actx, pctx.ginCtx, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func callPanicSafe(fn WHandlerFunc, pctx PreContext) (res HTTPResponse, stackTrace string, panicObj any) { | ||||||
|  | 	defer func() { | ||||||
|  | 		if rec := recover(); rec != nil { | ||||||
|  | 			res = nil | ||||||
|  | 			stackTrace = string(debug.Stack()) | ||||||
|  | 			panicObj = rec | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	res = fn(pctx) | ||||||
|  | 	return res, "", nil | ||||||
|  | } | ||||||
							
								
								
									
										153
									
								
								ginext/response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								ginext/response.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/rs/zerolog/log" | ||||||
|  | 	json "gogs.mikescher.com/BlackForestBytes/goext/gojson" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"runtime/debug" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type HTTPResponse interface { | ||||||
|  | 	Write(g *gin.Context) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type jsonHTTPResponse struct { | ||||||
|  | 	statusCode int | ||||||
|  | 	data       any | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j jsonHTTPResponse) Write(g *gin.Context) { | ||||||
|  | 	g.Render(j.statusCode, json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type emptyHTTPResponse struct { | ||||||
|  | 	statusCode int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j emptyHTTPResponse) Write(g *gin.Context) { | ||||||
|  | 	g.Status(j.statusCode) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type textHTTPResponse struct { | ||||||
|  | 	statusCode int | ||||||
|  | 	data       string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j textHTTPResponse) Write(g *gin.Context) { | ||||||
|  | 	g.String(j.statusCode, "%s", j.data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type dataHTTPResponse struct { | ||||||
|  | 	statusCode  int | ||||||
|  | 	data        []byte | ||||||
|  | 	contentType string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j dataHTTPResponse) Write(g *gin.Context) { | ||||||
|  | 	g.Data(j.statusCode, j.contentType, j.data) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type fileHTTPResponse struct { | ||||||
|  | 	mimetype string | ||||||
|  | 	filepath string | ||||||
|  | 	filename *string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j fileHTTPResponse) 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)) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | 	g.File(j.filepath) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type redirectHTTPResponse struct { | ||||||
|  | 	statusCode int | ||||||
|  | 	url        string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (j redirectHTTPResponse) Write(g *gin.Context) { | ||||||
|  | 	g.Redirect(j.statusCode, j.url) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Status(sc int) HTTPResponse { | ||||||
|  | 	return &emptyHTTPResponse{statusCode: sc} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func JSON(sc int, data any) HTTPResponse { | ||||||
|  | 	return &jsonHTTPResponse{statusCode: sc, data: data} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Data(sc int, contentType string, data []byte) HTTPResponse { | ||||||
|  | 	return &dataHTTPResponse{statusCode: sc, contentType: contentType, data: data} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Text(sc int, data string) HTTPResponse { | ||||||
|  | 	return &textHTTPResponse{statusCode: sc, data: data} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func File(mimetype string, filepath string) HTTPResponse { | ||||||
|  | 	return &fileHTTPResponse{mimetype: mimetype, filepath: filepath} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Download(mimetype string, filepath string, filename string) HTTPResponse { | ||||||
|  | 	return &fileHTTPResponse{mimetype: mimetype, filepath: filepath, filename: &filename} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Redirect(sc int, newURL string) HTTPResponse { | ||||||
|  | 	return &redirectHTTPResponse{statusCode: sc, url: newURL} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func APIError(g *gin.Context, errcode commonApiErr.APIErrorCode, msg string, e error) HTTPResponse { | ||||||
|  | 	return createApiError(g, errcode, msg, e) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NotImplemented(g *gin.Context) HTTPResponse { | ||||||
|  | 	return createApiError(g, commonApiErr.NotImplemented, "", nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func createApiError(g *gin.Context, errcode commonApiErr.APIErrorCode, msg string, e error) HTTPResponse { | ||||||
|  | 	reqUri := "" | ||||||
|  | 	if g != nil && g.Request != nil { | ||||||
|  | 		reqUri = g.Request.Method + " :: " + g.Request.RequestURI | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Error(). | ||||||
|  | 		Str("errorcode.key", errcode.Key). | ||||||
|  | 		Int("errcode.status", errcode.HTTPStatusCode). | ||||||
|  | 		Str("uri", reqUri). | ||||||
|  | 		AnErr("err", e). | ||||||
|  | 		Stack(). | ||||||
|  | 		Msg(msg) | ||||||
|  |  | ||||||
|  | 	var fapiMessage *string = nil | ||||||
|  | 	if v, ok := e.(interface{ FAPIMessage() string }); ok { | ||||||
|  | 		fapiMessage = langext.Ptr(v.FAPIMessage()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if g.GetBool("__returnRawErrors") { | ||||||
|  | 		return &jsonHTTPResponse{ | ||||||
|  | 			statusCode: errcode.HTTPStatusCode, | ||||||
|  | 			data: extAPIError{ | ||||||
|  | 				ErrorCode:        errcode.Key, | ||||||
|  | 				Message:          msg, | ||||||
|  | 				RawError:         langext.Ptr(langext.Conditional(e == nil, "", fmt.Sprintf("%+v", e))), | ||||||
|  | 				FAPIErrorMessage: fapiMessage, | ||||||
|  | 				Trace:            strings.Split(string(debug.Stack()), "\n"), | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		return &jsonHTTPResponse{ | ||||||
|  | 			statusCode: errcode.HTTPStatusCode, | ||||||
|  | 			data: apiError{ | ||||||
|  | 				ErrorCode:        errcode.Key, | ||||||
|  | 				Message:          msg, | ||||||
|  | 				FAPIErrorMessage: fapiMessage, | ||||||
|  | 			}, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										85
									
								
								ginext/routes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								ginext/routes.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | package ginext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var anyMethods = []string{ | ||||||
|  | 	http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, | ||||||
|  | 	http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect, | ||||||
|  | 	http.MethodTrace, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GinRoutesWrapper struct { | ||||||
|  | 	wrapper *GinWrapper | ||||||
|  | 	routes  gin.IRouter | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GinRouteBuilder struct { | ||||||
|  | 	routes *GinRoutesWrapper | ||||||
|  |  | ||||||
|  | 	methods  []string | ||||||
|  | 	relPath  string | ||||||
|  | 	handlers []gin.HandlerFunc | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinWrapper) Routes() *GinRoutesWrapper { | ||||||
|  | 	return &GinRoutesWrapper{wrapper: w, routes: w.engine} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper { | ||||||
|  | 	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes.Group(relativePath)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodGet}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) POST(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodPost}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) DELETE(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodDelete}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) PATCH(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodPatch}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) PUT(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodPut}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) OPTIONS(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodOptions}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) HEAD(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{http.MethodHead}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) COUNT(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: []string{"COUNT"}, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRoutesWrapper) Any(relativePath string) *GinRouteBuilder { | ||||||
|  | 	return &GinRouteBuilder{routes: w, methods: anyMethods, relPath: relativePath, handlers: make([]gin.HandlerFunc, 0)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder { | ||||||
|  | 	w.handlers = append(w.handlers, middleware...) | ||||||
|  | 	return w | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinRouteBuilder) Handle(handler WHandlerFunc) { | ||||||
|  | 	w.handlers = append(w.handlers, Wrap(w.routes.wrapper, handler)) | ||||||
|  | 	for _, m := range w.methods { | ||||||
|  | 		w.routes.routes.Handle(m, w.relPath, w.handlers...) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (w *GinWrapper) NoRoute(handler WHandlerFunc) { | ||||||
|  | 	w.engine.NoRoute(Wrap(w, handler)) | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,12 +3,47 @@ module gogs.mikescher.com/BlackForestBytes/goext | |||||||
| go 1.19 | go 1.19 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	golang.org/x/sys v0.3.0 | 	github.com/gin-gonic/gin v1.9.1 | ||||||
| 	golang.org/x/term v0.3.0 | 	github.com/jmoiron/sqlx v1.3.5 | ||||||
|  | 	github.com/rs/zerolog v1.29.1 | ||||||
|  | 	go.mongodb.org/mongo-driver v1.12.0 | ||||||
|  | 	golang.org/x/crypto v0.11.0 | ||||||
|  | 	golang.org/x/sys v0.10.0 | ||||||
|  | 	golang.org/x/term v0.10.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/jmoiron/sqlx v1.3.5 // indirect | 	github.com/bytedance/sonic v1.10.0-rc2 // indirect | ||||||
| 	go.mongodb.org/mongo-driver v1.11.1 // indirect | 	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect | ||||||
| 	golang.org/x/crypto v0.4.0 // indirect | 	github.com/chenzhuoyu/iasm v0.9.0 // indirect | ||||||
|  | 	github.com/gabriel-vasile/mimetype v1.4.2 // 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.14.1 // indirect | ||||||
|  | 	github.com/goccy/go-json v0.10.2 // indirect | ||||||
|  | 	github.com/golang/snappy v0.0.4 // indirect | ||||||
|  | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
|  | 	github.com/klauspost/compress v1.16.7 // indirect | ||||||
|  | 	github.com/klauspost/cpuid/v2 v2.2.5 // indirect | ||||||
|  | 	github.com/leodido/go-urn v1.2.4 // indirect | ||||||
|  | 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||||
|  | 	github.com/mattn/go-isatty v0.0.19 // 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.0.9 // indirect | ||||||
|  | 	github.com/pkg/errors v0.9.1 // indirect | ||||||
|  | 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||||
|  | 	github.com/ugorji/go/codec v1.2.11 // 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.4.0 // indirect | ||||||
|  | 	golang.org/x/net v0.12.0 // indirect | ||||||
|  | 	golang.org/x/sync v0.3.0 // indirect | ||||||
|  | 	golang.org/x/text v0.11.0 // indirect | ||||||
|  | 	google.golang.org/protobuf v1.31.0 // indirect | ||||||
|  | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										198
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										198
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,50 +1,240 @@ | |||||||
|  | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= | ||||||
|  | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= | ||||||
|  | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= | ||||||
|  | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= | ||||||
|  | github.com/bytedance/sonic v1.10.0-rc2 h1:oDfRZ+4m6AYCOC0GFeOCeYqvBmucy1isvouS2K0cPzo= | ||||||
|  | github.com/bytedance/sonic v1.10.0-rc2/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 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= | ||||||
|  | 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= | ||||||
|  | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= | ||||||
|  | github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= | ||||||
|  | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= | ||||||
|  | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
|  | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= | ||||||
|  | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= | ||||||
|  | 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/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||||
|  | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||||
|  | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||||
|  | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||||
|  | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= | ||||||
|  | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||||
|  | github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= | ||||||
|  | github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= | ||||||
|  | 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/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 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= | ||||||
| github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||||
|  | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= | ||||||
|  | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||||
|  | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= | ||||||
| github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | 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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= | ||||||
| github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= | ||||||
|  | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= | ||||||
|  | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||||
|  | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= | ||||||
| github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= | ||||||
|  | github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= | ||||||
|  | github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||||
|  | github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= | ||||||
|  | github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||||
|  | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= | ||||||
|  | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= | ||||||
|  | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||||
|  | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= | ||||||
|  | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||||
|  | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||||
|  | 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/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= | ||||||
| github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||||
|  | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= | ||||||
|  | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||||||
|  | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||||
|  | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||||||
|  | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||||
|  | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||||
|  | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= | ||||||
|  | github.com/mattn/go-isatty v0.0.19/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.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||||
|  | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
|  | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= | ||||||
|  | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
|  | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||||
|  | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||||
|  | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= | ||||||
| github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= | ||||||
|  | github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= | ||||||
|  | github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= | ||||||
|  | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= | ||||||
|  | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= | ||||||
|  | github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= | ||||||
|  | github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | ||||||
|  | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
|  | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||||
|  | github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= | ||||||
|  | github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | 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= | ||||||
|  | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
|  | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= | ||||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
|  | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
|  | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
|  | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
|  | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
|  | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= | ||||||
| github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= | ||||||
|  | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= | ||||||
|  | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
|  | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= | ||||||
|  | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||||
|  | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= | ||||||
| github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= | ||||||
|  | github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= | ||||||
| github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= | ||||||
|  | github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= | ||||||
|  | github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= | ||||||
|  | github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= | ||||||
| github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= | ||||||
|  | github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= | ||||||
|  | github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= | ||||||
|  | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= | ||||||
| github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= | ||||||
|  | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= | ||||||
|  | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= | ||||||
|  | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
| go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= | go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= | ||||||
| go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= | go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= | ||||||
|  | go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs= | ||||||
|  | go.mongodb.org/mongo-driver v1.11.7/go.mod h1:G9TgswdsWjX4tmDA5zfs2+6AEPpYJwqblyjsfuh8oXY= | ||||||
|  | go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= | ||||||
|  | go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= | ||||||
|  | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
|  | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= | ||||||
|  | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
|  | golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= | ||||||
|  | golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||||
|  | 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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= | ||||||
| golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= | golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= | ||||||
| golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= | golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= | ||||||
|  | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= | ||||||
|  | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= | ||||||
|  | golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= | ||||||
|  | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= | ||||||
|  | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= | ||||||
|  | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= | ||||||
|  | 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= | ||||||
|  | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
| golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||||
|  | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
|  | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= | ||||||
|  | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||||
|  | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= | ||||||
|  | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= | ||||||
|  | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= | ||||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= | ||||||
|  | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= | ||||||
|  | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= | ||||||
|  | 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= | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= | ||||||
| golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | 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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= | ||||||
|  | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= | ||||||
|  | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= | ||||||
|  | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
| golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
| golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |  | ||||||
| golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= | golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= | ||||||
| golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= | ||||||
|  | golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= | ||||||
|  | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||||
|  | golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= | ||||||
|  | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= | ||||||
|  | golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= | ||||||
|  | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= | ||||||
|  | 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= | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||||
|  | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= | ||||||
|  | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= | ||||||
|  | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||||
|  | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||||
|  | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= | ||||||
|  | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||||
|  | golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= | ||||||
|  | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||||
|  | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= | ||||||
|  | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||||
|  | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= | ||||||
|  | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||||
|  | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= | ||||||
|  | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | 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= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|  | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= | ||||||
|  | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								goextVersion.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								goextVersion.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | package goext | ||||||
|  |  | ||||||
|  | const GoextVersion = "0.0.182" | ||||||
|  |  | ||||||
|  | const GoextVersionTimestamp = "2023-07-19T11:26:18+0200" | ||||||
| @@ -1,57 +0,0 @@ | |||||||
| package mongoext |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"go.mongodb.org/mongo-driver/mongo" |  | ||||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rfctime" |  | ||||||
| 	"inopart/backend/common/cursortoken" |  | ||||||
| 	"inopart/backend/models" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func HandleMongoCursorWithToken[T models.Entity](ctx context.Context, cursor *mongo.Cursor, pageSize *int) ([]T, string, error) { |  | ||||||
| 	var tokenStr string |  | ||||||
| 	var docs []T |  | ||||||
| 	numDocs := cursor.RemainingBatchLength() |  | ||||||
|  |  | ||||||
| 	if pageSize == nil || (numDocs <= *pageSize) && numDocs > 0 { |  | ||||||
| 		docs = make([]T, numDocs) |  | ||||||
|  |  | ||||||
| 		err := cursor.All(ctx, &docs) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return []T{}, "", err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} else if numDocs == *pageSize+1 { |  | ||||||
|  |  | ||||||
| 		docs = make([]T, numDocs-1) |  | ||||||
|  |  | ||||||
| 		for i := 0; i < numDocs-1; i++ { |  | ||||||
| 			cursor.Next(ctx) |  | ||||||
| 			err := cursor.Decode(&docs[i]) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return []T{}, "", err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		var nextPageDoc T |  | ||||||
| 		cursor.Next(ctx) |  | ||||||
| 		err := cursor.Decode(&nextPageDoc) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return []T{}, "", err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		nextPageToken := cursortoken.CursorToken{ |  | ||||||
| 			Timestamp: rfctime.RFC3339NanoTime{}, |  | ||||||
| 			Id:        nextPageDoc.GetId(), |  | ||||||
| 			Offset:    0, |  | ||||||
| 			PageSize:  *pageSize, |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		tokenStr, err = nextPageToken.Token() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, "", err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return docs, tokenStr, nil |  | ||||||
| } |  | ||||||
| @@ -16,6 +16,9 @@ func CreateGoExtBsonRegistry() *bsoncodec.Registry { | |||||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{}) | 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{}) | ||||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{}) | 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.RFC3339NanoTime{}), rfctime.RFC3339NanoTime{}) | ||||||
|  |  | ||||||
|  | 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.Date{}), rfctime.Date{}) | ||||||
|  | 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.Date{}), rfctime.Date{}) | ||||||
|  |  | ||||||
| 	bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb) | 	bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb) | ||||||
| 	bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb) | 	bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| package langext | package reflectext | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"reflect" | 	"reflect" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var reflectBasicTypes = []reflect.Type{ | var reflectBasicTypes = map[reflect.Kind]reflect.Type{ | ||||||
| 	reflect.Bool:       reflect.TypeOf(false), | 	reflect.Bool:       reflect.TypeOf(false), | ||||||
| 	reflect.Int:        reflect.TypeOf(int(0)), | 	reflect.Int:        reflect.TypeOf(int(0)), | ||||||
| 	reflect.Int8:       reflect.TypeOf(int8(0)), | 	reflect.Int8:       reflect.TypeOf(int8(0)), | ||||||
| @@ -109,3 +109,28 @@ func TryCast[T any](v any) (T, bool) { | |||||||
| 
 | 
 | ||||||
| 	return r4, true | 	return r4, true | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TryCastType(v any, dest reflect.Type) (any, bool) { | ||||||
|  | 
 | ||||||
|  | 	underlying := Underlying(reflect.TypeOf(v)) | ||||||
|  | 
 | ||||||
|  | 	if underlying != Underlying(dest) { | ||||||
|  | 		return nil, false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r1 := reflect.ValueOf(v) | ||||||
|  | 
 | ||||||
|  | 	if !r1.CanConvert(underlying) { | ||||||
|  | 		return nil, false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r2 := r1.Convert(underlying) | ||||||
|  | 
 | ||||||
|  | 	if !r2.CanConvert(dest) { | ||||||
|  | 		return nil, false | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r4 := r2.Convert(dest) | ||||||
|  | 
 | ||||||
|  | 	return r4.Interface(), true | ||||||
|  | } | ||||||
							
								
								
									
										136
									
								
								reflectext/primStrSer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								reflectext/primStrSer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | |||||||
|  | package reflectext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var primitiveSerializer = map[reflect.Type]genSerializer{ | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(""): newGenSerializer(serStringToString, serStringToString), | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(int(0)):   newGenSerializer(serIntNumToString[int], serStringToSIntNum[int]), | ||||||
|  | 	reflect.TypeOf(int32(0)): newGenSerializer(serIntNumToString[int32], serStringToSIntNum[int32]), | ||||||
|  | 	reflect.TypeOf(int64(0)): newGenSerializer(serIntNumToString[int64], serStringToSIntNum[int64]), | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(uint(0)):   newGenSerializer(serIntNumToString[uint], serStringToUIntNum[uint]), | ||||||
|  | 	reflect.TypeOf(uint32(0)): newGenSerializer(serIntNumToString[uint32], serStringToUIntNum[uint32]), | ||||||
|  | 	reflect.TypeOf(uint64(0)): newGenSerializer(serIntNumToString[uint64], serStringToUIntNum[uint64]), | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(float32(0)): newGenSerializer(serFloatNumToString[float32], serStringToFloatNum[float32]), | ||||||
|  | 	reflect.TypeOf(float64(0)): newGenSerializer(serFloatNumToString[float64], serStringToFloatNum[float64]), | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(true): newGenSerializer(serBoolToString, serStringToBool), | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(primitive.ObjectID{}): newGenSerializer(serObjectIDToString, serStringToObjectID), | ||||||
|  |  | ||||||
|  | 	reflect.TypeOf(time.Time{}): newGenSerializer(serTimeToString, serStringToTime), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type genSerializer struct { | ||||||
|  | 	ToString   func(v any) (string, error) | ||||||
|  | 	FromString func(v string) (any, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newGenSerializer[TData any](tostr func(v TData) (string, error), fromstr func(v string) (TData, error)) genSerializer { | ||||||
|  | 	return genSerializer{ | ||||||
|  | 		ToString: func(v any) (string, error) { | ||||||
|  | 			if tdv, ok := v.(TData); ok { | ||||||
|  | 				rv, err := tostr(tdv) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return "", err | ||||||
|  | 				} | ||||||
|  | 				return rv, nil | ||||||
|  | 			} else { | ||||||
|  | 				return "", errors.New(fmt.Sprintf("cannot convert type %T to TData (%T)", v, *new(TData))) | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		FromString: func(v string) (any, error) { | ||||||
|  | 			nv, err := fromstr(v) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return "", err | ||||||
|  | 			} | ||||||
|  | 			return nv, nil | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToString(v string) (string, error) { | ||||||
|  | 	return v, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serIntNumToString[TNum langext.IntegerConstraint](v TNum) (string, error) { | ||||||
|  | 	return strconv.FormatInt(int64(v), 10), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToSIntNum[TNum langext.SignedConstraint](v string) (TNum, error) { | ||||||
|  | 	r, err := strconv.ParseInt(v, 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return TNum(r), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToUIntNum[TNum langext.UnsignedConstraint](v string) (TNum, error) { | ||||||
|  | 	r, err := strconv.ParseUint(v, 10, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return TNum(r), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serFloatNumToString[TNum langext.FloatConstraint](v TNum) (string, error) { | ||||||
|  | 	return strconv.FormatFloat(float64(v), 'f', -1, 64), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToFloatNum[TNum langext.FloatConstraint](v string) (TNum, error) { | ||||||
|  | 	r, err := strconv.ParseFloat(v, 64) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 	return TNum(r), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serBoolToString(v bool) (string, error) { | ||||||
|  | 	return langext.Conditional(v, "true", "false"), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToBool(v string) (bool, error) { | ||||||
|  | 	if strings.ToLower(v) == "true" { | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 	if strings.ToLower(v) == "false" { | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 	return false, errors.New(fmt.Sprintf("invalid boolean value '%s'", v)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serObjectIDToString(v primitive.ObjectID) (string, error) { | ||||||
|  | 	return v.Hex(), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToObjectID(v string) (primitive.ObjectID, error) { | ||||||
|  | 	if rv, err := primitive.ObjectIDFromHex(v); err == nil { | ||||||
|  | 		return rv, nil | ||||||
|  | 	} else { | ||||||
|  | 		return primitive.ObjectID{}, err | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serTimeToString(v time.Time) (string, error) { | ||||||
|  | 	return v.Format(time.RFC3339Nano), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func serStringToTime(v string) (time.Time, error) { | ||||||
|  | 	if rv, err := time.Parse(time.RFC3339Nano, v); err == nil { | ||||||
|  | 		return rv, nil | ||||||
|  | 	} else { | ||||||
|  | 		return time.Time{}, err | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										92
									
								
								reflectext/primitiveStringSerializer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								reflectext/primitiveStringSerializer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | package reflectext | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"reflect" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // PrimitiveStringSerializer is used to serialize primitive types (and a few more) from and to string | ||||||
|  | // This is not really intended to be user facing, and more as a simple building block for other mechanisms | ||||||
|  | // supports: | ||||||
|  | //   - golang primitives (ints, uints, floats, bool, string) | ||||||
|  | //   - type aliases | ||||||
|  | //   - time.Time | ||||||
|  | //   - primitive.ObjectID | ||||||
|  | type PrimitiveStringSerializer struct{} | ||||||
|  |  | ||||||
|  | func (pss PrimitiveStringSerializer) ValueToString(v any) (string, error) { | ||||||
|  |  | ||||||
|  | 	inType := reflect.TypeOf(v) | ||||||
|  |  | ||||||
|  | 	if inType.Kind() == reflect.Ptr && langext.IsNil(v) { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if inType.Kind() == reflect.Ptr { | ||||||
|  | 		rval1 := reflect.ValueOf(v) | ||||||
|  | 		rval2 := rval1.Elem() | ||||||
|  | 		rval3 := rval2.Interface() | ||||||
|  | 		return pss.ValueToString(rval3) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if conv, ok := primitiveSerializer[inType]; ok { | ||||||
|  | 		return conv.ToString(v) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for convType, conv := range primitiveSerializer { | ||||||
|  | 		if castV, ok := TryCastType(v, convType); ok { | ||||||
|  | 			return conv.ToString(castV) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", errors.New(fmt.Sprintf("failed to find a matching generic <toString> conversion fo type %T", v)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pss PrimitiveStringSerializer) ValueFromString(str string, outType reflect.Type) (any, error) { | ||||||
|  |  | ||||||
|  | 	if outType.Kind() == reflect.Ptr && str == "" { | ||||||
|  | 		return reflect.Zero(outType).Interface(), nil // = nil.(outType), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if str == "" { | ||||||
|  | 		return reflect.Zero(outType).Interface(), nil // = <default>(outType), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if outType.Kind() == reflect.Ptr { | ||||||
|  |  | ||||||
|  | 		innerValue, err := pss.ValueFromString(str, outType.Elem()) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// this weird piece of shit converts innerValue to &innerValue  (while keeping types) | ||||||
|  |  | ||||||
|  | 		rval1 := reflect.ValueOf(innerValue) | ||||||
|  | 		rval2 := rval1.Convert(outType.Elem()) | ||||||
|  | 		rval3 := reflect.New(outType.Elem()) | ||||||
|  | 		rval3.Elem().Set(rval2) | ||||||
|  | 		rval4 := rval3.Interface() | ||||||
|  |  | ||||||
|  | 		return rval4, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if conv, ok := primitiveSerializer[outType]; ok { | ||||||
|  | 		return conv.FromString(str) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	emptyResultVal := reflect.Zero(outType).Interface() | ||||||
|  |  | ||||||
|  | 	for convType, conv := range primitiveSerializer { | ||||||
|  | 		if _, ok := TryCastType(emptyResultVal, convType); ok { | ||||||
|  | 			if convVal, err := conv.FromString(str); err == nil { | ||||||
|  | 				if resVal, ok := TryCastType(convVal, outType); ok { | ||||||
|  | 					return resVal, nil | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", errors.New(fmt.Sprintf("failed to find a matching generic <toString> conversion fo type %s", outType.String())) | ||||||
|  | } | ||||||
							
								
								
									
										240
									
								
								rfctime/date.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								rfctime/date.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,240 @@ | |||||||
|  | package rfctime | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/bsoncodec" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/bsonrw" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/bsontype" | ||||||
|  | 	"reflect" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Date struct { | ||||||
|  | 	Year  int | ||||||
|  | 	Month int | ||||||
|  | 	Day   int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) Time(loc *time.Location) time.Time { | ||||||
|  | 	return time.Date(t.Year, time.Month(t.Month), t.Day, 0, 0, 0, 0, loc) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) TimeUTC() time.Time { | ||||||
|  | 	return time.Date(t.Year, time.Month(t.Month), t.Day, 0, 0, 0, 0, time.UTC) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) TimeLocal() time.Time { | ||||||
|  | 	return time.Date(t.Year, time.Month(t.Month), t.Day, 0, 0, 0, 0, time.Local) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) MarshalBinary() ([]byte, error) { | ||||||
|  | 	return t.TimeUTC().MarshalBinary() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Date) UnmarshalBinary(data []byte) error { | ||||||
|  | 	nt := time.Time{} | ||||||
|  | 	if err := nt.UnmarshalBinary(data); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	t.Year = nt.Year() | ||||||
|  | 	t.Month = int(nt.Month()) | ||||||
|  | 	t.Day = nt.Day() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) GobEncode() ([]byte, error) { | ||||||
|  | 	return t.TimeUTC().GobEncode() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Date) GobDecode(data []byte) error { | ||||||
|  | 	nt := time.Time{} | ||||||
|  | 	if err := nt.GobDecode(data); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	t.Year = nt.Year() | ||||||
|  | 	t.Month = int(nt.Month()) | ||||||
|  | 	t.Day = nt.Day() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Date) UnmarshalJSON(data []byte) error { | ||||||
|  | 	str := "" | ||||||
|  | 	if err := json.Unmarshal(data, &str); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	t0, err := time.Parse(t.FormatStr(), str) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	t.Year = t0.Year() | ||||||
|  | 	t.Month = int(t0.Month()) | ||||||
|  | 	t.Day = t0.Day() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) MarshalJSON() ([]byte, error) { | ||||||
|  | 	str := t.TimeUTC().Format(t.FormatStr()) | ||||||
|  | 	return json.Marshal(str) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) MarshalText() ([]byte, error) { | ||||||
|  | 	b := make([]byte, 0, len(t.FormatStr())) | ||||||
|  | 	return t.TimeUTC().AppendFormat(b, t.FormatStr()), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Date) UnmarshalText(data []byte) error { | ||||||
|  | 	var err error | ||||||
|  | 	v, err := time.Parse(t.FormatStr(), string(data)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	t.Year = v.Year() | ||||||
|  | 	t.Month = int(v.Month()) | ||||||
|  | 	t.Day = v.Day() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *Date) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { | ||||||
|  | 	if bt == bsontype.Null { | ||||||
|  | 		// we can't set nil in UnmarshalBSONValue (so we use default(struct)) | ||||||
|  | 		// Use mongoext.CreateGoExtBsonRegistry if you need to unmarsh pointer values | ||||||
|  | 		// https://stackoverflow.com/questions/75167597 | ||||||
|  | 		// https://jira.mongodb.org/browse/GODRIVER-2252 | ||||||
|  | 		*t = Date{} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if bt != bsontype.String { | ||||||
|  | 		return errors.New(fmt.Sprintf("cannot unmarshal %v into Date", bt)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var tt string | ||||||
|  | 	err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	v, err := time.Parse(t.FormatStr(), tt) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	t.Year = v.Year() | ||||||
|  | 	t.Month = int(v.Month()) | ||||||
|  | 	t.Day = v.Day() | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) MarshalBSONValue() (bsontype.Type, []byte, error) { | ||||||
|  | 	return bson.MarshalValue(t.TimeUTC().Format(t.FormatStr())) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error { | ||||||
|  | 	if val.Kind() == reflect.Ptr && val.IsNil() { | ||||||
|  | 		if !val.CanSet() { | ||||||
|  | 			return errors.New("ValueUnmarshalerDecodeValue") | ||||||
|  | 		} | ||||||
|  | 		val.Set(reflect.New(val.Type().Elem())) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tp, src, err := bsonrw.Copier{}.CopyValueToBytes(vr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if val.Kind() == reflect.Ptr && len(src) == 0 { | ||||||
|  | 		val.Set(reflect.Zero(val.Type())) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = t.UnmarshalBSONValue(tp, src) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if val.Kind() == reflect.Ptr { | ||||||
|  | 		val.Set(reflect.ValueOf(&t)) | ||||||
|  | 	} else { | ||||||
|  | 		val.Set(reflect.ValueOf(t)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) Serialize() string { | ||||||
|  | 	return t.TimeUTC().Format(t.FormatStr()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) FormatStr() string { | ||||||
|  | 	return "2006-01-02" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) Date() (year int, month time.Month, day int) { | ||||||
|  | 	return t.TimeUTC().Date() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) Weekday() time.Weekday { | ||||||
|  | 	return t.TimeUTC().Weekday() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) ISOWeek() (year, week int) { | ||||||
|  | 	return t.TimeUTC().ISOWeek() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) YearDay() int { | ||||||
|  | 	return t.TimeUTC().YearDay() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) AddDate(years int, months int, days int) Date { | ||||||
|  | 	return NewDate(t.TimeUTC().AddDate(years, months, days)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) Unix() int64 { | ||||||
|  | 	return t.TimeUTC().Unix() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) UnixMilli() int64 { | ||||||
|  | 	return t.TimeUTC().UnixMilli() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) UnixMicro() int64 { | ||||||
|  | 	return t.TimeUTC().UnixMicro() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) UnixNano() int64 { | ||||||
|  | 	return t.TimeUTC().UnixNano() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) Format(layout string) string { | ||||||
|  | 	return t.TimeUTC().Format(layout) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) GoString() string { | ||||||
|  | 	return t.TimeUTC().GoString() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t Date) String() string { | ||||||
|  | 	return t.TimeUTC().String() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewDate(t time.Time) Date { | ||||||
|  | 	return Date{ | ||||||
|  | 		Year:  t.Year(), | ||||||
|  | 		Month: int(t.Month()), | ||||||
|  | 		Day:   t.Day(), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NowDate(loc *time.Location) Date { | ||||||
|  | 	return NewDate(time.Now().In(loc)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NowDateLoc() Date { | ||||||
|  | 	return NewDate(time.Now().In(time.UTC)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NowDateUTC() Date { | ||||||
|  | 	return NewDate(time.Now().In(time.Local)) | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| package rfctime | package rfctime | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -81,7 +81,7 @@ func (t *RFC3339Time) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { | |||||||
| 		return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339Time", bt)) | 		return errors.New(fmt.Sprintf("cannot unmarshal %v into RFC3339Time", bt)) | ||||||
| 	} | 	} | ||||||
| 	var tt time.Time | 	var tt time.Time | ||||||
| 	err := bson.Unmarshal(data, &tt) | 	err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -116,6 +116,12 @@ func (t RFC3339Time) DecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueRead | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if val.Kind() == reflect.Ptr { | ||||||
|  | 		val.Set(reflect.ValueOf(&t)) | ||||||
|  | 	} else { | ||||||
|  | 		val.Set(reflect.ValueOf(t)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package tst | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
|  | 	"runtime/debug" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -54,12 +55,18 @@ func AssertHexEqual(t *testing.T, expected string, actual []byte) { | |||||||
|  |  | ||||||
| func AssertTrue(t *testing.T, value bool) { | func AssertTrue(t *testing.T, value bool) { | ||||||
| 	if !value { | 	if !value { | ||||||
| 		t.Error("value should be true") | 		t.Error("value should be true\n" + string(debug.Stack())) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func AssertFalse(t *testing.T, value bool) { | func AssertFalse(t *testing.T, value bool) { | ||||||
| 	if value { | 	if value { | ||||||
| 		t.Error("value should be false") | 		t.Error("value should be false\n" + string(debug.Stack())) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func AssertNoErr(t *testing.T, anerr error) { | ||||||
|  | 	if anerr != nil { | ||||||
|  | 		t.Error("Function returned an error: " + anerr.Error() + "\n" + string(debug.Stack())) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										95
									
								
								wmo/collection.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								wmo/collection.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/bsontype" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"reflect" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type EntityID interface { | ||||||
|  | 	MarshalBSONValue() (bsontype.Type, []byte, error) | ||||||
|  | 	String() string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Decodable interface { | ||||||
|  | 	Decode(v any) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Cursorable interface { | ||||||
|  | 	Decode(v any) error | ||||||
|  | 	Err() error | ||||||
|  | 	Close(ctx context.Context) error | ||||||
|  | 	All(ctx context.Context, results any) error | ||||||
|  | 	RemainingBatchLength() int | ||||||
|  | 	Next(ctx context.Context) bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type fullTypeRef struct { | ||||||
|  | 	IsPointer      bool | ||||||
|  | 	Kind           reflect.Kind | ||||||
|  | 	RealType       reflect.Type | ||||||
|  | 	Type           reflect.Type | ||||||
|  | 	UnderlyingType reflect.Type | ||||||
|  | 	Name           string | ||||||
|  | 	Index          []int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Coll[TData any] struct { | ||||||
|  | 	coll                *mongo.Collection                                        // internal mongo collection, access via Collection() | ||||||
|  | 	dataTypeMap         map[string]fullTypeRef                                   // list of TData fields (only if TData is not an interface) | ||||||
|  | 	implDataTypeMap     map[reflect.Type]map[string]fullTypeRef                  // dynamic list of fields of TData implementations (only if TData is an interface) | ||||||
|  | 	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) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Collection() *mongo.Collection { | ||||||
|  | 	return c.coll | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Name() string { | ||||||
|  | 	return c.coll.Name() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) WithDecodeFunc(cdf func(ctx context.Context, dec Decodable) (TData, error), example TData) *Coll[TData] { | ||||||
|  |  | ||||||
|  | 	c.EnsureInitializedReflection(example) | ||||||
|  |  | ||||||
|  | 	c.customDecoder = langext.Ptr(cdf) | ||||||
|  | 	return c | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Indexes() mongo.IndexView { | ||||||
|  | 	return c.coll.Indexes() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Drop(ctx context.Context) error { | ||||||
|  | 	return c.coll.Drop(ctx) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) createToken(fieldPrimary string, dirPrimary ct.SortDirection, fieldSecondary *string, dirSecondary *ct.SortDirection, lastEntity TData, pageSize *int) (ct.CursorToken, error) { | ||||||
|  |  | ||||||
|  | 	valuePrimary, err := c.getFieldValueAsTokenString(lastEntity, fieldPrimary) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return ct.CursorToken{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	valueSeconary := "" | ||||||
|  | 	if fieldSecondary != nil && dirSecondary != nil { | ||||||
|  | 		valueSeconary, err = c.getFieldValueAsTokenString(lastEntity, *fieldSecondary) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return ct.CursorToken{}, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ct.CursorToken{ | ||||||
|  | 		Mode:           ct.CTMNormal, | ||||||
|  | 		ValuePrimary:   valuePrimary, | ||||||
|  | 		ValueSecondary: valueSeconary, | ||||||
|  | 		Direction:      dirPrimary, | ||||||
|  | 		PageSize:       langext.Coalesce(pageSize, 0), | ||||||
|  | 		Extra:          ct.Extra{}, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								wmo/decoding.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								wmo/decoding.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) decodeSingle(ctx context.Context, dec Decodable) (TData, error) { | ||||||
|  | 	if c.customDecoder != nil { | ||||||
|  |  | ||||||
|  | 		return (*c.customDecoder)(ctx, dec) | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  |  | ||||||
|  | 		var res TData | ||||||
|  |  | ||||||
|  | 		err := dec.Decode(&res) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return *new(TData), err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return res, nil | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) decodeAll(ctx context.Context, cursor Cursorable) ([]TData, error) { | ||||||
|  | 	if c.customDecoder != nil { | ||||||
|  |  | ||||||
|  | 		res := make([]TData, 0, cursor.RemainingBatchLength()) | ||||||
|  |  | ||||||
|  | 		for cursor.Next(ctx) { | ||||||
|  | 			entry, err := (*c.customDecoder)(ctx, cursor) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			res = append(res, entry) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return res, nil | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  |  | ||||||
|  | 		res := make([]TData, 0, cursor.RemainingBatchLength()) | ||||||
|  |  | ||||||
|  | 		err := cursor.All(ctx, &res) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return res, nil | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								wmo/mongo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								wmo/mongo.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import "go.mongodb.org/mongo-driver/mongo" | ||||||
|  |  | ||||||
|  | func W[TData any](collection *mongo.Collection) *Coll[TData] { | ||||||
|  | 	c := Coll[TData]{coll: collection} | ||||||
|  |  | ||||||
|  | 	c.init() | ||||||
|  |  | ||||||
|  | 	return &c | ||||||
|  | } | ||||||
							
								
								
									
										87
									
								
								wmo/pagination.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								wmo/pagination.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func CreatePagination[TData any](coll *Coll[TData], token ct.CursorToken, fieldPrimary string, sortPrimary ct.SortDirection, fieldSecondary *string, sortSecondary *ct.SortDirection, pageSize *int) ([]bson.D, error) { | ||||||
|  |  | ||||||
|  | 	cond := bson.A{} | ||||||
|  | 	sort := bson.D{} | ||||||
|  |  | ||||||
|  | 	valuePrimary, err := coll.getTokenValueAsMongoType(token.ValuePrimary, fieldPrimary) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if sortPrimary == ct.SortASC { | ||||||
|  | 		// We sort ASC on <field> - so we want all entries newer ($gt) than the $primary | ||||||
|  | 		cond = append(cond, bson.M{fieldPrimary: bson.M{"$gt": valuePrimary}}) | ||||||
|  | 		sort = append(sort, bson.E{Key: fieldPrimary, Value: +1}) | ||||||
|  | 	} else if sortPrimary == ct.SortDESC { | ||||||
|  | 		// We sort DESC on <field> - so we want all entries older ($lt) than the $primary | ||||||
|  | 		cond = append(cond, bson.M{fieldPrimary: bson.M{"$lt": valuePrimary}}) | ||||||
|  | 		sort = append(sort, bson.E{Key: fieldPrimary, Value: -1}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fieldSecondary != nil && sortSecondary != nil && *fieldSecondary != fieldPrimary { | ||||||
|  |  | ||||||
|  | 		valueSecondary, err := coll.getTokenValueAsMongoType(token.ValueSecondary, *fieldSecondary) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if *sortSecondary == ct.SortASC { | ||||||
|  |  | ||||||
|  | 			// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a greater $secondary (= newer) | ||||||
|  | 			cond = append(cond, bson.M{"$and": bson.A{ | ||||||
|  | 				bson.M{fieldPrimary: valuePrimary}, | ||||||
|  | 				bson.M{*fieldSecondary: bson.M{"$gt": valueSecondary}}, | ||||||
|  | 			}}) | ||||||
|  |  | ||||||
|  | 			sort = append(sort, bson.E{Key: fieldPrimary, Value: +1}) | ||||||
|  |  | ||||||
|  | 		} else if *sortSecondary == ct.SortDESC { | ||||||
|  |  | ||||||
|  | 			// the conflict-resolution condition, for entries with the _same_ <field> as the $primary we take the ones with a smaller $secondary (= older) | ||||||
|  | 			cond = append(cond, bson.M{"$and": bson.A{ | ||||||
|  | 				bson.M{fieldPrimary: valuePrimary}, | ||||||
|  | 				bson.M{*fieldSecondary: bson.M{"$lt": valueSecondary}}, | ||||||
|  | 			}}) | ||||||
|  |  | ||||||
|  | 			sort = append(sort, bson.E{Key: fieldPrimary, Value: -1}) | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pipeline := make([]bson.D, 0, 3) | ||||||
|  |  | ||||||
|  | 	if token.Mode == ct.CTMStart { | ||||||
|  |  | ||||||
|  | 		// no gt/lt condition | ||||||
|  |  | ||||||
|  | 	} else if token.Mode == ct.CTMNormal { | ||||||
|  |  | ||||||
|  | 		pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.M{"$or": cond}}}) | ||||||
|  |  | ||||||
|  | 	} else if token.Mode == ct.CTMEnd { | ||||||
|  |  | ||||||
|  | 		// false | ||||||
|  | 		pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.M{"$expr": bson.M{"$eq": bson.A{"1", "0"}}}}}) | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  |  | ||||||
|  | 		return nil, errors.New("unknown ct mode: " + string(token.Mode)) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pipeline = append(pipeline, bson.D{{Key: "$sort", Value: sort}}) | ||||||
|  |  | ||||||
|  | 	if pageSize != nil { | ||||||
|  | 		pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(*pageSize + 1)}}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return pipeline, nil | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								wmo/queryAggregate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								wmo/queryAggregate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo/options" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Aggregate(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) ([]TData, error) { | ||||||
|  | 	cursor, err := c.coll.Aggregate(ctx, pipeline, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res, err := c.decodeAll(ctx, cursor) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) AggregateOneOpt(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (*TData, error) { | ||||||
|  | 	cursor, err := c.coll.Aggregate(ctx, pipeline, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cursor.Next(ctx) { | ||||||
|  | 		v, err := c.decodeSingle(ctx, cursor) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		return &v, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) AggregateOne(ctx context.Context, pipeline mongo.Pipeline, opts ...*options.AggregateOptions) (TData, error) { | ||||||
|  | 	cursor, err := c.coll.Aggregate(ctx, pipeline, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return *new(TData), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cursor.Next(ctx) { | ||||||
|  | 		v, err := c.decodeSingle(ctx, cursor) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return *new(TData), err | ||||||
|  | 		} | ||||||
|  | 		return v, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return *new(TData), errors.New("no document in result") | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								wmo/queryDelete.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								wmo/queryDelete.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) DeleteOneByID(ctx context.Context, id EntityID) error { | ||||||
|  | 	_, err := c.coll.DeleteOne(ctx, bson.M{"_id": id}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) DeleteOne(ctx context.Context, filterQuery bson.M) error { | ||||||
|  | 	_, err := c.coll.DeleteOne(ctx, filterQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) DeleteMany(ctx context.Context, filterQuery bson.M) (*mongo.DeleteResult, error) { | ||||||
|  | 	res, err := c.coll.DeleteMany(ctx, filterQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								wmo/queryFind.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								wmo/queryFind.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo/options" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) FindOne(ctx context.Context, filter bson.M) (TData, error) { | ||||||
|  | 	mongoRes := c.coll.FindOne(ctx, filter) | ||||||
|  |  | ||||||
|  | 	return c.decodeSingle(ctx, mongoRes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) FindOneOpt(ctx context.Context, filter bson.M) (*TData, error) { | ||||||
|  | 	mongoRes := c.coll.FindOne(ctx, filter) | ||||||
|  |  | ||||||
|  | 	res, err := c.decodeSingle(ctx, mongoRes) | ||||||
|  | 	if err == mongo.ErrNoDocuments { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) FindOneByID(ctx context.Context, id EntityID) (TData, error) { | ||||||
|  | 	mongoRes := c.coll.FindOne(ctx, bson.M{"_id": id}) | ||||||
|  |  | ||||||
|  | 	return c.decodeSingle(ctx, mongoRes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) FindOneOptByID(ctx context.Context, id EntityID) (*TData, error) { | ||||||
|  | 	mongoRes := c.coll.FindOne(ctx, bson.M{"_id": id}) | ||||||
|  |  | ||||||
|  | 	res, err := c.decodeSingle(ctx, mongoRes) | ||||||
|  | 	if err == mongo.ErrNoDocuments { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Find(ctx context.Context, filter bson.M, opts ...*options.FindOptions) ([]TData, error) { | ||||||
|  | 	cursor, err := c.coll.Find(ctx, filter, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res, err := c.decodeAll(ctx, cursor) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								wmo/queryInsert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								wmo/queryInsert.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) InsertOne(ctx context.Context, valueIn TData) (TData, error) { | ||||||
|  | 	insRes, err := c.coll.InsertOne(ctx, valueIn) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return *new(TData), err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	mongoRes := c.coll.FindOne(ctx, bson.M{"_id": insRes.InsertedID}) | ||||||
|  |  | ||||||
|  | 	return c.decodeSingle(ctx, mongoRes) | ||||||
|  | } | ||||||
							
								
								
									
										107
									
								
								wmo/queryList.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								wmo/queryList.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) List(ctx context.Context, filter ct.Filter, pageSize *int, inTok ct.CursorToken) ([]TData, ct.CursorToken, error) { | ||||||
|  | 	if inTok.Mode == ct.CTMEnd { | ||||||
|  | 		return make([]TData, 0), ct.End(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pipeline := mongo.Pipeline{} | ||||||
|  | 	pf1 := "_id" | ||||||
|  | 	pd1 := ct.SortASC | ||||||
|  | 	pf2 := "_id" | ||||||
|  | 	pd2 := ct.SortASC | ||||||
|  |  | ||||||
|  | 	if filter != nil { | ||||||
|  | 		pipeline = filter.FilterQuery() | ||||||
|  | 		pf1, pd1, pf2, pd2 = filter.Pagination() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sortPrimary := pf1 | ||||||
|  | 	sortDirPrimary := pd1 | ||||||
|  | 	sortSecondary := &pf2 | ||||||
|  | 	sortDirSecondary := &pd2 | ||||||
|  |  | ||||||
|  | 	if pf1 == pf2 { | ||||||
|  | 		sortSecondary = nil | ||||||
|  | 		sortDirSecondary = nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	paginationPipeline, err := CreatePagination(c, inTok, sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary, pageSize) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, ct.CursorToken{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pipeline = append(pipeline, paginationPipeline...) | ||||||
|  |  | ||||||
|  | 	cursor, err := c.coll.Aggregate(ctx, pipeline) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, ct.CursorToken{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// fast branch | ||||||
|  | 	if pageSize == nil { | ||||||
|  | 		entries, err := c.decodeAll(ctx, cursor) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, ct.CursorToken{}, err | ||||||
|  | 		} | ||||||
|  | 		return entries, ct.End(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	entities := make([]TData, 0, cursor.RemainingBatchLength()) | ||||||
|  | 	for (pageSize == nil || len(entities) != *pageSize) && cursor.Next(ctx) { | ||||||
|  | 		var entry TData | ||||||
|  | 		entry, err = c.decodeSingle(ctx, cursor) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, ct.CursorToken{}, err | ||||||
|  | 		} | ||||||
|  | 		entities = append(entities, entry) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if pageSize == nil || len(entities) < *pageSize || !cursor.TryNext(ctx) { | ||||||
|  | 		return entities, ct.End(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	last := entities[len(entities)-1] | ||||||
|  |  | ||||||
|  | 	c.EnsureInitializedReflection(last) | ||||||
|  |  | ||||||
|  | 	nextToken, err := c.createToken(sortPrimary, sortDirPrimary, sortSecondary, sortDirSecondary, last, pageSize) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, ct.CursorToken{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return entities, nextToken, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type countRes struct { | ||||||
|  | 	Count int64 `bson:"c"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) Count(ctx context.Context, filter ct.Filter) (int64, error) { | ||||||
|  | 	pipeline := filter.FilterQuery() | ||||||
|  |  | ||||||
|  | 	pipeline = append(pipeline, bson.D{{Key: "$count", Value: "c"}}) | ||||||
|  |  | ||||||
|  | 	cursor, err := c.coll.Aggregate(ctx, pipeline) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cursor.Next(ctx) { | ||||||
|  | 		v := countRes{} | ||||||
|  | 		err = cursor.Decode(&v) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return 0, err | ||||||
|  | 		} | ||||||
|  | 		return v.Count, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return 0, nil | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								wmo/queryUpdate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								wmo/queryUpdate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo/options" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) FindOneAndUpdate(ctx context.Context, filterQuery bson.M, updateQuery bson.M) (TData, error) { | ||||||
|  | 	mongoRes := c.coll.FindOneAndUpdate(ctx, filterQuery, updateQuery, options.FindOneAndUpdate().SetReturnDocument(options.After)) | ||||||
|  |  | ||||||
|  | 	return c.decodeSingle(ctx, mongoRes) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) UpdateOne(ctx context.Context, filterQuery bson.M, updateQuery bson.M) error { | ||||||
|  | 	_, err := c.coll.UpdateOne(ctx, filterQuery, updateQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) UpdateOneByID(ctx context.Context, id EntityID, updateQuery bson.M) error { | ||||||
|  | 	_, err := c.coll.UpdateOne(ctx, bson.M{"_id": id}, updateQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) UpdateMany(ctx context.Context, filterQuery bson.M, updateQuery bson.M) (*mongo.UpdateResult, error) { | ||||||
|  | 	res, err := c.coll.UpdateMany(ctx, filterQuery, updateQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return res, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) ReplaceOne(ctx context.Context, filterQuery bson.M, value TData) error { | ||||||
|  | 	_, err := c.coll.UpdateOne(ctx, filterQuery, bson.M{"$set": value}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) FindOneAndReplace(ctx context.Context, filterQuery bson.M, value TData) (TData, error) { | ||||||
|  | 	mongoRes := c.coll.FindOneAndUpdate(ctx, filterQuery, bson.M{"$set": value}, options.FindOneAndUpdate().SetReturnDocument(options.After)) | ||||||
|  |  | ||||||
|  | 	return c.decodeSingle(ctx, mongoRes) | ||||||
|  | } | ||||||
							
								
								
									
										215
									
								
								wmo/reflection.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								wmo/reflection.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,215 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/reflectext" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) EnsureInitializedReflection(v TData) { | ||||||
|  |  | ||||||
|  | 	if !c.isInterfaceDataType { | ||||||
|  | 		return // only dynamically load dataTypeMap on interface TData | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rval := reflect.ValueOf(v) | ||||||
|  | 	for rval.Type().Kind() == reflect.Pointer { | ||||||
|  | 		rval = rval.Elem() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if _, ok := c.implDataTypeMap[rval.Type()]; ok { | ||||||
|  | 		return // already loaded | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m := make(map[string]fullTypeRef) | ||||||
|  |  | ||||||
|  | 	c.initFields("", rval, m, make([]int, 0)) | ||||||
|  |  | ||||||
|  | 	c.implDataTypeMap[rval.Type()] = m | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) init() { | ||||||
|  |  | ||||||
|  | 	example := *new(TData) | ||||||
|  |  | ||||||
|  | 	datatype := reflect.TypeOf(&example).Elem() | ||||||
|  |  | ||||||
|  | 	if datatype.Kind() == reflect.Interface { | ||||||
|  |  | ||||||
|  | 		c.isInterfaceDataType = true | ||||||
|  |  | ||||||
|  | 		c.dataTypeMap = make(map[string]fullTypeRef) | ||||||
|  | 		c.implDataTypeMap = make(map[reflect.Type]map[string]fullTypeRef) | ||||||
|  | 	} else { | ||||||
|  |  | ||||||
|  | 		c.isInterfaceDataType = false | ||||||
|  |  | ||||||
|  | 		c.dataTypeMap = make(map[string]fullTypeRef) | ||||||
|  | 		c.implDataTypeMap = make(map[reflect.Type]map[string]fullTypeRef) | ||||||
|  |  | ||||||
|  | 		v := reflect.ValueOf(example) | ||||||
|  | 		c.initFields("", v, c.dataTypeMap, make([]int, 0)) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) initFields(prefix string, rval reflect.Value, m map[string]fullTypeRef, idxarr []int) { | ||||||
|  |  | ||||||
|  | 	rtyp := rval.Type() | ||||||
|  |  | ||||||
|  | 	for i := 0; i < rtyp.NumField(); i++ { | ||||||
|  |  | ||||||
|  | 		rsfield := rtyp.Field(i) | ||||||
|  | 		rvfield := rval.Field(i) | ||||||
|  |  | ||||||
|  | 		if !rsfield.IsExported() { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		bsontags := make([]string, 0) | ||||||
|  | 		bsonkey, found := rsfield.Tag.Lookup("bson") | ||||||
|  | 		if !found { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if strings.Contains(bsonkey, ",") { | ||||||
|  | 			bsontags = strings.Split(bsonkey[strings.Index(bsonkey, ",")+1:], ",") | ||||||
|  | 			bsonkey = bsonkey[:strings.Index(bsonkey, ",")] | ||||||
|  | 		} | ||||||
|  | 		if bsonkey == "-" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if bsonkey == "" { | ||||||
|  | 			bsonkey = rsfield.Name | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		fullKey := prefix + bsonkey | ||||||
|  |  | ||||||
|  | 		newIdxArr := langext.ArrCopy(idxarr) | ||||||
|  | 		newIdxArr = append(newIdxArr, i) | ||||||
|  |  | ||||||
|  | 		if langext.InArray("inline", bsontags) && rvfield.Kind() == reflect.Struct { | ||||||
|  |  | ||||||
|  | 			// pass-through field | ||||||
|  | 			c.initFields(prefix, rvfield, m, newIdxArr) | ||||||
|  |  | ||||||
|  | 		} else { | ||||||
|  |  | ||||||
|  | 			if rvfield.Type().Kind() == reflect.Pointer { | ||||||
|  |  | ||||||
|  | 				m[fullKey] = fullTypeRef{ | ||||||
|  | 					IsPointer:      true, | ||||||
|  | 					RealType:       rvfield.Type(), | ||||||
|  | 					Kind:           rvfield.Type().Elem().Kind(), | ||||||
|  | 					Type:           rvfield.Type().Elem(), | ||||||
|  | 					UnderlyingType: reflectext.Underlying(rvfield.Type().Elem()), | ||||||
|  | 					Name:           rsfield.Name, | ||||||
|  | 					Index:          newIdxArr, | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 			} else { | ||||||
|  |  | ||||||
|  | 				m[fullKey] = fullTypeRef{ | ||||||
|  | 					IsPointer:      false, | ||||||
|  | 					RealType:       rvfield.Type(), | ||||||
|  | 					Kind:           rvfield.Type().Kind(), | ||||||
|  | 					Type:           rvfield.Type(), | ||||||
|  | 					UnderlyingType: reflectext.Underlying(rvfield.Type()), | ||||||
|  | 					Name:           rsfield.Name, | ||||||
|  | 					Index:          newIdxArr, | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if rvfield.Kind() == reflect.Struct { | ||||||
|  | 				c.initFields(fullKey+".", rvfield, m, newIdxArr) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) getTokenValueAsMongoType(value string, fieldName string) (any, error) { | ||||||
|  |  | ||||||
|  | 	fref, err := c.getFieldType(fieldName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pss := reflectext.PrimitiveStringSerializer{} | ||||||
|  |  | ||||||
|  | 	return pss.ValueFromString(value, fref.RealType) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) getFieldValueAsTokenString(entity TData, fieldName string) (string, error) { | ||||||
|  |  | ||||||
|  | 	realValue, err := c.getFieldValue(entity, fieldName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pss := reflectext.PrimitiveStringSerializer{} | ||||||
|  |  | ||||||
|  | 	return pss.ValueToString(realValue) | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) getFieldType(fieldName string) (fullTypeRef, error) { | ||||||
|  | 	if c.isInterfaceDataType { | ||||||
|  |  | ||||||
|  | 		for _, m := range c.implDataTypeMap { | ||||||
|  | 			if r, ok := m[fieldName]; ok { | ||||||
|  | 				return r, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return fullTypeRef{}, errors.New("unknown field: '" + fieldName + "' (in any impl)") | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  |  | ||||||
|  | 		if r, ok := c.dataTypeMap[fieldName]; ok { | ||||||
|  | 			return r, nil | ||||||
|  | 		} else { | ||||||
|  | 			return fullTypeRef{}, errors.New("unknown field: '" + fieldName + "'") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Coll[TData]) getFieldValue(data TData, fieldName string) (any, error) { | ||||||
|  | 	if c.isInterfaceDataType { | ||||||
|  |  | ||||||
|  | 		rval := reflect.ValueOf(data) | ||||||
|  | 		for rval.Type().Kind() == reflect.Pointer { | ||||||
|  | 			rval = rval.Elem() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if m, ok := c.implDataTypeMap[rval.Type()]; ok { | ||||||
|  | 			if fref, ok := m[fieldName]; ok { | ||||||
|  | 				rval := reflect.ValueOf(data) | ||||||
|  | 				return rval.FieldByIndex(fref.Index).Interface(), nil | ||||||
|  | 			} else { | ||||||
|  | 				return nil, errors.New("unknown bson field '" + fieldName + "' in type '" + rval.Type().String() + "'") | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			return nil, errors.New("unknown TData type: '" + rval.Type().String() + "'") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  |  | ||||||
|  | 		if fref, ok := c.dataTypeMap[fieldName]; ok { | ||||||
|  | 			rval := reflect.ValueOf(data) | ||||||
|  | 			return rval.FieldByIndex(fref.Index).Interface(), nil | ||||||
|  | 		} else { | ||||||
|  | 			return nil, errors.New("unknown bson field '" + fieldName + "'") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										226
									
								
								wmo/reflection_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								wmo/reflection_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,226 @@ | |||||||
|  | package wmo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||||
|  | 	"go.mongodb.org/mongo-driver/mongo" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/rfctime" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/timeext" | ||||||
|  | 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestReflectionGetFieldType(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type IDType string | ||||||
|  |  | ||||||
|  | 	type TestData struct { | ||||||
|  | 		ID    IDType    `bson:"_id"` | ||||||
|  | 		CDate time.Time `bson:"cdate"` | ||||||
|  | 		Sub   struct { | ||||||
|  | 			A string `bson:"a"` | ||||||
|  | 		} `bson:"sub"` | ||||||
|  | 		Str   string                  `bson:"str"` | ||||||
|  | 		Ptr   *int                    `bson:"ptr"` | ||||||
|  | 		MDate rfctime.RFC3339NanoTime `bson:"mdate"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	coll := W[TestData](&mongo.Collection{}) | ||||||
|  |  | ||||||
|  | 	coll.init() | ||||||
|  |  | ||||||
|  | 	t0 := time.Now() | ||||||
|  | 	t1 := rfctime.NewRFC3339Nano(t0) | ||||||
|  |  | ||||||
|  | 	d := TestData{ | ||||||
|  | 		ID:    "1", | ||||||
|  | 		CDate: t0, | ||||||
|  | 		Sub: struct { | ||||||
|  | 			A string `bson:"a"` | ||||||
|  | 		}{ | ||||||
|  | 			A: "2", | ||||||
|  | 		}, | ||||||
|  | 		Str:   "3", | ||||||
|  | 		Ptr:   langext.Ptr(4), | ||||||
|  | 		MDate: t1, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gft := func(k string) fullTypeRef { | ||||||
|  | 		v, err := coll.getFieldType(k) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Errorf("%s: %v", "failed to getFieldType", err) | ||||||
|  | 		} | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gfv := func(k string) any { | ||||||
|  | 		v, err := coll.getFieldValue(d, k) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Errorf("%s: %v", "failed to getFieldType", err) | ||||||
|  | 		} | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gft("_id").Kind.String(), "string") | ||||||
|  | 	tst.AssertEqual(t, gft("_id").Type.String(), "wmo.IDType") | ||||||
|  | 	tst.AssertEqual(t, gft("_id").Name, "ID") | ||||||
|  | 	tst.AssertEqual(t, gft("_id").IsPointer, false) | ||||||
|  | 	tst.AssertEqual(t, gfv("_id").(IDType), "1") | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gft("cdate").Kind.String(), "struct") | ||||||
|  | 	tst.AssertEqual(t, gft("cdate").Type.String(), "time.Time") | ||||||
|  | 	tst.AssertEqual(t, gft("cdate").Name, "CDate") | ||||||
|  | 	tst.AssertEqual(t, gft("cdate").IsPointer, false) | ||||||
|  | 	tst.AssertEqual(t, gfv("cdate").(time.Time), t0) | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gft("sub.a").Kind.String(), "string") | ||||||
|  | 	tst.AssertEqual(t, gft("sub.a").Type.String(), "string") | ||||||
|  | 	tst.AssertEqual(t, gft("sub.a").Name, "A") | ||||||
|  | 	tst.AssertEqual(t, gft("sub.a").IsPointer, false) | ||||||
|  | 	tst.AssertEqual(t, gfv("sub.a").(string), "2") | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gft("str").Kind.String(), "string") | ||||||
|  | 	tst.AssertEqual(t, gft("str").Type.String(), "string") | ||||||
|  | 	tst.AssertEqual(t, gft("str").Name, "Str") | ||||||
|  | 	tst.AssertEqual(t, gft("str").IsPointer, false) | ||||||
|  | 	tst.AssertEqual(t, gfv("str").(string), "3") | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gft("ptr").Kind.String(), "int") | ||||||
|  | 	tst.AssertEqual(t, gft("ptr").Type.String(), "int") | ||||||
|  | 	tst.AssertEqual(t, gft("ptr").Name, "Ptr") | ||||||
|  | 	tst.AssertEqual(t, gft("ptr").IsPointer, true) | ||||||
|  | 	tst.AssertEqual(t, *gfv("ptr").(*int), 4) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReflectionGetTokenValueAsMongoType(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type IDType string | ||||||
|  |  | ||||||
|  | 	type TestData struct { | ||||||
|  | 		ID    IDType    `bson:"_id"` | ||||||
|  | 		CDate time.Time `bson:"cdate"` | ||||||
|  | 		Sub   struct { | ||||||
|  | 			A string `bson:"a"` | ||||||
|  | 		} `bson:"sub"` | ||||||
|  | 		Str   string                  `bson:"str"` | ||||||
|  | 		Ptr   *int                    `bson:"ptr"` | ||||||
|  | 		Num   int                     `bson:"num"` | ||||||
|  | 		MDate rfctime.RFC3339NanoTime `bson:"mdate"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	coll := W[TestData](&mongo.Collection{}) | ||||||
|  |  | ||||||
|  | 	coll.init() | ||||||
|  |  | ||||||
|  | 	gtvasmt := func(value string, fieldName string) any { | ||||||
|  | 		v, err := coll.getTokenValueAsMongoType(value, fieldName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Errorf("%s", "failed to getTokenValueAsMongoType") | ||||||
|  | 			t.Errorf("%v+", err) | ||||||
|  | 		} | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tx, err := time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("%v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gtvasmt("hello", "str").(string), "hello") | ||||||
|  | 	tst.AssertEqual(t, gtvasmt("4", "num").(int), 4) | ||||||
|  | 	tst.AssertEqual(t, gtvasmt("asdf", "_id").(IDType), "asdf") | ||||||
|  | 	tst.AssertEqual(t, gtvasmt("", "ptr").(*int), nil) | ||||||
|  | 	tst.AssertEqual(t, *(gtvasmt("123", "ptr").(*int)), 123) | ||||||
|  | 	tst.AssertEqual(t, gtvasmt("2009-11-10T23:00:00Z", "cdate").(time.Time), tx) | ||||||
|  | 	tst.AssertEqual(t, gtvasmt("2009-11-10T23:00:00Z", "mdate").(rfctime.RFC3339NanoTime), rfctime.NewRFC3339Nano(tx)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReflectionGetFieldValueAsTokenString(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type IDType string | ||||||
|  |  | ||||||
|  | 	type TestData struct { | ||||||
|  | 		ID    IDType    `bson:"_id"` | ||||||
|  | 		CDate time.Time `bson:"cdate"` | ||||||
|  | 		Sub   struct { | ||||||
|  | 			A string `bson:"a"` | ||||||
|  | 		} `bson:"sub"` | ||||||
|  | 		Str   string                  `bson:"str"` | ||||||
|  | 		Ptr   *int                    `bson:"ptr"` | ||||||
|  | 		Num   int                     `bson:"num"` | ||||||
|  | 		Ptr2  *int                    `bson:"ptr2"` | ||||||
|  | 		FFF   float64                 `bson:"fff"` | ||||||
|  | 		MDate rfctime.RFC3339NanoTime `bson:"mdate"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	coll := W[TestData](&mongo.Collection{}) | ||||||
|  |  | ||||||
|  | 	coll.init() | ||||||
|  |  | ||||||
|  | 	t0 := time.Date(2000, 1, 1, 12, 0, 0, 0, timeext.TimezoneBerlin) | ||||||
|  | 	t1 := rfctime.NewRFC3339Nano(t0) | ||||||
|  |  | ||||||
|  | 	d := TestData{ | ||||||
|  | 		ID:    "1", | ||||||
|  | 		CDate: t0, | ||||||
|  | 		MDate: t1, | ||||||
|  | 		Sub: struct { | ||||||
|  | 			A string `bson:"a"` | ||||||
|  | 		}{ | ||||||
|  | 			A: "2", | ||||||
|  | 		}, | ||||||
|  | 		Str:  "3", | ||||||
|  | 		Ptr:  langext.Ptr(4), | ||||||
|  | 		Num:  22, | ||||||
|  | 		FFF:  22.5, | ||||||
|  | 		Ptr2: nil, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gfvats := func(value TestData, fieldName string) string { | ||||||
|  | 		v, err := coll.getFieldValueAsTokenString(value, fieldName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Errorf("%s: %v", "failed to getTokenValueAsMongoType", err) | ||||||
|  | 		} | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "str"), "3") | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "num"), "22") | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "_id"), "1") | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "ptr"), "4") | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "ptr2"), "") | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "fff"), "22.5") | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "cdate"), t0.Format(time.RFC3339Nano)) | ||||||
|  | 	tst.AssertEqual(t, gfvats(d, "mdate"), t0.Format(time.RFC3339Nano)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestReflectionWithInterface(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	type TestData struct { | ||||||
|  | 		ID    primitive.ObjectID `bson:"_id"` | ||||||
|  | 		CDate time.Time          `bson:"cdate"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	type TestInterface interface { | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	coll1 := W[TestInterface](&mongo.Collection{}) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, coll1.coll != nil) | ||||||
|  | 	tst.AssertEqual(t, 0, len(coll1.implDataTypeMap)) | ||||||
|  |  | ||||||
|  | 	df := func(ctx context.Context, dec Decodable) (TestInterface, error) { | ||||||
|  | 		return TestData{}, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	coll2 := W[TestInterface](&mongo.Collection{}).WithDecodeFunc(df, TestData{}) | ||||||
|  |  | ||||||
|  | 	tst.AssertTrue(t, coll2.coll != nil) | ||||||
|  | 	tst.AssertEqual(t, 1, len(coll2.implDataTypeMap)) | ||||||
|  |  | ||||||
|  | 	tst.AssertEqual(t, "ID", coll2.implDataTypeMap[reflect.TypeOf(TestData{})]["_id"].Name) | ||||||
|  | 	tst.AssertEqual(t, "CDate", coll2.implDataTypeMap[reflect.TypeOf(TestData{})]["cdate"].Name) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user