Compare commits
	
		
			37 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 05580c384a | |||
| 3188b951fb | |||
| 6b211d1443 | |||
| b2b9b40792 | |||
| 2f915cb6c1 | |||
| b2b93f570a | |||
| 8247fc4524 | |||
| 5dad44ad09 | |||
| f042183433 | |||
| b0be93a7a0 | |||
| 1c143921e6 | |||
| 68e63a9cf6 | |||
| c3162fec95 | |||
| 1124aa781a | |||
| eef0e9f2aa | |||
| af38b06d22 | |||
| 2fad6340c7 | |||
| 03aa0a2282 | |||
| 358c238f3d | |||
| d65ac8ba2b | |||
| 55d02b8c65 | |||
| 8a3965f666 | |||
| 4aa2f494b1 | |||
| 8f13eb2f16 | |||
| 8f15d42173 | |||
| 07fa21dcca | |||
| e657de7f78 | |||
| c534e998e8 | |||
| 88642770c5 | |||
| 8528b5cb66 | |||
| 5ba84bd8ee | |||
| 1260b2dc77 | |||
| 7d18b913c6 | |||
| d1f9069f2f | |||
| fa6d73301e | |||
| bfe62799d3 | |||
| ede912eb7b | 
| @@ -6,7 +6,12 @@ | ||||
| name: Build Docker and Deploy | ||||
| run-name: Build & Deploy ${{ gitea.ref }} on ${{ gitea.actor }} | ||||
|  | ||||
| on: [push] | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - '*' | ||||
|       - '**' | ||||
|  | ||||
|  | ||||
| jobs: | ||||
|   run_tests: | ||||
| @@ -34,3 +39,17 @@ jobs: | ||||
|       - name: Run tests | ||||
|         run: cd "${{ gitea.workspace }}" && make test | ||||
|  | ||||
|       - name: Send failure mail | ||||
|         if: failure() | ||||
|         uses: dawidd6/action-send-mail@v3 | ||||
|         with: | ||||
|           server_address: smtp.fastmail.com | ||||
|           server_port: 465 | ||||
|           secure: true | ||||
|           username: ${{secrets.MAIL_USERNAME}} | ||||
|           password: ${{secrets.MAIL_PASSWORD}} | ||||
|           subject: Pipeline on '${{ gitea.repository }}' failed | ||||
|           to: ${{ steps.commiter_info.outputs.MAIL }} | ||||
|           from: Gitea Actions <gitea_actions@blackforestbytes.de> | ||||
|           body: "Go to https://gogs.blackforestbytes.com/${{ gitea.repository }}/actions" | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								bfcodegen/_test_example_2.tgz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								bfcodegen/_test_example_2.tgz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -70,9 +70,9 @@ func GenerateCharsetIDSpecs(sourceDir string, destFile string) error { | ||||
| 	newChecksum := cryptext.BytesSha256([]byte(newChecksumStr)) | ||||
|  | ||||
| 	if newChecksum != oldChecksum { | ||||
| 		fmt.Printf("[IDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) | ||||
| 		fmt.Printf("[CSIDGenerate] Checksum has changed ( %s -> %s ), will generate new file\n\n", oldChecksum, newChecksum) | ||||
| 	} else { | ||||
| 		fmt.Printf("[IDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum) | ||||
| 		fmt.Printf("[CSIDGenerate] Checksum unchanged ( %s ), nothing to do\n", oldChecksum) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -12,8 +12,8 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| //go:embed _test_example.tgz | ||||
| var CSIDExampleModels []byte | ||||
| //go:embed _test_example_1.tgz | ||||
| var CSIDExampleModels1 []byte | ||||
|  | ||||
| func TestGenerateCSIDSpecs(t *testing.T) { | ||||
|  | ||||
| @@ -21,7 +21,7 @@ func TestGenerateCSIDSpecs(t *testing.T) { | ||||
|  | ||||
| 	tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID()) | ||||
|  | ||||
| 	err := os.WriteFile(tmpFile, CSIDExampleModels, 0o777) | ||||
| 	err := os.WriteFile(tmpFile, CSIDExampleModels1, 0o777) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	t.Cleanup(func() { _ = os.Remove(tmpFile) }) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package bfcodegen | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	_ "embed" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"go/format" | ||||
| @@ -14,6 +15,7 @@ import ( | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| @@ -23,6 +25,8 @@ type EnumDefVal struct { | ||||
| 	VarName     string | ||||
| 	Value       string | ||||
| 	Description *string | ||||
| 	Data        *map[string]any | ||||
| 	RawComment  *string | ||||
| } | ||||
|  | ||||
| type EnumDef struct { | ||||
| @@ -37,7 +41,7 @@ var rexEnumPackage = rext.W(regexp.MustCompile(`^package\s+(?P<name>[A-Za-z0-9_] | ||||
|  | ||||
| var rexEnumDef = rext.W(regexp.MustCompile(`^\s*type\s+(?P<name>[A-Za-z0-9_]+)\s+(?P<type>[A-Za-z0-9_]+)\s*//\s*(@enum:type).*$`)) | ||||
|  | ||||
| var rexEnumValueDef = 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_:\s]+"|[0-9]+))\s*(//(?P<descr>.*))?.*$`)) | ||||
| var rexEnumValueDef = 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_:\s\-.]+"|[0-9]+))\s*(//(?P<comm>.*))?.*$`)) | ||||
|  | ||||
| var rexEnumChecksumConst = rext.W(regexp.MustCompile(`const ChecksumEnumGenerator = "(?P<cs>[A-Za-z0-9_]*)"`)) | ||||
|  | ||||
| @@ -46,11 +50,6 @@ var templateEnumGenerateText string | ||||
|  | ||||
| func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||
|  | ||||
| 	files, err := os.ReadDir(sourceDir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	oldChecksum := "N/A" | ||||
| 	if _, err := os.Stat(destFile); !os.IsNotExist(err) { | ||||
| 		content, err := os.ReadFile(destFile) | ||||
| @@ -62,6 +61,30 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	gocode, _, changed, err := _generateEnumSpecs(sourceDir, destFile, oldChecksum, true) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if !changed { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = os.WriteFile(destFile, []byte(gocode), 0o755) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func _generateEnumSpecs(sourceDir string, destFile string, oldChecksum string, gofmt bool) (string, string, bool, error) { | ||||
|  | ||||
| 	files, err := os.ReadDir(sourceDir) | ||||
| 	if err != nil { | ||||
| 		return "", "", false, err | ||||
| 	} | ||||
|  | ||||
| 	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") }) | ||||
| 	files = langext.ArrFilter(files, func(v os.DirEntry) bool { return !strings.HasSuffix(v.Name(), "_gen.go") }) | ||||
| @@ -71,7 +94,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||
| 	for _, f := range files { | ||||
| 		content, err := os.ReadFile(path.Join(sourceDir, f.Name())) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return "", "", false, err | ||||
| 		} | ||||
| 		newChecksumStr += "\n" + f.Name() + "\t" + cryptext.BytesSha256(content) | ||||
| 	} | ||||
| @@ -82,7 +105,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||
| 		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 | ||||
| 		return "", oldChecksum, false, nil | ||||
| 	} | ||||
|  | ||||
| 	allEnums := make([]EnumDef, 0) | ||||
| @@ -93,7 +116,7 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||
| 		fmt.Printf("========= %s =========\n\n", f.Name()) | ||||
| 		fileEnums, pn, err := processEnumFile(sourceDir, path.Join(sourceDir, f.Name())) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return "", "", false, err | ||||
| 		} | ||||
|  | ||||
| 		fmt.Printf("\n") | ||||
| @@ -106,20 +129,21 @@ func GenerateEnumSpecs(sourceDir string, destFile string) error { | ||||
| 	} | ||||
|  | ||||
| 	if pkgname == "" { | ||||
| 		return errors.New("no package name found in any file") | ||||
| 		return "", "", false, errors.New("no package name found in any file") | ||||
| 	} | ||||
|  | ||||
| 	fdata, err := format.Source([]byte(fmtEnumOutput(newChecksum, allEnums, pkgname))) | ||||
| 	rdata := fmtEnumOutput(newChecksum, allEnums, pkgname) | ||||
|  | ||||
| 	if !gofmt { | ||||
| 		return rdata, newChecksum, true, nil | ||||
| 	} | ||||
|  | ||||
| 	fdata, err := format.Source([]byte(rdata)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return "", "", false, err | ||||
| 	} | ||||
|  | ||||
| 	err = os.WriteFile(destFile, fdata, 0o755) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	return string(fdata), newChecksum, true, nil | ||||
| } | ||||
|  | ||||
| func processEnumFile(basedir string, fn string) ([]EnumDef, string, error) { | ||||
| @@ -171,10 +195,34 @@ func processEnumFile(basedir string, fn string) ([]EnumDef, string, error) { | ||||
|  | ||||
| 		if match, ok := rexEnumValueDef.MatchFirst(line); ok { | ||||
| 			typename := match.GroupByName("type").Value() | ||||
|  | ||||
| 			comment := match.GroupByNameOrEmpty("comm").ValueOrNil() | ||||
| 			var descr *string = nil | ||||
| 			var data *map[string]any = nil | ||||
| 			if comment != nil { | ||||
| 				comment = langext.Ptr(strings.TrimSpace(*comment)) | ||||
| 				if strings.HasPrefix(*comment, "{") { | ||||
| 					if v, ok := tryParseDataComment(*comment); ok { | ||||
| 						data = &v | ||||
| 						if anyDataDescr, ok := v["description"]; ok { | ||||
| 							if dataDescr, ok := anyDataDescr.(string); ok { | ||||
| 								descr = &dataDescr | ||||
| 							} | ||||
| 						} | ||||
| 					} else { | ||||
| 						descr = comment | ||||
| 					} | ||||
| 				} else { | ||||
| 					descr = comment | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			def := EnumDefVal{ | ||||
| 				VarName:     match.GroupByName("name").Value(), | ||||
| 				Value:       match.GroupByName("value").Value(), | ||||
| 				Description: match.GroupByNameOrEmpty("descr").ValueOrNil(), | ||||
| 				RawComment:  comment, | ||||
| 				Description: descr, | ||||
| 				Data:        data, | ||||
| 			} | ||||
|  | ||||
| 			found := false | ||||
| @@ -199,6 +247,41 @@ func processEnumFile(basedir string, fn string) ([]EnumDef, string, error) { | ||||
| 	return enums, pkgname, nil | ||||
| } | ||||
|  | ||||
| func tryParseDataComment(s string) (map[string]any, bool) { | ||||
|  | ||||
| 	r := make(map[string]any) | ||||
|  | ||||
| 	err := json.Unmarshal([]byte(s), &r) | ||||
| 	if err != nil { | ||||
| 		return nil, false | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range r { | ||||
|  | ||||
| 		rv := reflect.ValueOf(v) | ||||
|  | ||||
| 		if rv.Kind() == reflect.Ptr && rv.IsNil() { | ||||
| 			continue | ||||
| 		} | ||||
| 		if rv.Kind() == reflect.Bool { | ||||
| 			continue | ||||
| 		} | ||||
| 		if rv.Kind() == reflect.String { | ||||
| 			continue | ||||
| 		} | ||||
| 		if rv.Kind() == reflect.Int64 { | ||||
| 			continue | ||||
| 		} | ||||
| 		if rv.Kind() == reflect.Float64 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		return nil, false | ||||
| 	} | ||||
|  | ||||
| 	return r, true | ||||
| } | ||||
|  | ||||
| func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string { | ||||
|  | ||||
| 	templ := template.New("enum-generate") | ||||
| @@ -211,6 +294,47 @@ func fmtEnumOutput(cs string, enums []EnumDef, pkgname string) string { | ||||
| 		"hasDescr": func(v EnumDef) bool { | ||||
| 			return langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Description != nil }) | ||||
| 		}, | ||||
| 		"hasData": func(v EnumDef) bool { | ||||
| 			return len(v.Values) > 0 && langext.ArrAll(v.Values, func(val EnumDefVal) bool { return val.Data != nil }) | ||||
| 		}, | ||||
| 		"gostr": func(v any) string { | ||||
| 			return fmt.Sprintf("%#+v", v) | ||||
| 		}, | ||||
| 		"goobj": func(name string, v any) string { | ||||
| 			return fmt.Sprintf("%#+v", v) | ||||
| 		}, | ||||
| 		"godatakey": func(v string) string { | ||||
| 			return strings.ToUpper(v[0:1]) + v[1:] | ||||
| 		}, | ||||
| 		"godatavalue": func(v any) string { | ||||
| 			return fmt.Sprintf("%#+v", v) | ||||
| 		}, | ||||
| 		"godatatype": func(v any) string { | ||||
| 			return fmt.Sprintf("%T", v) | ||||
| 		}, | ||||
| 		"mapindex": func(v map[string]any, k string) any { | ||||
| 			return v[k] | ||||
| 		}, | ||||
| 		"generalDataKeys": func(v EnumDef) map[string]string { | ||||
| 			r0 := make(map[string]int) | ||||
|  | ||||
| 			for _, eval := range v.Values { | ||||
| 				for k := range *eval.Data { | ||||
| 					if ctr, ok := r0[k]; ok { | ||||
| 						r0[k] = ctr + 1 | ||||
| 					} else { | ||||
| 						r0[k] = 1 | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			r1 := langext.MapToArr(r0) | ||||
| 			r2 := langext.ArrFilter(r1, func(p langext.MapEntry[string, int]) bool { return p.Value == len(v.Values) }) | ||||
| 			r3 := langext.ArrMap(r2, func(p langext.MapEntry[string, int]) string { return p.Key }) | ||||
| 			r4 := langext.ArrToKVMap(r3, func(p string) string { return p }, func(p string) string { return fmt.Sprintf("%T", (*v.Values[0].Data)[p]) }) | ||||
|  | ||||
| 			return r4 | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	templ = template.Must(templ.Parse(templateEnumGenerateText)) | ||||
|   | ||||
| @@ -11,21 +11,38 @@ const ChecksumEnumGenerator = "{{.Checksum}}" // GoExtVersion: {{.GoextVersion}} | ||||
|  | ||||
| {{ $hasStr   := ( . | hasStr   ) }} | ||||
| {{ $hasDescr := ( . | hasDescr ) }} | ||||
| {{ $hasData  := ( . | hasData  ) }} | ||||
|  | ||||
| // ================================ {{.EnumTypeName}} ================================ | ||||
| // | ||||
| // File:       {{.FileRelative}} | ||||
| // StringEnum: {{$hasStr   | boolToStr}} | ||||
| // DescrEnum:  {{$hasDescr | boolToStr}} | ||||
| // DataEnum:   {{$hasData  | boolToStr}} | ||||
| // | ||||
|  | ||||
| {{ $typename := .EnumTypeName }} | ||||
| {{ $enumdef  := . }} | ||||
|  | ||||
| var __{{.EnumTypeName}}Values = []{{.EnumTypeName}}{ {{range .Values}} | ||||
| 	{{.VarName}}, {{end}} | ||||
| } | ||||
|  | ||||
| {{if $hasDescr}} | ||||
| var __{{.EnumTypeName}}Descriptions = map[{{.EnumTypeName}}]string{ {{range .Values}} | ||||
| 	{{.VarName}}: "{{.Description | deref | trimSpace}}", {{end}} | ||||
| 	{{.VarName}}: {{.Description | deref | trimSpace | gostr}}, {{end}} | ||||
| } | ||||
| {{end}} | ||||
|  | ||||
| {{if $hasData}} | ||||
| type {{ .EnumTypeName }}Data struct { {{ range $datakey, $datatype := ($enumdef | generalDataKeys)  }} | ||||
|     {{ $datakey | godatakey }} {{ $datatype }} `json:"{{ $datakey }}"` {{ end }} | ||||
| } | ||||
|  | ||||
| var __{{.EnumTypeName}}Data = map[{{.EnumTypeName}}]{{.EnumTypeName}}Data{ {{range .Values}} {{ $enumvalue := . }} | ||||
| 	{{.VarName}}: {{ $typename }}Data{ {{ range $datakey, $datatype := $enumdef | generalDataKeys  }} | ||||
| 	    {{ $datakey | godatakey }}: {{ (mapindex $enumvalue.Data $datakey) | godatavalue }}, {{ end }} | ||||
| 	}, {{end}} | ||||
| } | ||||
| {{end}} | ||||
|  | ||||
| @@ -64,6 +81,15 @@ func (e {{.EnumTypeName}}) Description() string { | ||||
| } | ||||
| {{end}} | ||||
|  | ||||
| {{if $hasData}} | ||||
| func (e {{.EnumTypeName}}) Data() {{.EnumTypeName}}Data { | ||||
| 	if d, ok := __{{.EnumTypeName}}Data[e]; ok { | ||||
| 		return d | ||||
| 	} | ||||
| 	return {{.EnumTypeName}}Data{} | ||||
| } | ||||
| {{end}} | ||||
|  | ||||
| func (e {{.EnumTypeName}}) VarName() string { | ||||
| 	if d, ok := __{{.EnumTypeName}}Varnames[e]; ok { | ||||
| 		return d | ||||
| @@ -75,6 +101,12 @@ func (e {{.EnumTypeName}}) Meta() enums.EnumMetaValue { | ||||
|     {{if $hasDescr}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: langext.Ptr(e.Description())} {{else}} return enums.EnumMetaValue{VarName: e.VarName(), Value: e, Description: nil} {{end}} | ||||
| } | ||||
|  | ||||
| {{if $hasDescr}} | ||||
| func (e {{.EnumTypeName}}) DescriptionMeta() enums.EnumDescriptionMetaValue { | ||||
|     return enums.EnumDescriptionMetaValue{VarName: e.VarName(), Value: e, Description: e.Description()} | ||||
| } | ||||
| {{end}} | ||||
|  | ||||
| func Parse{{.EnumTypeName}}(vv string) ({{.EnumTypeName}}, bool) { | ||||
| 	for _, ev := range __{{.EnumTypeName}}Values { | ||||
| 		if string(ev) == vv { | ||||
| @@ -94,4 +126,12 @@ func {{.EnumTypeName}}ValuesMeta() []enums.EnumMetaValue { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| {{if $hasDescr}} | ||||
| func {{.EnumTypeName}}ValuesDescriptionMeta() []enums.EnumDescriptionMetaValue { | ||||
| 	return []enums.EnumDescriptionMetaValue{ {{range .Values}} | ||||
|             {{.VarName}}.DescriptionMeta(), {{end}} | ||||
| 	} | ||||
| } | ||||
| {{end}} | ||||
|  | ||||
| {{end}} | ||||
| @@ -12,8 +12,11 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| //go:embed _test_example.tgz | ||||
| var EnumExampleModels []byte | ||||
| //go:embed _test_example_1.tgz | ||||
| var EnumExampleModels1 []byte | ||||
|  | ||||
| //go:embed _test_example_2.tgz | ||||
| var EnumExampleModels2 []byte | ||||
|  | ||||
| func TestGenerateEnumSpecs(t *testing.T) { | ||||
|  | ||||
| @@ -21,7 +24,7 @@ func TestGenerateEnumSpecs(t *testing.T) { | ||||
|  | ||||
| 	tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID()) | ||||
|  | ||||
| 	err := os.WriteFile(tmpFile, EnumExampleModels, 0o777) | ||||
| 	err := os.WriteFile(tmpFile, EnumExampleModels1, 0o777) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	t.Cleanup(func() { _ = os.Remove(tmpFile) }) | ||||
| @@ -34,17 +37,53 @@ func TestGenerateEnumSpecs(t *testing.T) { | ||||
| 	_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run() | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	err = GenerateEnumSpecs(tmpDir, tmpDir+"/enums_gen.go") | ||||
| 	s1, cs1, _, err := _generateEnumSpecs(tmpDir, "", "N/A", true) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	err = GenerateEnumSpecs(tmpDir, tmpDir+"/enums_gen.go") | ||||
| 	s2, cs2, _, err := _generateEnumSpecs(tmpDir, "", "N/A", true) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	tst.AssertEqual(t, cs1, cs2) | ||||
| 	tst.AssertEqual(t, s1, s2) | ||||
|  | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
| 	fmt.Println("=====================================================================================================") | ||||
| 	fmt.Println(string(tst.Must(os.ReadFile(tmpDir + "/enums_gen.go"))(t))) | ||||
| 	fmt.Println(s1) | ||||
| 	fmt.Println("=====================================================================================================") | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
| } | ||||
|  | ||||
| func TestGenerateEnumSpecsData(t *testing.T) { | ||||
|  | ||||
| 	tmpFile := filepath.Join(t.TempDir(), langext.MustHexUUID()+".tgz") | ||||
|  | ||||
| 	tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID()) | ||||
|  | ||||
| 	err := os.WriteFile(tmpFile, EnumExampleModels2, 0o777) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	t.Cleanup(func() { _ = os.Remove(tmpFile) }) | ||||
|  | ||||
| 	err = os.Mkdir(tmpDir, 0o777) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	t.Cleanup(func() { _ = os.RemoveAll(tmpFile) }) | ||||
|  | ||||
| 	_, err = cmdext.Runner("tar").Arg("-xvzf").Arg(tmpFile).Arg("-C").Arg(tmpDir).FailOnExitCode().FailOnTimeout().Timeout(time.Minute).Run() | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	s1, _, _, err := _generateEnumSpecs(tmpDir, "", "", true) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
| 	fmt.Println("=====================================================================================================") | ||||
| 	fmt.Println(s1) | ||||
| 	fmt.Println("=====================================================================================================") | ||||
| 	fmt.Println() | ||||
| 	fmt.Println() | ||||
|   | ||||
| @@ -12,8 +12,8 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| //go:embed _test_example.tgz | ||||
| var IDExampleModels []byte | ||||
| //go:embed _test_example_1.tgz | ||||
| var IDExampleModels1 []byte | ||||
|  | ||||
| func TestGenerateIDSpecs(t *testing.T) { | ||||
|  | ||||
| @@ -21,7 +21,7 @@ func TestGenerateIDSpecs(t *testing.T) { | ||||
|  | ||||
| 	tmpDir := filepath.Join(t.TempDir(), langext.MustHexUUID()) | ||||
|  | ||||
| 	err := os.WriteFile(tmpFile, IDExampleModels, 0o777) | ||||
| 	err := os.WriteFile(tmpFile, IDExampleModels1, 0o777) | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	t.Cleanup(func() { _ = os.Remove(tmpFile) }) | ||||
|   | ||||
| @@ -15,10 +15,17 @@ type StringEnum interface { | ||||
| type DescriptionEnum interface { | ||||
| 	Enum | ||||
| 	Description() string | ||||
| 	DescriptionMeta() EnumDescriptionMetaValue | ||||
| } | ||||
|  | ||||
| type EnumMetaValue struct { | ||||
| 	VarName     string  `json:"varName"` | ||||
| 	Value       any     `json:"value"` | ||||
| 	Value       Enum    `json:"value"` | ||||
| 	Description *string `json:"description"` | ||||
| } | ||||
|  | ||||
| type EnumDescriptionMetaValue struct { | ||||
| 	VarName     string `json:"varName"` | ||||
| 	Value       Enum   `json:"value"` | ||||
| 	Description string `json:"description"` | ||||
| } | ||||
|   | ||||
| @@ -70,6 +70,7 @@ func init() { | ||||
| type Builder struct { | ||||
| 	errorData       *ExErr | ||||
| 	containsGinData bool | ||||
| 	noLog           bool | ||||
| } | ||||
|  | ||||
| func Get(err error) *Builder { | ||||
| @@ -190,6 +191,13 @@ func (b *Builder) System() *Builder { | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| func (b *Builder) NoLog() *Builder { | ||||
| 	b.noLog = true | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| // ---------------------------------------------------------------------------- | ||||
|  | ||||
| func (b *Builder) Id(key string, val fmt.Stringer) *Builder { | ||||
| 	return b.addMeta(key, MDTID, newIDWrap(val)) | ||||
| } | ||||
| @@ -401,12 +409,14 @@ func extractHeader(header map[string][]string) []string { | ||||
|  | ||||
| // Build creates a new error, ready to pass up the stack | ||||
| // If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout | ||||
| // Can be gloablly configured with ZeroLogErrTraces and ZeroLogAllTraces | ||||
| // Can be locally suppressed with Builder.NoLog() | ||||
| func (b *Builder) Build() error { | ||||
| 	warnOnPkgConfigNotInitialized() | ||||
|  | ||||
| 	if pkgconfig.ZeroLogErrTraces && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) { | ||||
| 	if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) { | ||||
| 		b.errorData.ShortLog(stackSkipLogger.Error()) | ||||
| 	} else if pkgconfig.ZeroLogAllTraces { | ||||
| 	} else if pkgconfig.ZeroLogAllTraces && !b.noLog { | ||||
| 		b.errorData.ShortLog(stackSkipLogger.Error()) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -59,6 +59,9 @@ var ( | ||||
| 	TypeMarshalEntityID = NewType("MARSHAL_ENTITY_ID", langext.Ptr(400)) | ||||
| 	TypeInvalidCSID     = NewType("INVALID_CSID", langext.Ptr(400)) | ||||
|  | ||||
| 	TypeGoogleStatuscode = NewType("GOOGLE_STATUSCODE", langext.Ptr(400)) | ||||
| 	TypeGoogleResponse   = NewType("GOOGLE_RESPONSE", langext.Ptr(400)) | ||||
|  | ||||
| 	TypeUnauthorized = NewType("UNAUTHORIZED", langext.Ptr(401)) | ||||
| 	TypeAuthFailed   = NewType("AUTH_FAILED", langext.Ptr(401)) | ||||
|  | ||||
|   | ||||
| @@ -68,6 +68,10 @@ func Init(cfg ErrorPackageConfigInit) { | ||||
| 	initialized = true | ||||
| } | ||||
|  | ||||
| func Initialized() bool { | ||||
| 	return initialized | ||||
| } | ||||
|  | ||||
| func warnOnPkgConfigNotInitialized() { | ||||
| 	if !initialized { | ||||
| 		fmt.Printf("\n") | ||||
|   | ||||
							
								
								
									
										13
									
								
								exerr/gin.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								exerr/gin.go
									
									
									
									
									
								
							| @@ -57,6 +57,19 @@ func (ee *ExErr) toJson(depth int, applyExtendListener bool, outputMeta bool) la | ||||
| 	return ginJson | ||||
| } | ||||
|  | ||||
| func (ee *ExErr) ToDefaultAPIJson() (string, error) { | ||||
|  | ||||
| 	gjr := json.GoJsonRender{Data: ee.ToAPIJson(true, pkgconfig.ExtendedGinOutput, pkgconfig.IncludeMetaInGinOutput), NilSafeSlices: true, NilSafeMaps: true} | ||||
|  | ||||
| 	r, err := gjr.RenderString() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return r, nil | ||||
| } | ||||
|  | ||||
| // ToAPIJson converts the ExError to a json object | ||||
| // (the same object as used in the Output(gin) method) | ||||
| // | ||||
|   | ||||
| @@ -6,8 +6,10 @@ import ( | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/mathext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rext" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| @@ -20,6 +22,8 @@ type GinWrapper struct { | ||||
| 	ginDebug              bool | ||||
| 	bufferBody            bool | ||||
| 	requestTimeout        time.Duration | ||||
| 	listenerBeforeRequest []func(g *gin.Context) | ||||
| 	listenerAfterRequest  []func(g *gin.Context, resp HTTPResponse) | ||||
|  | ||||
| 	routeSpecs []ginRouteSpec | ||||
| } | ||||
| @@ -31,35 +35,41 @@ type ginRouteSpec struct { | ||||
| 	Handler     string | ||||
| } | ||||
|  | ||||
| type Options struct { | ||||
| 	AllowCors             *bool                                     // Add cors handler to allow all CORS requests on the default http methods | ||||
| 	GinDebug              *bool                                     // Set gin.debug to true (adds more logs) | ||||
| 	BufferBody            *bool                                     // Buffers the input body stream, this way the ginext error handler can later include the whole request body | ||||
| 	Timeout               *time.Duration                            // The default handler timeout | ||||
| 	ListenerBeforeRequest []func(g *gin.Context)                    // Register listener that are called before the handler method | ||||
| 	ListenerAfterRequest  []func(g *gin.Context, resp HTTPResponse) // Register listener that are called after the handler method | ||||
| } | ||||
|  | ||||
| // NewEngine creates a new (wrapped) ginEngine | ||||
| // Parameters are: | ||||
| // - [allowCors]    Add cors handler to allow all CORS requests on the default http methods | ||||
| // - [ginDebug]     Set gin.debug to true (adds more logs) | ||||
| // - [bufferBody]   Buffers the input body stream, this way the ginext error handler can later include the whole request body | ||||
| // - [timeout]      The default handler timeout | ||||
| func NewEngine(allowCors bool, ginDebug bool, bufferBody bool, timeout time.Duration) *GinWrapper { | ||||
| func NewEngine(opt Options) *GinWrapper { | ||||
| 	engine := gin.New() | ||||
|  | ||||
| 	wrapper := &GinWrapper{ | ||||
| 		engine:                engine, | ||||
| 		SuppressGinLogs:       false, | ||||
| 		allowCors:       allowCors, | ||||
| 		ginDebug:        ginDebug, | ||||
| 		bufferBody:      bufferBody, | ||||
| 		requestTimeout:  timeout, | ||||
| 		allowCors:             langext.Coalesce(opt.AllowCors, false), | ||||
| 		ginDebug:              langext.Coalesce(opt.GinDebug, true), | ||||
| 		bufferBody:            langext.Coalesce(opt.BufferBody, false), | ||||
| 		requestTimeout:        langext.Coalesce(opt.Timeout, 24*time.Hour), | ||||
| 		listenerBeforeRequest: opt.ListenerBeforeRequest, | ||||
| 		listenerAfterRequest:  opt.ListenerAfterRequest, | ||||
| 	} | ||||
|  | ||||
| 	engine.RedirectFixedPath = false | ||||
| 	engine.RedirectTrailingSlash = false | ||||
|  | ||||
| 	if allowCors { | ||||
| 	if wrapper.allowCors { | ||||
| 		engine.Use(CorsMiddleware()) | ||||
| 	} | ||||
|  | ||||
| 	// do not debug-print routes | ||||
| 	gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {} | ||||
|  | ||||
| 	if !ginDebug { | ||||
| 	if !wrapper.ginDebug { | ||||
| 		gin.SetMode(gin.ReleaseMode) | ||||
|  | ||||
| 		ginlogger := gin.Logger() | ||||
| @@ -126,8 +136,8 @@ func (w *GinWrapper) DebugPrintRoutes() { | ||||
| 		line := [4]string{ | ||||
| 			spec.Method, | ||||
| 			spec.URL, | ||||
| 			strings.Join(spec.Middlewares, " -> "), | ||||
| 			spec.Handler, | ||||
| 			strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "), | ||||
| 			w.cleanMiddlewareName(spec.Handler), | ||||
| 		} | ||||
|  | ||||
| 		lines = append(lines, line) | ||||
| @@ -138,12 +148,40 @@ func (w *GinWrapper) DebugPrintRoutes() { | ||||
| 		pad[3] = mathext.Max(pad[3], len(line[3])) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Printf("Gin-Routes:\n") | ||||
| 	fmt.Printf("{\n") | ||||
| 	for _, line := range lines { | ||||
|  | ||||
| 		fmt.Printf("Gin-Route: %s  %s  -->  %s  -->  %s\n", | ||||
| 		fmt.Printf(" %s  %s  -->  %s  -->  %s\n", | ||||
| 			langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2), | ||||
| 			langext.StrPadRight(line[1], " ", pad[1]), | ||||
| 			langext.StrPadRight(line[2], " ", pad[2]), | ||||
| 			langext.StrPadRight(line[3], " ", pad[3])) | ||||
| 	} | ||||
| 	fmt.Printf("}\n") | ||||
| } | ||||
|  | ||||
| func (w *GinWrapper) cleanMiddlewareName(fname string) string { | ||||
|  | ||||
| 	funcSuffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`)) | ||||
| 	if match, ok := funcSuffix.MatchFirst(fname); ok { | ||||
| 		fname = fname[:len(fname)-match.FullMatch().Length()] | ||||
| 	} | ||||
|  | ||||
| 	if strings.HasSuffix(fname, ".(*GinRoutesWrapper).WithJSONFilter") { | ||||
| 		fname = "[JSONFilter]" | ||||
| 	} | ||||
|  | ||||
| 	if fname == "ginext.BodyBuffer" { | ||||
| 		fname = "[BodyBuffer]" | ||||
| 	} | ||||
|  | ||||
| 	skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."} | ||||
| 	for _, pfx := range skipPrefixes { | ||||
| 		if strings.HasPrefix(fname, pfx) { | ||||
| 			fname = fname[len(pfx):] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return fname | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,17 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc { | ||||
|  | ||||
| 		reqctx := g.Request.Context() | ||||
|  | ||||
| 		wrap, stackTrace, panicObj := callPanicSafe(fn, PreContext{wrapper: w, ginCtx: g}) | ||||
| 		pctx := PreContext{ | ||||
| 			wrapper:        w, | ||||
| 			ginCtx:         g, | ||||
| 			persistantData: &preContextData{}, | ||||
| 		} | ||||
|  | ||||
| 		for _, lstr := range w.listenerBeforeRequest { | ||||
| 			lstr(g) | ||||
| 		} | ||||
|  | ||||
| 		wrap, stackTrace, panicObj := callPanicSafe(fn, pctx) | ||||
| 		if panicObj != nil { | ||||
|  | ||||
| 			fmt.Printf("\n======== ======== STACKTRACE ======== ========\n%s\n======== ======== ======== ========\n\n", stackTrace) | ||||
| @@ -32,6 +42,17 @@ func Wrap(w *GinWrapper, fn WHandlerFunc) gin.HandlerFunc { | ||||
| 			panic("Writing in WrapperFunc is not supported") | ||||
| 		} | ||||
|  | ||||
| 		if pctx.persistantData.sessionObj != nil { | ||||
| 			err := pctx.persistantData.sessionObj.Finish(reqctx, wrap) | ||||
| 			if err != nil { | ||||
| 				wrap = Error(exerr.Wrap(err, "Failed to finish session").Any("originalResponse", wrap).Build()) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for _, lstr := range w.listenerAfterRequest { | ||||
| 			lstr(g, wrap) | ||||
| 		} | ||||
|  | ||||
| 		if reqctx.Err() == nil { | ||||
| 			wrap.Write(g) | ||||
| 		} | ||||
|   | ||||
| @@ -24,6 +24,11 @@ type PreContext struct { | ||||
| 	form           any | ||||
| 	header         any | ||||
| 	timeout        *time.Duration | ||||
| 	persistantData *preContextData // must be a ptr, so that we can get the values back in out Wrap func | ||||
| } | ||||
|  | ||||
| type preContextData struct { | ||||
| 	sessionObj SessionObject | ||||
| } | ||||
|  | ||||
| func (pctx *PreContext) URI(uri any) *PreContext { | ||||
| @@ -61,6 +66,11 @@ func (pctx *PreContext) WithTimeout(to time.Duration) *PreContext { | ||||
| 	return pctx | ||||
| } | ||||
|  | ||||
| func (pctx *PreContext) WithSession(sessionObj SessionObject) *PreContext { | ||||
| 	pctx.persistantData.sessionObj = sessionObj | ||||
| 	return pctx | ||||
| } | ||||
|  | ||||
| func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) { | ||||
| 	if pctx.uri != nil { | ||||
| 		if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil { | ||||
| @@ -152,6 +162,15 @@ func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) { | ||||
| 	} | ||||
|  | ||||
| 	ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout)) | ||||
|  | ||||
| 	if pctx.persistantData.sessionObj != nil { | ||||
| 		err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, ictx) | ||||
| 		if err != nil { | ||||
| 			cancel() | ||||
| 			return nil, nil, langext.Ptr(Error(exerr.Wrap(err, "Failed to init session").Build())) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	actx := CreateAppContext(pctx.ginCtx, ictx, cancel) | ||||
|  | ||||
| 	return actx, pctx.ginCtx, nil | ||||
|   | ||||
| @@ -5,6 +5,8 @@ import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	json "gogs.mikescher.com/BlackForestBytes/goext/gojson" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| type headerval struct { | ||||
| @@ -15,6 +17,16 @@ type headerval struct { | ||||
| type HTTPResponse interface { | ||||
| 	Write(g *gin.Context) | ||||
| 	WithHeader(k string, v string) HTTPResponse | ||||
| 	IsSuccess() bool | ||||
| } | ||||
|  | ||||
| type InspectableHTTPResponse interface { | ||||
| 	HTTPResponse | ||||
|  | ||||
| 	Statuscode() int | ||||
| 	BodyString(g *gin.Context) *string | ||||
| 	ContentType() string | ||||
| 	Headers() []string | ||||
| } | ||||
|  | ||||
| type jsonHTTPResponse struct { | ||||
| @@ -23,15 +35,19 @@ type jsonHTTPResponse struct { | ||||
| 	headers    []headerval | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) Write(g *gin.Context) { | ||||
| 	for _, v := range j.headers { | ||||
| 		g.Header(v.Key, v.Val) | ||||
| 	} | ||||
| func (j jsonHTTPResponse) jsonRenderer(g *gin.Context) json.GoJsonRender { | ||||
| 	var f *string | ||||
| 	if jsonfilter := g.GetString("goext.jsonfilter"); jsonfilter != "" { | ||||
| 		f = &jsonfilter | ||||
| 	} | ||||
| 	g.Render(j.statusCode, json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true, Filter: f}) | ||||
| 	return json.GoJsonRender{Data: j.data, NilSafeSlices: true, NilSafeMaps: true, Filter: f} | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) Write(g *gin.Context) { | ||||
| 	for _, v := range j.headers { | ||||
| 		g.Header(v.Key, v.Val) | ||||
| 	} | ||||
| 	g.Render(j.statusCode, j.jsonRenderer(g)) | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| @@ -39,6 +55,30 @@ func (j jsonHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) IsSuccess() bool { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) Statuscode() int { | ||||
| 	return j.statusCode | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) BodyString(g *gin.Context) *string { | ||||
| 	if str, err := j.jsonRenderer(g).RenderString(); err == nil { | ||||
| 		return &str | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) ContentType() string { | ||||
| 	return "application/json" | ||||
| } | ||||
|  | ||||
| func (j jsonHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type emptyHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	headers    []headerval | ||||
| @@ -56,6 +96,26 @@ func (j emptyHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j emptyHTTPResponse) IsSuccess() bool { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
|  | ||||
| func (j emptyHTTPResponse) Statuscode() int { | ||||
| 	return j.statusCode | ||||
| } | ||||
|  | ||||
| func (j emptyHTTPResponse) BodyString(*gin.Context) *string { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (j emptyHTTPResponse) ContentType() string { | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (j emptyHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type textHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	data       string | ||||
| @@ -74,6 +134,26 @@ func (j textHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j textHTTPResponse) IsSuccess() bool { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
|  | ||||
| func (j textHTTPResponse) Statuscode() int { | ||||
| 	return j.statusCode | ||||
| } | ||||
|  | ||||
| func (j textHTTPResponse) BodyString(*gin.Context) *string { | ||||
| 	return langext.Ptr(j.data) | ||||
| } | ||||
|  | ||||
| func (j textHTTPResponse) ContentType() string { | ||||
| 	return "text/plain" | ||||
| } | ||||
|  | ||||
| func (j textHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type dataHTTPResponse struct { | ||||
| 	statusCode  int | ||||
| 	data        []byte | ||||
| @@ -93,6 +173,26 @@ func (j dataHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j dataHTTPResponse) IsSuccess() bool { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
|  | ||||
| func (j dataHTTPResponse) Statuscode() int { | ||||
| 	return j.statusCode | ||||
| } | ||||
|  | ||||
| func (j dataHTTPResponse) BodyString(*gin.Context) *string { | ||||
| 	return langext.Ptr(string(j.data)) | ||||
| } | ||||
|  | ||||
| func (j dataHTTPResponse) ContentType() string { | ||||
| 	return j.contentType | ||||
| } | ||||
|  | ||||
| func (j dataHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type fileHTTPResponse struct { | ||||
| 	mimetype string | ||||
| 	filepath string | ||||
| @@ -117,6 +217,30 @@ func (j fileHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j fileHTTPResponse) IsSuccess() bool { | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (j fileHTTPResponse) Statuscode() int { | ||||
| 	return 200 | ||||
| } | ||||
|  | ||||
| func (j fileHTTPResponse) BodyString(*gin.Context) *string { | ||||
| 	data, err := os.ReadFile(j.filepath) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return langext.Ptr(string(data)) | ||||
| } | ||||
|  | ||||
| func (j fileHTTPResponse) ContentType() string { | ||||
| 	return j.mimetype | ||||
| } | ||||
|  | ||||
| func (j fileHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type downloadDataHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	mimetype   string | ||||
| @@ -142,6 +266,26 @@ func (j downloadDataHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j downloadDataHTTPResponse) IsSuccess() bool { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
|  | ||||
| func (j downloadDataHTTPResponse) Statuscode() int { | ||||
| 	return j.statusCode | ||||
| } | ||||
|  | ||||
| func (j downloadDataHTTPResponse) BodyString(*gin.Context) *string { | ||||
| 	return langext.Ptr(string(j.data)) | ||||
| } | ||||
|  | ||||
| func (j downloadDataHTTPResponse) ContentType() string { | ||||
| 	return j.mimetype | ||||
| } | ||||
|  | ||||
| func (j downloadDataHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type redirectHTTPResponse struct { | ||||
| 	statusCode int | ||||
| 	url        string | ||||
| @@ -157,6 +301,26 @@ func (j redirectHTTPResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j redirectHTTPResponse) IsSuccess() bool { | ||||
| 	return j.statusCode >= 200 && j.statusCode <= 399 | ||||
| } | ||||
|  | ||||
| func (j redirectHTTPResponse) Statuscode() int { | ||||
| 	return j.statusCode | ||||
| } | ||||
|  | ||||
| func (j redirectHTTPResponse) BodyString(*gin.Context) *string { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (j redirectHTTPResponse) ContentType() string { | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (j redirectHTTPResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| type jsonAPIErrResponse struct { | ||||
| 	err     *exerr.ExErr | ||||
| 	headers []headerval | ||||
| @@ -171,6 +335,30 @@ func (j jsonAPIErrResponse) WithHeader(k string, v string) HTTPResponse { | ||||
| 	return j | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) IsSuccess() bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) Statuscode() int { | ||||
| 	return langext.Coalesce(j.err.RecursiveStatuscode(), 0) | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) BodyString(*gin.Context) *string { | ||||
| 	if str, err := j.err.ToDefaultAPIJson(); err == nil { | ||||
| 		return &str | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) ContentType() string { | ||||
| 	return "application/json" | ||||
| } | ||||
|  | ||||
| func (j jsonAPIErrResponse) Headers() []string { | ||||
| 	return langext.ArrMap(j.headers, func(v headerval) string { return v.Key + "=" + v.Val }) | ||||
| } | ||||
|  | ||||
| func Status(sc int) HTTPResponse { | ||||
| 	return &emptyHTTPResponse{statusCode: sc} | ||||
| } | ||||
|   | ||||
| @@ -3,11 +3,9 @@ package ginext | ||||
| import ( | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rext" | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 	"reflect" | ||||
| 	"regexp" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| ) | ||||
| @@ -196,12 +194,6 @@ func nameOfFunction(f any) string { | ||||
| 		fname = fname[:len(fname)-len("-fm")] | ||||
| 	} | ||||
|  | ||||
| 	suffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`)) | ||||
|  | ||||
| 	if match, ok := suffix.MatchFirst(fname); ok { | ||||
| 		fname = fname[:len(fname)-match.FullMatch().Length()] | ||||
| 	} | ||||
|  | ||||
| 	return fname | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										11
									
								
								ginext/session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ginext/session.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| package ginext | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| ) | ||||
|  | ||||
| type SessionObject interface { | ||||
| 	Init(g *gin.Context, ctx context.Context) error | ||||
| 	Finish(ctx context.Context, resp HTTPResponse) error | ||||
| } | ||||
							
								
								
									
										20
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,16 +1,16 @@ | ||||
| module gogs.mikescher.com/BlackForestBytes/goext | ||||
|  | ||||
| go 1.19 | ||||
| go 1.21 | ||||
|  | ||||
| require ( | ||||
| 	github.com/gin-gonic/gin v1.9.1 | ||||
| 	github.com/jmoiron/sqlx v1.3.5 | ||||
| 	github.com/rs/xid v1.5.0 | ||||
| 	github.com/rs/zerolog v1.31.0 | ||||
| 	go.mongodb.org/mongo-driver v1.13.0 | ||||
| 	golang.org/x/crypto v0.15.0 | ||||
| 	golang.org/x/sys v0.14.0 | ||||
| 	golang.org/x/term v0.14.0 | ||||
| 	go.mongodb.org/mongo-driver v1.13.1 | ||||
| 	golang.org/x/crypto v0.17.0 | ||||
| 	golang.org/x/sys v0.15.0 | ||||
| 	golang.org/x/term v0.15.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| @@ -25,7 +25,7 @@ require ( | ||||
| 	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.17.2 // indirect | ||||
| 	github.com/klauspost/compress v1.17.4 // indirect | ||||
| 	github.com/klauspost/cpuid/v2 v2.2.6 // indirect | ||||
| 	github.com/leodido/go-urn v1.2.4 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| @@ -33,17 +33,17 @@ require ( | ||||
| 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||||
| 	github.com/modern-go/reflect2 v1.0.2 // indirect | ||||
| 	github.com/montanaflynn/stats v0.7.1 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.1.0 // indirect | ||||
| 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect | ||||
| 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.11 // indirect | ||||
| 	github.com/ugorji/go/codec v1.2.12 // indirect | ||||
| 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect | ||||
| 	github.com/xdg-go/scram v1.1.2 // indirect | ||||
| 	github.com/xdg-go/stringprep v1.0.4 // indirect | ||||
| 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect | ||||
| 	golang.org/x/arch v0.6.0 // indirect | ||||
| 	golang.org/x/net v0.18.0 // indirect | ||||
| 	golang.org/x/net v0.19.0 // indirect | ||||
| 	golang.org/x/sync v0.5.0 // indirect | ||||
| 	golang.org/x/text v0.14.0 // indirect | ||||
| 	google.golang.org/protobuf v1.31.0 // indirect | ||||
| 	google.golang.org/protobuf v1.32.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										22
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								go.sum
									
									
									
									
									
								
							| @@ -55,6 +55,10 @@ github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2g | ||||
| github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||
| github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= | ||||
| github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= | ||||
| github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= | ||||
| github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | ||||
| github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= | ||||
| github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= | ||||
| github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= | ||||
| github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= | ||||
| @@ -84,6 +88,8 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 | ||||
| github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= | ||||
| github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= | ||||
| github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | ||||
| github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= | ||||
| github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= | ||||
| 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= | ||||
| @@ -106,6 +112,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS | ||||
| 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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||
| github.com/ugorji/go/codec v1.2.12/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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= | ||||
| @@ -120,6 +128,8 @@ go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecq | ||||
| go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= | ||||
| go.mongodb.org/mongo-driver v1.13.0 h1:67DgFFjYOCMWdtTEmKFpV3ffWlFnh+CYZ8ZS/tXWUfY= | ||||
| go.mongodb.org/mongo-driver v1.13.0/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= | ||||
| go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= | ||||
| go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= | ||||
| golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= | ||||
| golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= | ||||
| @@ -133,6 +143,10 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= | ||||
| golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= | ||||
| golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= | ||||
| golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= | ||||
| golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= | ||||
| golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | ||||
| golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= | ||||
| golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= | ||||
| golang.org/x/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= | ||||
| @@ -145,6 +159,8 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= | ||||
| golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= | ||||
| golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= | ||||
| golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= | ||||
| golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= | ||||
| golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= | ||||
| @@ -166,12 +182,16 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= | ||||
| golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= | ||||
| golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | ||||
| golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= | ||||
| golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= | ||||
| golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= | ||||
| golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= | ||||
| golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= | ||||
| golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= | ||||
| golang.org/x/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= | ||||
| @@ -191,6 +211,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T | ||||
| google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= | ||||
| google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= | ||||
| google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= | ||||
| google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= | ||||
| google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| package goext | ||||
|  | ||||
| const GoextVersion = "0.0.312" | ||||
| const GoextVersion = "0.0.349" | ||||
|  | ||||
| const GoextVersionTimestamp = "2023-11-10T10:16:31+0100" | ||||
| const GoextVersionTimestamp = "2023-12-28T01:36:21+0100" | ||||
|   | ||||
| @@ -37,6 +37,14 @@ func (r GoJsonRender) Render(w http.ResponseWriter) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (r GoJsonRender) RenderString() (string, error) { | ||||
| 	jsonBytes, err := MarshalSafeCollections(r.Data, r.NilSafeSlices, r.NilSafeMaps, r.Indent, r.Filter) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	return string(jsonBytes), nil | ||||
| } | ||||
|  | ||||
| func (r GoJsonRender) WriteContentType(w http.ResponseWriter) { | ||||
| 	header := w.Header() | ||||
| 	if val := header["Content-Type"]; len(val) == 0 { | ||||
|   | ||||
							
								
								
									
										54
									
								
								googleapi/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								googleapi/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
|  | ||||
| Google OAuth Setup (to send mails) | ||||
| ================================== | ||||
|  | ||||
|  | ||||
|  - Login @ https://console.cloud.google.com | ||||
|  | ||||
|  - GMail API akivieren: https://console.cloud.google.com/apis/library/gmail.googleapis.com? | ||||
|  | ||||
|  - Create new Project (aka 'BackendMailAPI') @ https://console.cloud.google.com/projectcreate | ||||
|    User Type: Intern | ||||
|    Anwendungsname: 'BackendMailAPI' | ||||
|    Support-Email: ... | ||||
|    Authorisierte Domains: 'heydyno.de' (or project domain) | ||||
|    Kontakt-Email: ... | ||||
|     | ||||
|  | ||||
|  - Unter "Anmeldedaten" neuer OAuth Client erstellen @ https://console.cloud.google.com/apis/credentials | ||||
|    Anwendungstyp: Web | ||||
|    Name: 'BackendMailOAuth' | ||||
|    Redirect-Uri: 'http://localhost/oauth' | ||||
|    Client-ID und Client-Key merken | ||||
|  | ||||
|  - Open in Browser: | ||||
|    https://accounts.google.com/o/oauth2/v2/auth?redirect_uri=http://localhost/oauth&prompt=consent&response_type=code&client_id={...}&scope=https://www.googleapis.com/auth/gmail.send&access_type=offline | ||||
|    Code aus redirected URI merken | ||||
|  | ||||
|  - Code via request einlösen (und refresh_roken merken): | ||||
|  | ||||
| ``` | ||||
| curl --request POST \ | ||||
|   --url https://oauth2.googleapis.com/token \ | ||||
|   --data code={...} \ | ||||
|   --data redirect_uri=http://localhost/oauth \ | ||||
|   --data client_id={...} \ | ||||
|   --data client_secret={...} \ | ||||
|   --data grant_type=authorization_code \ | ||||
|   --data scope=https://www.googleapis.com/auth/gmail.send | ||||
| ``` | ||||
|  | ||||
|  - Fertig, mit `client_id`, `client_secret` und `refresh_token` kann das package benutzt werden | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										46
									
								
								googleapi/attachment.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								googleapi/attachment.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| type MailAttachment struct { | ||||
| 	IsInline    bool | ||||
| 	ContentType string | ||||
| 	Filename    string | ||||
| 	Data        []byte | ||||
| } | ||||
|  | ||||
| func (a MailAttachment) dump() []string { | ||||
| 	res := make([]string, 0, 4) | ||||
|  | ||||
| 	if a.ContentType != "" { | ||||
| 		res = append(res, "Content-Type: "+a.ContentType+"; charset=UTF-8") | ||||
| 	} | ||||
|  | ||||
| 	res = append(res, "Content-Transfer-Encoding: base64") | ||||
|  | ||||
| 	if a.IsInline { | ||||
| 		if a.Filename != "" { | ||||
| 			res = append(res, fmt.Sprintf("Content-Disposition: inline;filename=\"%s\"", a.Filename)) | ||||
| 		} else { | ||||
| 			res = append(res, "Content-Disposition: inline") | ||||
| 		} | ||||
| 	} else { | ||||
| 		if a.Filename != "" { | ||||
| 			res = append(res, fmt.Sprintf("Content-Disposition: attachment;filename=\"%s\"", a.Filename)) | ||||
| 		} else { | ||||
| 			res = append(res, "Content-Disposition: attachment") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b64 := base64.StdEncoding.EncodeToString(a.Data) | ||||
| 	for i := 0; i < len(b64); i += 80 { | ||||
| 		res = append(res, b64[i:min(i+80, len(b64))]) | ||||
| 	} | ||||
|  | ||||
| 	res = append(res) | ||||
|  | ||||
| 	return res | ||||
| } | ||||
							
								
								
									
										6
									
								
								googleapi/body.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								googleapi/body.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| package googleapi | ||||
|  | ||||
| type MailBody struct { | ||||
| 	Plain string | ||||
| 	HTML  string | ||||
| } | ||||
							
								
								
									
										224
									
								
								googleapi/mimeMessage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								googleapi/mimeMessage.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,224 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"mime" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // https://datatracker.ietf.org/doc/html/rfc2822 | ||||
| func encodeMimeMail(from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) string { | ||||
|  | ||||
| 	data := make([]string, 0, 32) | ||||
|  | ||||
| 	data = append(data, "Date: "+time.Now().Format(time.RFC1123Z)) | ||||
| 	data = append(data, "MIME-Version: 1.0") | ||||
| 	data = append(data, "From: "+mime.QEncoding.Encode("UTF-8", from)) | ||||
| 	data = append(data, "To: "+strings.Join(langext.ArrMap(recipients, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", ")) | ||||
| 	if len(cc) > 0 { | ||||
| 		data = append(data, "To: "+strings.Join(langext.ArrMap(cc, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", ")) | ||||
| 	} | ||||
| 	if len(bcc) > 0 { | ||||
| 		data = append(data, "Bcc: "+strings.Join(langext.ArrMap(bcc, func(v string) string { return mime.QEncoding.Encode("UTF-8", v) }), ", ")) | ||||
| 	} | ||||
| 	data = append(data, "Subject: "+mime.QEncoding.Encode("UTF-8", subject)) | ||||
|  | ||||
| 	hasInlineAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return v.IsInline }) | ||||
| 	hasNormalAttachments := langext.ArrAny(attachments, func(v MailAttachment) bool { return !v.IsInline }) | ||||
| 	hasPlain := body.Plain != "" | ||||
| 	hasHTML := body.HTML != "" | ||||
|  | ||||
| 	mixedBoundary := langext.MustRawHexUUID() | ||||
| 	relatedBoundary := langext.MustRawHexUUID() | ||||
| 	altBoundary := langext.MustRawHexUUID() | ||||
|  | ||||
| 	inlineAttachments := langext.ArrFilter(attachments, func(v MailAttachment) bool { return v.IsInline }) | ||||
| 	normalAttachments := langext.ArrFilter(attachments, func(v MailAttachment) bool { return !v.IsInline }) | ||||
|  | ||||
| 	if hasInlineAttachments && hasNormalAttachments { | ||||
| 		// "mixed+related" | ||||
|  | ||||
| 		data = append(data, "Content-Type: multipart/mixed; boundary="+mixedBoundary) | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, "--"+mixedBoundary) | ||||
|  | ||||
| 		data = append(data, "Content-Type: multipart/related; boundary="+relatedBoundary) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, relatedBoundary, altBoundary)...) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		for i, attachment := range inlineAttachments { | ||||
| 			data = append(data, "--"+relatedBoundary) | ||||
| 			data = append(data, attachment.dump()...) | ||||
|  | ||||
| 			if i < len(inlineAttachments)-1 { | ||||
| 				data = append(data, "") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		data = append(data, "--"+relatedBoundary+"--") | ||||
|  | ||||
| 		for i, attachment := range normalAttachments { | ||||
| 			data = append(data, "--"+mixedBoundary) | ||||
| 			data = append(data, attachment.dump()...) | ||||
|  | ||||
| 			if i < len(normalAttachments)-1 { | ||||
| 				data = append(data, "") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		data = append(data, "--"+mixedBoundary+"--") | ||||
|  | ||||
| 	} else if hasNormalAttachments { | ||||
| 		// "mixed" | ||||
|  | ||||
| 		data = append(data, "Content-Type: multipart/mixed; boundary="+mixedBoundary) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, mixedBoundary, altBoundary)...) | ||||
| 		if hasPlain && hasHTML { | ||||
| 			data = append(data, "") | ||||
| 		} | ||||
|  | ||||
| 		for i, attachment := range normalAttachments { | ||||
| 			data = append(data, "--"+mixedBoundary) | ||||
| 			data = append(data, attachment.dump()...) | ||||
|  | ||||
| 			if i < len(normalAttachments)-1 { | ||||
| 				data = append(data, "") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		data = append(data, "--"+mixedBoundary+"--") | ||||
|  | ||||
| 	} else if hasInlineAttachments { | ||||
| 		// "related" | ||||
|  | ||||
| 		data = append(data, "Content-Type: multipart/related; boundary="+relatedBoundary) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, relatedBoundary, altBoundary)...) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		for i, attachment := range inlineAttachments { | ||||
| 			data = append(data, "--"+relatedBoundary) | ||||
| 			data = append(data, attachment.dump()...) | ||||
|  | ||||
| 			if i < len(inlineAttachments)-1 { | ||||
| 				data = append(data, "") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		data = append(data, "--"+relatedBoundary+"--") | ||||
|  | ||||
| 	} else if hasPlain && hasHTML { | ||||
| 		// "alternative" | ||||
|  | ||||
| 		data = append(data, "Content-Type: multipart/alternative; boundary="+altBoundary) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		data = append(data, dumpMailBody(body, hasInlineAttachments, hasNormalAttachments, altBoundary, altBoundary)...) | ||||
| 		data = append(data, "") | ||||
|  | ||||
| 		data = append(data, "--"+altBoundary+"--") | ||||
|  | ||||
| 	} else if hasPlain { | ||||
| 		// "plain" | ||||
|  | ||||
| 		data = append(data, "Content-Type: text/plain; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.Plain) | ||||
|  | ||||
| 	} else if hasHTML { | ||||
| 		// "plain" | ||||
|  | ||||
| 		data = append(data, "Content-Type: text/html; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.HTML) | ||||
|  | ||||
| 	} else { | ||||
| 		// "empty??" | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	return strings.Join(data, "\r\n") | ||||
| } | ||||
|  | ||||
| func dumpMailBody(body MailBody, hasInlineAttachments bool, hasNormalAttachments bool, boundary string, boundaryAlt string) []string { | ||||
|  | ||||
| 	if body.HTML != "" && body.Plain != "" && !hasInlineAttachments && hasNormalAttachments { | ||||
| 		data := make([]string, 0, 16) | ||||
| 		data = append(data, "--"+boundary) | ||||
| 		data = append(data, "Content-Type: multipart/alternative; boundary="+boundaryAlt) | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, "--"+boundaryAlt) | ||||
| 		data = append(data, "Content-Type: text/plain; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.Plain) | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, "--"+boundaryAlt) | ||||
| 		data = append(data, "Content-Type: text/html; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.HTML) | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, "--"+boundaryAlt+"--") | ||||
| 		return data | ||||
| 	} | ||||
|  | ||||
| 	if body.HTML != "" && body.Plain != "" && hasInlineAttachments { | ||||
| 		data := make([]string, 0, 2) | ||||
| 		data = append(data, "--"+boundary) | ||||
| 		data = append(data, body.HTML) | ||||
| 		return data | ||||
| 	} | ||||
|  | ||||
| 	if body.HTML != "" && body.Plain != "" { | ||||
| 		data := make([]string, 0, 8) | ||||
| 		data = append(data, "--"+boundary) | ||||
| 		data = append(data, "Content-Type: text/plain; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.Plain) | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, "--"+boundary) | ||||
| 		data = append(data, "Content-Type: text/html; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.HTML) | ||||
| 		return data | ||||
| 	} | ||||
|  | ||||
| 	if body.HTML != "" { | ||||
| 		data := make([]string, 0, 2) | ||||
| 		data = append(data, "--"+boundary) | ||||
| 		data = append(data, "Content-Type: text/html; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.HTML) | ||||
| 		return data | ||||
| 	} | ||||
|  | ||||
| 	if body.Plain != "" { | ||||
| 		data := make([]string, 0, 2) | ||||
| 		data = append(data, "--"+boundary) | ||||
| 		data = append(data, "Content-Type: text/plain; charset=UTF-8") | ||||
| 		data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 		data = append(data, "") | ||||
| 		data = append(data, body.Plain) | ||||
| 		return data | ||||
| 	} | ||||
|  | ||||
| 	data := make([]string, 0, 16) | ||||
| 	data = append(data, "--"+boundary) | ||||
| 	data = append(data, "Content-Type: text/plain; charset=UTF-8") | ||||
| 	data = append(data, "Content-Transfer-Encoding: 7bit") | ||||
| 	data = append(data, "") | ||||
| 	data = append(data, "") // no content ?!? | ||||
| 	return data | ||||
| } | ||||
							
								
								
									
										80
									
								
								googleapi/mimeMessage_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								googleapi/mimeMessage_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestEncodeMimeMail(t *testing.T) { | ||||
|  | ||||
| 	mail := encodeMimeMail( | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail", | ||||
| 		MailBody{Plain: "Plain Text"}, | ||||
| 		nil) | ||||
|  | ||||
| 	verifyMime(mail) | ||||
| } | ||||
|  | ||||
| func TestEncodeMimeMail2(t *testing.T) { | ||||
|  | ||||
| 	mail := encodeMimeMail( | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail (alternative)", | ||||
| 		MailBody{ | ||||
| 			Plain: "Plain Text", | ||||
| 			HTML:  "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>", | ||||
| 		}, | ||||
| 		nil) | ||||
|  | ||||
| 	verifyMime(mail) | ||||
| } | ||||
|  | ||||
| func TestEncodeMimeMail3(t *testing.T) { | ||||
|  | ||||
| 	mail := encodeMimeMail( | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail (alternative)", | ||||
| 		MailBody{ | ||||
| 			HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>", | ||||
| 		}, | ||||
| 		[]MailAttachment{ | ||||
| 			{Data: []byte("HelloWorld"), Filename: "test.txt", IsInline: false, ContentType: "text/plain"}, | ||||
| 		}) | ||||
|  | ||||
| 	verifyMime(mail) | ||||
| } | ||||
|  | ||||
| func TestEncodeMimeMail4(t *testing.T) { | ||||
|  | ||||
| 	b := tst.Must(os.ReadFile("test_placeholder.png"))(t) | ||||
|  | ||||
| 	mail := encodeMimeMail( | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail (inline)", | ||||
| 		MailBody{ | ||||
| 			HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>", | ||||
| 		}, | ||||
| 		[]MailAttachment{ | ||||
| 			{Data: b, Filename: "img.png", IsInline: true, ContentType: "image/png"}, | ||||
| 		}) | ||||
|  | ||||
| 	verifyMime(mail) | ||||
| } | ||||
|  | ||||
| func verifyMime(mail string) { | ||||
| 	//fmt.Printf("%s\n\n", mail) | ||||
| } | ||||
							
								
								
									
										91
									
								
								googleapi/oAuth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								googleapi/oAuth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/timeext" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type GoogleOAuth interface { | ||||
| 	AccessToken() (string, error) | ||||
| } | ||||
|  | ||||
| type oauth struct { | ||||
| 	clientID     string | ||||
| 	clientSecret string | ||||
| 	refreshToken string | ||||
|  | ||||
| 	lock        sync.RWMutex | ||||
| 	accessToken *string | ||||
| 	expiryDate  *time.Time | ||||
| } | ||||
|  | ||||
| func NewGoogleOAuth(clientid string, clientsecret, refreshtoken string) GoogleOAuth { | ||||
| 	return &oauth{ | ||||
| 		clientID:     clientid, | ||||
| 		clientSecret: clientsecret, | ||||
| 		refreshToken: refreshtoken, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *oauth) AccessToken() (string, error) { | ||||
| 	c.lock.RLock() | ||||
| 	if c.accessToken != nil && c.expiryDate != nil && (*c.expiryDate).After(time.Now()) { | ||||
| 		c.lock.RUnlock() | ||||
| 		return *c.accessToken, nil // still valid | ||||
| 	} | ||||
| 	c.lock.RUnlock() | ||||
|  | ||||
| 	httpclient := http.Client{} | ||||
|  | ||||
| 	url := fmt.Sprintf("https://oauth2.googleapis.com/token?client_id=%s&client_secret=%s&grant_type=%s&refresh_token=%s", | ||||
| 		c.clientID, | ||||
| 		c.clientSecret, | ||||
| 		"refresh_token", | ||||
| 		c.refreshToken) | ||||
|  | ||||
| 	req, err := http.NewRequest(http.MethodPost, url, nil) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	reqStartTime := time.Now() | ||||
|  | ||||
| 	res, err := httpclient.Do(req) | ||||
|  | ||||
| 	type response struct { | ||||
| 		AccessToken string `json:"access_token"` | ||||
| 		ExpiresIn   int    `json:"expires_in"` | ||||
| 		Scope       string `json:"scope"` | ||||
| 		TokenType   string `json:"token_type"` | ||||
| 	} | ||||
|  | ||||
| 	var r response | ||||
|  | ||||
| 	data, err := io.ReadAll(res.Body) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	err = json.Unmarshal(data, &r) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if r.ExpiresIn == 0 || r.AccessToken == "" { | ||||
| 		return "", exerr.New(exerr.TypeGoogleResponse, "google oauth returned no response").Str("body", string(data)).Build() | ||||
| 	} | ||||
|  | ||||
| 	c.lock.Lock() | ||||
| 	c.expiryDate = langext.Ptr(reqStartTime.Add(timeext.FromSeconds(r.ExpiresIn - 10))) | ||||
| 	c.accessToken = langext.Ptr(r.AccessToken) | ||||
| 	c.lock.Unlock() | ||||
|  | ||||
| 	return r.AccessToken, nil | ||||
| } | ||||
							
								
								
									
										69
									
								
								googleapi/sendMail.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								googleapi/sendMail.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type MailRef struct { | ||||
| 	ID       string   `json:"id"` | ||||
| 	ThreadID string   `json:"threadId"` | ||||
| 	LabelIDs []string `json:"labelIds"` | ||||
| } | ||||
|  | ||||
| func (c *client) SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error) { | ||||
|  | ||||
| 	mm := encodeMimeMail(from, recipients, cc, bcc, subject, body, attachments) | ||||
|  | ||||
| 	tok, err := c.oauth.AccessToken() | ||||
| 	if err != nil { | ||||
| 		return MailRef{}, exerr.Wrap(err, "").Build() | ||||
| 	} | ||||
|  | ||||
| 	url := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/%s/messages/send?alt=json&prettyPrint=false", "me") | ||||
|  | ||||
| 	msgbody, err := json.Marshal(langext.H{"raw": base64.URLEncoding.EncodeToString([]byte(mm))}) | ||||
| 	if err != nil { | ||||
| 		return MailRef{}, exerr.Wrap(err, "").Build() | ||||
| 	} | ||||
|  | ||||
| 	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(msgbody)) | ||||
| 	if err != nil { | ||||
| 		return MailRef{}, exerr.Wrap(err, "").Build() | ||||
| 	} | ||||
|  | ||||
| 	req.Header.Add("Authorization", "Bearer "+tok) | ||||
| 	req.Header.Add("X-Goog-Api-Client", "blackforestbytes-goext/"+goext.GoextVersion) | ||||
| 	req.Header.Add("User-Agent", "blackforestbytes-goext/"+goext.GoextVersion) | ||||
| 	req.Header.Add("Content-Type", "application/json") | ||||
|  | ||||
| 	resp, err := c.http.Do(req) | ||||
| 	if err != nil { | ||||
| 		return MailRef{}, exerr.Wrap(err, "").Build() | ||||
| 	} | ||||
|  | ||||
| 	respBody, err := io.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return MailRef{}, exerr.Wrap(err, "").Build() | ||||
| 	} | ||||
|  | ||||
| 	if resp.StatusCode != 200 { | ||||
| 		return MailRef{}, exerr.New(exerr.TypeGoogleStatuscode, "gmail returned non-200 statuscode").Int("sc", resp.StatusCode).Str("body", string(respBody)).Build() | ||||
| 	} | ||||
|  | ||||
| 	var respObj MailRef | ||||
| 	err = json.Unmarshal(respBody, &respObj) | ||||
| 	if err != nil { | ||||
| 		return MailRef{}, exerr.Wrap(err, "").Str("body", string(respBody)).Build() | ||||
| 	} | ||||
|  | ||||
| 	return respObj, nil | ||||
| } | ||||
							
								
								
									
										151
									
								
								googleapi/sendMail_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								googleapi/sendMail_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/exerr" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/tst" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestMain(m *testing.M) { | ||||
| 	if !exerr.Initialized() { | ||||
| 		exerr.Init(exerr.ErrorPackageConfigInit{ZeroLogErrTraces: langext.PFalse, ZeroLogAllTraces: langext.PFalse}) | ||||
| 	} | ||||
| 	os.Exit(m.Run()) | ||||
| } | ||||
|  | ||||
| func TestSendMail1(t *testing.T) { | ||||
| 	t.Skip() | ||||
| 	return | ||||
|  | ||||
| 	auth := NewGoogleOAuth( | ||||
| 		"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com", | ||||
| 		"TODO", | ||||
| 		"TODO") | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	gclient := NewGoogleClient(auth) | ||||
|  | ||||
| 	mail, err := gclient.SendMail( | ||||
| 		ctx, | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail", | ||||
| 		MailBody{Plain: "Plain Text"}, | ||||
| 		nil) | ||||
|  | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	fmt.Printf("mail.ID        := %s\n", mail.ID) | ||||
| 	fmt.Printf("mail.ThreadID  := %s\n", mail.ThreadID) | ||||
| 	fmt.Printf("mail.LabelIDs  := %v\n", mail.LabelIDs) | ||||
| } | ||||
|  | ||||
| func TestSendMail2(t *testing.T) { | ||||
| 	t.Skip() | ||||
| 	return | ||||
|  | ||||
| 	auth := NewGoogleOAuth( | ||||
| 		"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com", | ||||
| 		"TODO", | ||||
| 		"TODO") | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	gclient := NewGoogleClient(auth) | ||||
|  | ||||
| 	mail, err := gclient.SendMail( | ||||
| 		ctx, | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail (alternative)", | ||||
| 		MailBody{ | ||||
| 			Plain: "Plain Text", | ||||
| 			HTML:  "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>", | ||||
| 		}, | ||||
| 		nil) | ||||
|  | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	fmt.Printf("mail.ID        := %s\n", mail.ID) | ||||
| 	fmt.Printf("mail.ThreadID  := %s\n", mail.ThreadID) | ||||
| 	fmt.Printf("mail.LabelIDs  := %v\n", mail.LabelIDs) | ||||
| } | ||||
|  | ||||
| func TestSendMail3(t *testing.T) { | ||||
| 	t.Skip() | ||||
| 	return | ||||
|  | ||||
| 	auth := NewGoogleOAuth( | ||||
| 		"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com", | ||||
| 		"TODO", | ||||
| 		"TODO") | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	gclient := NewGoogleClient(auth) | ||||
|  | ||||
| 	mail, err := gclient.SendMail( | ||||
| 		ctx, | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail (attach)", | ||||
| 		MailBody{ | ||||
| 			HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>", | ||||
| 		}, | ||||
| 		[]MailAttachment{ | ||||
| 			{Data: []byte("HelloWorld"), Filename: "test.txt", IsInline: false, ContentType: "text/plain"}, | ||||
| 		}) | ||||
|  | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	fmt.Printf("mail.ID        := %s\n", mail.ID) | ||||
| 	fmt.Printf("mail.ThreadID  := %s\n", mail.ThreadID) | ||||
| 	fmt.Printf("mail.LabelIDs  := %v\n", mail.LabelIDs) | ||||
| } | ||||
|  | ||||
| func TestSendMail4(t *testing.T) { | ||||
| 	t.Skip() | ||||
| 	return | ||||
|  | ||||
| 	auth := NewGoogleOAuth( | ||||
| 		"554617284247-8di0j6s5dcmlk4lmk4hdf9kdn8scss54.apps.googleusercontent.com", | ||||
| 		"TODO", | ||||
| 		"TODO") | ||||
|  | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	gclient := NewGoogleClient(auth) | ||||
|  | ||||
| 	b := tst.Must(os.ReadFile("test_placeholder.png"))(t) | ||||
|  | ||||
| 	mail, err := gclient.SendMail( | ||||
| 		ctx, | ||||
| 		"noreply@heydyno.de", | ||||
| 		[]string{"trash@mikescher.de"}, | ||||
| 		nil, | ||||
| 		nil, | ||||
| 		"Hello Test Mail (inline)", | ||||
| 		MailBody{ | ||||
| 			HTML: "<html><body><u>Non</u> Pl<i>ai</i>n T<b>ex</b>t</body></html>", | ||||
| 		}, | ||||
| 		[]MailAttachment{ | ||||
| 			{Data: b, Filename: "img.png", IsInline: true, ContentType: "image/png"}, | ||||
| 		}) | ||||
|  | ||||
| 	tst.AssertNoErr(t, err) | ||||
|  | ||||
| 	fmt.Printf("mail.ID        := %s\n", mail.ID) | ||||
| 	fmt.Printf("mail.ThreadID  := %s\n", mail.ThreadID) | ||||
| 	fmt.Printf("mail.LabelIDs  := %v\n", mail.LabelIDs) | ||||
| } | ||||
							
								
								
									
										22
									
								
								googleapi/service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								googleapi/service.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| package googleapi | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type GoogleClient interface { | ||||
| 	SendMail(ctx context.Context, from string, recipients []string, cc []string, bcc []string, subject string, body MailBody, attachments []MailAttachment) (MailRef, error) | ||||
| } | ||||
|  | ||||
| type client struct { | ||||
| 	oauth GoogleOAuth | ||||
| 	http  http.Client | ||||
| } | ||||
|  | ||||
| func NewGoogleClient(oauth GoogleOAuth) GoogleClient { | ||||
| 	return &client{ | ||||
| 		oauth: oauth, | ||||
| 		http:  http.Client{}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								googleapi/test_placeholder.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								googleapi/test_placeholder.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 11 KiB | 
| @@ -29,6 +29,14 @@ func ArrToMap[T comparable, V any](a []V, keyfunc func(V) T) map[T]V { | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func ArrToKVMap[T any, K comparable, V any](a []T, keyfunc func(T) K, valfunc func(T) V) map[K]V { | ||||
| 	result := make(map[K]V, len(a)) | ||||
| 	for _, v := range a { | ||||
| 		result[keyfunc(v)] = valfunc(v) | ||||
| 	} | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| func ArrToSet[T comparable](a []T) map[T]bool { | ||||
| 	result := make(map[T]bool, len(a)) | ||||
| 	for _, v := range a { | ||||
|   | ||||
| @@ -10,10 +10,23 @@ var PTrue = Ptr(true) | ||||
| // PFalse := &false | ||||
| var PFalse = Ptr(false) | ||||
|  | ||||
| // PNil := &nil | ||||
| var PNil = Ptr[any](nil) | ||||
|  | ||||
| func Ptr[T any](v T) *T { | ||||
| 	return &v | ||||
| } | ||||
|  | ||||
| func DblPtr[T any](v T) **T { | ||||
| 	v_ := &v | ||||
| 	return &v_ | ||||
| } | ||||
|  | ||||
| func DblPtrNil[T any]() **T { | ||||
| 	var v *T = nil | ||||
| 	return &v | ||||
| } | ||||
|  | ||||
| func PtrInt32(v int32) *int32 { | ||||
| 	return &v | ||||
| } | ||||
| @@ -35,7 +48,7 @@ func IsNil(i interface{}) bool { | ||||
| 		return true | ||||
| 	} | ||||
| 	switch reflect.TypeOf(i).Kind() { | ||||
| 	case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: | ||||
| 	case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice, reflect.Func, reflect.UnsafePointer: | ||||
| 		return reflect.ValueOf(i).IsNil() | ||||
| 	} | ||||
| 	return false | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"go.mongodb.org/mongo-driver/bson/bsoncodec" | ||||
| 	"go.mongodb.org/mongo-driver/bson/bsontype" | ||||
| 	"go.mongodb.org/mongo-driver/bson/primitive" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/rfctime" | ||||
| 	"reflect" | ||||
| ) | ||||
| @@ -18,9 +19,21 @@ 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.UnixTime{}), rfctime.UnixTime{}) | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.UnixTime{}), rfctime.UnixTime{}) | ||||
|  | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.UnixMilliTime{}), rfctime.UnixMilliTime{}) | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.UnixMilliTime{}), rfctime.UnixMilliTime{}) | ||||
|  | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.UnixNanoTime{}), rfctime.UnixNanoTime{}) | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.UnixNanoTime{}), rfctime.UnixNanoTime{}) | ||||
|  | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.Date{}), rfctime.Date{}) | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(&rfctime.Date{}), rfctime.Date{}) | ||||
|  | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(rfctime.SecondsF64(0)), rfctime.SecondsF64(0)) | ||||
| 	rb.RegisterTypeDecoder(reflect.TypeOf(langext.Ptr(rfctime.SecondsF64(0))), rfctime.SecondsF64(0)) | ||||
|  | ||||
| 	bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(rb) | ||||
| 	bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(rb) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ type Pagination struct { | ||||
| 	Limit            int `json:"limit"`            // max-page-size | ||||
| 	TotalPages       int `json:"totalPages"`       // total page-count | ||||
| 	TotalItems       int `json:"totalItems"`       // total items-count | ||||
| 	CurrentPageCount int `json:"currntPageCount"` // item-count in current page ( == len(data) ) | ||||
| 	CurrentPageCount int `json:"currentPageCount"` // item-count in current page ( == len(data) ) | ||||
| } | ||||
|  | ||||
| func CalcPaginationTotalPages(totalItems int, limit int) int { | ||||
|   | ||||
| @@ -2,7 +2,14 @@ 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" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/timeext" | ||||
| 	"reflect" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| @@ -54,6 +61,63 @@ func (d SecondsF64) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(secs) | ||||
| } | ||||
|  | ||||
| func (d *SecondsF64) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { | ||||
| 	if bt == bson.TypeNull { | ||||
| 		// 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 | ||||
| 		*d = SecondsF64(0) | ||||
| 		return nil | ||||
| 	} | ||||
| 	if bt != bson.TypeDouble { | ||||
| 		return errors.New(fmt.Sprintf("cannot unmarshal %v into SecondsF64", bt)) | ||||
| 	} | ||||
| 	var secValue float64 | ||||
| 	err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&secValue) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*d = SecondsF64(int64(secValue * float64(time.Second))) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (d SecondsF64) MarshalBSONValue() (bsontype.Type, []byte, error) { | ||||
| 	return bson.MarshalValue(d.Seconds()) | ||||
| } | ||||
|  | ||||
| func (d SecondsF64) 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 = d.UnmarshalBSONValue(tp, src) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if val.Kind() == reflect.Ptr { | ||||
| 		val.Set(reflect.ValueOf(&d)) | ||||
| 	} else { | ||||
| 		val.Set(reflect.ValueOf(d)) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NewSecondsF64(t time.Duration) SecondsF64 { | ||||
| 	return SecondsF64(t) | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,13 @@ 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" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
| @@ -59,6 +66,63 @@ func (t *UnixTime) UnmarshalText(data []byte) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *UnixTime) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { | ||||
| 	if bt == bson.TypeNull { | ||||
| 		// 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 = UnixTime{} | ||||
| 		return nil | ||||
| 	} | ||||
| 	if bt != bson.TypeDateTime { | ||||
| 		return errors.New(fmt.Sprintf("cannot unmarshal %v into UnixTime", bt)) | ||||
| 	} | ||||
| 	var tt time.Time | ||||
| 	err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*t = UnixTime(tt) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t UnixTime) MarshalBSONValue() (bsontype.Type, []byte, error) { | ||||
| 	return bson.MarshalValue(time.Time(t)) | ||||
| } | ||||
|  | ||||
| func (t UnixTime) 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 UnixTime) Serialize() string { | ||||
| 	return strconv.FormatInt(t.Time().Unix(), 10) | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,13 @@ 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" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
| @@ -59,6 +66,63 @@ func (t *UnixMilliTime) UnmarshalText(data []byte) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *UnixMilliTime) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { | ||||
| 	if bt == bson.TypeNull { | ||||
| 		// 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 = UnixMilliTime{} | ||||
| 		return nil | ||||
| 	} | ||||
| 	if bt != bson.TypeDateTime { | ||||
| 		return errors.New(fmt.Sprintf("cannot unmarshal %v into UnixMilliTime", bt)) | ||||
| 	} | ||||
| 	var tt time.Time | ||||
| 	err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*t = UnixMilliTime(tt) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t UnixMilliTime) MarshalBSONValue() (bsontype.Type, []byte, error) { | ||||
| 	return bson.MarshalValue(time.Time(t)) | ||||
| } | ||||
|  | ||||
| func (t UnixMilliTime) 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 UnixMilliTime) Serialize() string { | ||||
| 	return strconv.FormatInt(t.Time().UnixMilli(), 10) | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,13 @@ 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" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
| @@ -59,6 +66,63 @@ func (t *UnixNanoTime) UnmarshalText(data []byte) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *UnixNanoTime) UnmarshalBSONValue(bt bsontype.Type, data []byte) error { | ||||
| 	if bt == bson.TypeNull { | ||||
| 		// 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 = UnixNanoTime{} | ||||
| 		return nil | ||||
| 	} | ||||
| 	if bt != bson.TypeDateTime { | ||||
| 		return errors.New(fmt.Sprintf("cannot unmarshal %v into UnixNanoTime", bt)) | ||||
| 	} | ||||
| 	var tt time.Time | ||||
| 	err := bson.RawValue{Type: bt, Value: data}.Unmarshal(&tt) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*t = UnixNanoTime(tt) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t UnixNanoTime) MarshalBSONValue() (bsontype.Type, []byte, error) { | ||||
| 	return bson.MarshalValue(time.Time(t)) | ||||
| } | ||||
|  | ||||
| func (t UnixNanoTime) 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 UnixNanoTime) Serialize() string { | ||||
| 	return strconv.FormatInt(t.Time().UnixNano(), 10) | ||||
| } | ||||
|   | ||||
| @@ -1,33 +1,28 @@ | ||||
| package syncext | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type AtomicBool struct { | ||||
| 	v        bool | ||||
| 	listener map[string]chan bool | ||||
| 	lock     sync.Mutex | ||||
| type Atomic[T any] struct { | ||||
| 	v    T | ||||
| 	lock sync.RWMutex | ||||
| } | ||||
|  | ||||
| func NewAtomicBool(value bool) *AtomicBool { | ||||
| 	return &AtomicBool{ | ||||
| func NewAtomic[T any](value T) *Atomic[T] { | ||||
| 	return &Atomic[T]{ | ||||
| 		v:    value, | ||||
| 		listener: make(map[string]chan bool), | ||||
| 		lock:     sync.Mutex{}, | ||||
| 		lock: sync.RWMutex{}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) Get() bool { | ||||
| 	a.lock.Lock() | ||||
| 	defer a.lock.Unlock() | ||||
| func (a *Atomic[T]) Get() T { | ||||
| 	a.lock.RLock() | ||||
| 	defer a.lock.RUnlock() | ||||
| 	return a.v | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) Set(value bool) bool { | ||||
| func (a *Atomic[T]) Set(value T) T { | ||||
| 	a.lock.Lock() | ||||
| 	defer a.lock.Unlock() | ||||
|  | ||||
| @@ -35,79 +30,5 @@ func (a *AtomicBool) Set(value bool) bool { | ||||
|  | ||||
| 	a.v = value | ||||
|  | ||||
| 	for k, v := range a.listener { | ||||
| 		select { | ||||
| 		case v <- value: | ||||
| 			// message sent | ||||
| 		default: | ||||
| 			// no receiver on channel | ||||
| 			delete(a.listener, k) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return oldValue | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) Wait(waitFor bool) { | ||||
| 	_ = a.WaitWithContext(context.Background(), waitFor) | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) WaitWithTimeout(timeout time.Duration, waitFor bool) error { | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||||
| 	defer cancel() | ||||
| 	return a.WaitWithContext(ctx, waitFor) | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) WaitWithContext(ctx context.Context, waitFor bool) error { | ||||
| 	if err := ctx.Err(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if a.Get() == waitFor { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	uuid, _ := langext.NewHexUUID() | ||||
|  | ||||
| 	waitchan := make(chan bool) | ||||
|  | ||||
| 	a.lock.Lock() | ||||
| 	a.listener[uuid] = waitchan | ||||
| 	a.lock.Unlock() | ||||
| 	defer func() { | ||||
| 		a.lock.Lock() | ||||
| 		delete(a.listener, uuid) | ||||
| 		a.lock.Unlock() | ||||
| 	}() | ||||
|  | ||||
| 	for { | ||||
| 		if err := ctx.Err(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		timeOut := 1024 * time.Millisecond | ||||
|  | ||||
| 		if dl, ok := ctx.Deadline(); ok { | ||||
| 			timeOutMax := dl.Sub(time.Now()) | ||||
| 			if timeOutMax <= 0 { | ||||
| 				timeOut = 0 | ||||
| 			} else if 0 < timeOutMax && timeOutMax < timeOut { | ||||
| 				timeOut = timeOutMax | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if v, ok := ReadChannelWithTimeout(waitchan, timeOut); ok { | ||||
| 			if v == waitFor { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} else { | ||||
| 			if err := ctx.Err(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if a.Get() == waitFor { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										113
									
								
								syncext/bool.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								syncext/bool.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| package syncext | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"gogs.mikescher.com/BlackForestBytes/goext/langext" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type AtomicBool struct { | ||||
| 	v        bool | ||||
| 	listener map[string]chan bool | ||||
| 	lock     sync.Mutex | ||||
| } | ||||
|  | ||||
| func NewAtomicBool(value bool) *AtomicBool { | ||||
| 	return &AtomicBool{ | ||||
| 		v:        value, | ||||
| 		listener: make(map[string]chan bool), | ||||
| 		lock:     sync.Mutex{}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) Get() bool { | ||||
| 	a.lock.Lock() | ||||
| 	defer a.lock.Unlock() | ||||
| 	return a.v | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) Set(value bool) bool { | ||||
| 	a.lock.Lock() | ||||
| 	defer a.lock.Unlock() | ||||
|  | ||||
| 	oldValue := a.v | ||||
|  | ||||
| 	a.v = value | ||||
|  | ||||
| 	for k, v := range a.listener { | ||||
| 		select { | ||||
| 		case v <- value: | ||||
| 			// message sent | ||||
| 		default: | ||||
| 			// no receiver on channel | ||||
| 			delete(a.listener, k) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return oldValue | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) Wait(waitFor bool) { | ||||
| 	_ = a.WaitWithContext(context.Background(), waitFor) | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) WaitWithTimeout(timeout time.Duration, waitFor bool) error { | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), timeout) | ||||
| 	defer cancel() | ||||
| 	return a.WaitWithContext(ctx, waitFor) | ||||
| } | ||||
|  | ||||
| func (a *AtomicBool) WaitWithContext(ctx context.Context, waitFor bool) error { | ||||
| 	if err := ctx.Err(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if a.Get() == waitFor { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	uuid, _ := langext.NewHexUUID() | ||||
|  | ||||
| 	waitchan := make(chan bool) | ||||
|  | ||||
| 	a.lock.Lock() | ||||
| 	a.listener[uuid] = waitchan | ||||
| 	a.lock.Unlock() | ||||
| 	defer func() { | ||||
| 		a.lock.Lock() | ||||
| 		delete(a.listener, uuid) | ||||
| 		a.lock.Unlock() | ||||
| 	}() | ||||
|  | ||||
| 	for { | ||||
| 		if err := ctx.Err(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		timeOut := 1024 * time.Millisecond | ||||
|  | ||||
| 		if dl, ok := ctx.Deadline(); ok { | ||||
| 			timeOutMax := dl.Sub(time.Now()) | ||||
| 			if timeOutMax <= 0 { | ||||
| 				timeOut = 0 | ||||
| 			} else if 0 < timeOutMax && timeOutMax < timeOut { | ||||
| 				timeOut = timeOutMax | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if v, ok := ReadChannelWithTimeout(waitchan, timeOut); ok { | ||||
| 			if v == waitFor { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} else { | ||||
| 			if err := ctx.Err(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if a.Get() == waitFor { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -14,12 +14,12 @@ func (c *Coll[TData]) decodeSingle(ctx context.Context, dec Decodable) (TData, e | ||||
| 	if c.customDecoder != nil { | ||||
| 		res, err = (*c.customDecoder)(ctx, dec) | ||||
| 		if err != nil { | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to decode single entity with custom-decoder").Type("decoder", *c.customDecoder).Build() | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to decode single entity with custom-decoder").Type("decoder", *c.customDecoder).NoLog().Build() | ||||
| 		} | ||||
| 	} else { | ||||
| 		err = dec.Decode(&res) | ||||
| 		if err != nil { | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to decode single entity").Type("target-type", res).Build() | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to decode single entity").Type("target-type", res).NoLog().Build() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -75,12 +75,12 @@ func (c *Coll[TData]) decodeSingleOrRequery(ctx context.Context, dec Decodable) | ||||
| 		var res genDoc | ||||
| 		err := dec.Decode(&res) | ||||
| 		if err != nil { | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to ID-decode entity").Build() | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to ID-decode entity").NoLog().Build() | ||||
| 		} | ||||
|  | ||||
| 		v, err := c.findOneInternal(ctx, bson.M{"_id": res.ID}, false) | ||||
| 		if err != nil { | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to re-query entity").Any("_id", res.ID).Build() | ||||
| 			return *new(TData), exerr.Wrap(err, "failed to re-query entity").Any("_id", res.ID).NoLog().Build() | ||||
| 		} | ||||
|  | ||||
| 		return *v, nil | ||||
|   | ||||
| @@ -58,7 +58,7 @@ func (c *Coll[TData]) findOneInternal(ctx context.Context, filter bson.M, allowN | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return nil, exerr.Wrap(err, "mongo-query[find-one] failed").Any("filter", filter).Str("collection", c.Name()).Build() | ||||
| 			return nil, exerr.Wrap(err, "mongo-query[find-one] failed").Any("filter", filter).Str("collection", c.Name()).NoLog().Build() | ||||
| 		} | ||||
|  | ||||
| 		return &res, nil | ||||
| @@ -75,19 +75,19 @@ func (c *Coll[TData]) findOneInternal(ctx context.Context, filter bson.M, allowN | ||||
|  | ||||
| 		cursor, err := c.coll.Aggregate(ctx, pipeline) | ||||
| 		if err != nil { | ||||
| 			return nil, exerr.Wrap(err, "mongo-aggregation [find-one] failed").Any("pipeline", pipeline).Str("collection", c.Name()).Build() | ||||
| 			return nil, exerr.Wrap(err, "mongo-aggregation [find-one] failed").Any("pipeline", pipeline).Str("collection", c.Name()).NoLog().Build() | ||||
| 		} | ||||
|  | ||||
| 		if cursor.Next(ctx) { | ||||
| 			v, err := c.decodeSingle(ctx, cursor) | ||||
| 			if err != nil { | ||||
| 				return nil, exerr.Wrap(err, "mongo-aggregation [find-one] failed").Any("pipeline", pipeline).Str("collection", c.Name()).Build() | ||||
| 				return nil, exerr.Wrap(err, "mongo-aggregation [find-one] failed to decode results").Any("pipeline", pipeline).Str("collection", c.Name()).NoLog().Build() | ||||
| 			} | ||||
| 			return &v, nil | ||||
| 		} else if allowNull { | ||||
| 			return nil, nil | ||||
| 		} else { | ||||
| 			return nil, exerr.Wrap(err, "mongo-aggregation [find-one] failed").Any("pipeline", pipeline).Str("collection", c.Name()).Build() | ||||
| 			return nil, exerr.Wrap(mongo.ErrNoDocuments, "mongo-aggregation [find-one] returned no documents").Any("pipeline", pipeline).Str("collection", c.Name()).NoLog().Build() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -42,8 +42,8 @@ func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.Filter, page int, | ||||
| 	pipelineCount := mongo.Pipeline{} | ||||
| 	pipelineCount = append(pipelineCount, bson.D{{Key: "$count", Value: "count"}}) | ||||
|  | ||||
| 	pipelineList := langext.ArrConcat(mongo.Pipeline{}, pipelineFilter, pipelinePaginate, c.extraModPipeline) | ||||
| 	pipelineTotalCount := langext.ArrConcat(mongo.Pipeline{}, pipelineFilter, pipelineCount) | ||||
| 	pipelineList := langext.ArrConcat(pipelineFilter, pipelineSort, pipelinePaginate, c.extraModPipeline, pipelineSort) | ||||
| 	pipelineTotalCount := langext.ArrConcat(pipelineFilter, pipelineCount) | ||||
|  | ||||
| 	cursorList, err := c.coll.Aggregate(ctx, pipelineList) | ||||
| 	if err != nil { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user